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