diff --git a/src/__tests__/ariaAttributes.js b/src/__tests__/ariaAttributes.js
index e207721a..6ee4bb2c 100644
--- a/src/__tests__/ariaAttributes.js
+++ b/src/__tests__/ariaAttributes.js
@@ -167,3 +167,38 @@ test('`pressed: true|false` matches `pressed` elements with proper role', () =>
expect(getByRole('button', {pressed: true})).toBeInTheDocument()
expect(getByRole('button', {pressed: false})).toBeInTheDocument()
})
+
+test('`level` matches elements with `heading` role', () => {
+ const {getAllByRole, queryByRole} = renderIntoDocument(
+ `
+
H1
+
First H2
+
H3
+
Second H2
+
`,
+ )
+
+ expect(getAllByRole('heading', {level: 1}).map(({id}) => id)).toEqual([
+ 'heading-one',
+ ])
+
+ expect(getAllByRole('heading', {level: 2}).map(({id}) => id)).toEqual([
+ 'first-heading-two',
+ 'second-heading-two',
+ ])
+
+ expect(getAllByRole('heading', {level: 3}).map(({id}) => id)).toEqual([
+ 'heading-three',
+ ])
+
+ expect(queryByRole('heading', {level: 4})).not.toBeInTheDocument()
+})
+
+test('`level` throws on unsupported roles', () => {
+ const {getByRole} = render(``)
+ expect(() =>
+ getByRole('button', {level: 3}),
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"Role \\"button\\" cannot have \\"level\\" property."`,
+ )
+})
diff --git a/src/queries/role.js b/src/queries/role.js
index e3403a05..3102de9f 100644
--- a/src/queries/role.js
+++ b/src/queries/role.js
@@ -4,6 +4,7 @@ import {
computeAriaSelected,
computeAriaChecked,
computeAriaPressed,
+ computeHeadingLevel,
getImplicitAriaRoles,
prettyRoles,
isInaccessible,
@@ -33,6 +34,7 @@ function queryAllByRole(
selected,
checked,
pressed,
+ level,
} = {},
) {
checkContainerType(container)
@@ -60,6 +62,13 @@ function queryAllByRole(
}
}
+ if (level !== undefined) {
+ // guard against using `level` option with any role other than `heading`
+ if (role !== 'heading') {
+ throw new Error(`Role "${role}" cannot have "level" property.`)
+ }
+ }
+
const subtreeIsInaccessibleCache = new WeakMap()
function cachedIsSubtreeInaccessible(element) {
if (!subtreeIsInaccessibleCache.has(element)) {
@@ -106,6 +115,9 @@ function queryAllByRole(
if (pressed !== undefined) {
return pressed === computeAriaPressed(element)
}
+ if (level !== undefined) {
+ return level === computeHeadingLevel(element)
+ }
// don't care if aria attributes are unspecified
return true
})
diff --git a/src/role-helpers.js b/src/role-helpers.js
index c56b9599..816e592e 100644
--- a/src/role-helpers.js
+++ b/src/role-helpers.js
@@ -233,6 +233,30 @@ function checkBooleanAttribute(element, attribute) {
return undefined
}
+/**
+ * @param {Element} element -
+ * @returns {number | undefined} - number if implicit heading or aria-level present, otherwise undefined
+ */
+function computeHeadingLevel(element) {
+ // https://w3c.github.io/html-aam/#el-h1-h6
+ // https://w3c.github.io/html-aam/#el-h1-h6
+ const implicitHeadingLevels = {
+ H1: 1,
+ H2: 2,
+ H3: 3,
+ H4: 4,
+ H5: 5,
+ H6: 6,
+ }
+ // explicit aria-level value
+ // https://www.w3.org/TR/wai-aria-1.2/#aria-level
+ const ariaLevelAttribute =
+ element.getAttribute('aria-level') &&
+ Number(element.getAttribute('aria-level'))
+
+ return ariaLevelAttribute || implicitHeadingLevels[element.tagName]
+}
+
export {
getRoles,
logRoles,
@@ -243,4 +267,5 @@ export {
computeAriaSelected,
computeAriaChecked,
computeAriaPressed,
+ computeHeadingLevel,
}
diff --git a/types/queries.d.ts b/types/queries.d.ts
index 13056da7..8418d99c 100644
--- a/types/queries.d.ts
+++ b/types/queries.d.ts
@@ -88,6 +88,12 @@ export interface ByRoleOptions extends MatcherOptions {
* pressed in the accessibility tree, i.e., `aria-pressed="true"`
*/
pressed?: boolean
+ /**
+ * Includes elements with the `"heading"` role matching the indicated level,
+ * either by the semantic HTML heading elements `
-
` or matching
+ * the `aria-level` attribute.
+ */
+ level?: number
/**
* Includes every role used in the `role` attribute
* For example *ByRole('progressbar', {queryFallbacks: true})` will find
`.
diff --git a/types/role-helpers.d.ts b/types/role-helpers.d.ts
index 3dd35b78..4aaf54b0 100644
--- a/types/role-helpers.d.ts
+++ b/types/role-helpers.d.ts
@@ -1,6 +1,9 @@
-export function logRoles(container: HTMLElement): string;
-export function getRoles(container: HTMLElement): { [index: string]: HTMLElement[] };
+export function logRoles(container: HTMLElement): string
+export function getRoles(
+ container: HTMLElement,
+): {[index: string]: HTMLElement[]}
/**
* https://testing-library.com/docs/dom-testing-library/api-helpers#isinaccessible
*/
-export function isInaccessible(element: Element): boolean;
+export function isInaccessible(element: Element): boolean
+export function computeHeadingLevel(element: Element): number | undefined