Skip to content

Commit ea52c91

Browse files
winterlamoneps1lon
andauthored
feat(ByRole): Add 'level' option for *ByRole('heading') (#757)
Co-authored-by: Sebastian Silbermann <[email protected]>
1 parent a5f8657 commit ea52c91

File tree

5 files changed

+84
-3
lines changed

5 files changed

+84
-3
lines changed

src/__tests__/ariaAttributes.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,38 @@ test('`pressed: true|false` matches `pressed` elements with proper role', () =>
167167
expect(getByRole('button', {pressed: true})).toBeInTheDocument()
168168
expect(getByRole('button', {pressed: false})).toBeInTheDocument()
169169
})
170+
171+
test('`level` matches elements with `heading` role', () => {
172+
const {getAllByRole, queryByRole} = renderIntoDocument(
173+
`<div>
174+
<h1 id="heading-one">H1</h1>
175+
<h2 id="first-heading-two">First H2</h2>
176+
<h3 id="heading-three">H3</h3>
177+
<div role="heading" aria-level="2" id="second-heading-two">Second H2</div>
178+
</div>`,
179+
)
180+
181+
expect(getAllByRole('heading', {level: 1}).map(({id}) => id)).toEqual([
182+
'heading-one',
183+
])
184+
185+
expect(getAllByRole('heading', {level: 2}).map(({id}) => id)).toEqual([
186+
'first-heading-two',
187+
'second-heading-two',
188+
])
189+
190+
expect(getAllByRole('heading', {level: 3}).map(({id}) => id)).toEqual([
191+
'heading-three',
192+
])
193+
194+
expect(queryByRole('heading', {level: 4})).not.toBeInTheDocument()
195+
})
196+
197+
test('`level` throws on unsupported roles', () => {
198+
const {getByRole} = render(`<button>Button</button>`)
199+
expect(() =>
200+
getByRole('button', {level: 3}),
201+
).toThrowErrorMatchingInlineSnapshot(
202+
`"Role \\"button\\" cannot have \\"level\\" property."`,
203+
)
204+
})

src/queries/role.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
computeAriaSelected,
55
computeAriaChecked,
66
computeAriaPressed,
7+
computeHeadingLevel,
78
getImplicitAriaRoles,
89
prettyRoles,
910
isInaccessible,
@@ -33,6 +34,7 @@ function queryAllByRole(
3334
selected,
3435
checked,
3536
pressed,
37+
level,
3638
} = {},
3739
) {
3840
checkContainerType(container)
@@ -60,6 +62,13 @@ function queryAllByRole(
6062
}
6163
}
6264

65+
if (level !== undefined) {
66+
// guard against using `level` option with any role other than `heading`
67+
if (role !== 'heading') {
68+
throw new Error(`Role "${role}" cannot have "level" property.`)
69+
}
70+
}
71+
6372
const subtreeIsInaccessibleCache = new WeakMap()
6473
function cachedIsSubtreeInaccessible(element) {
6574
if (!subtreeIsInaccessibleCache.has(element)) {
@@ -106,6 +115,9 @@ function queryAllByRole(
106115
if (pressed !== undefined) {
107116
return pressed === computeAriaPressed(element)
108117
}
118+
if (level !== undefined) {
119+
return level === computeHeadingLevel(element)
120+
}
109121
// don't care if aria attributes are unspecified
110122
return true
111123
})

src/role-helpers.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,30 @@ function checkBooleanAttribute(element, attribute) {
233233
return undefined
234234
}
235235

236+
/**
237+
* @param {Element} element -
238+
* @returns {number | undefined} - number if implicit heading or aria-level present, otherwise undefined
239+
*/
240+
function computeHeadingLevel(element) {
241+
// https://w3c.github.io/html-aam/#el-h1-h6
242+
// https://w3c.github.io/html-aam/#el-h1-h6
243+
const implicitHeadingLevels = {
244+
H1: 1,
245+
H2: 2,
246+
H3: 3,
247+
H4: 4,
248+
H5: 5,
249+
H6: 6,
250+
}
251+
// explicit aria-level value
252+
// https://www.w3.org/TR/wai-aria-1.2/#aria-level
253+
const ariaLevelAttribute =
254+
element.getAttribute('aria-level') &&
255+
Number(element.getAttribute('aria-level'))
256+
257+
return ariaLevelAttribute || implicitHeadingLevels[element.tagName]
258+
}
259+
236260
export {
237261
getRoles,
238262
logRoles,
@@ -243,4 +267,5 @@ export {
243267
computeAriaSelected,
244268
computeAriaChecked,
245269
computeAriaPressed,
270+
computeHeadingLevel,
246271
}

types/queries.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ export interface ByRoleOptions extends MatcherOptions {
8888
* pressed in the accessibility tree, i.e., `aria-pressed="true"`
8989
*/
9090
pressed?: boolean
91+
/**
92+
* Includes elements with the `"heading"` role matching the indicated level,
93+
* either by the semantic HTML heading elements `<h1>-<h6>` or matching
94+
* the `aria-level` attribute.
95+
*/
96+
level?: number
9197
/**
9298
* Includes every role used in the `role` attribute
9399
* For example *ByRole('progressbar', {queryFallbacks: true})` will find <div role="meter progressbar">`.

types/role-helpers.d.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
export function logRoles(container: HTMLElement): string;
2-
export function getRoles(container: HTMLElement): { [index: string]: HTMLElement[] };
1+
export function logRoles(container: HTMLElement): string
2+
export function getRoles(
3+
container: HTMLElement,
4+
): {[index: string]: HTMLElement[]}
35
/**
46
* https://testing-library.com/docs/dom-testing-library/api-helpers#isinaccessible
57
*/
6-
export function isInaccessible(element: Element): boolean;
8+
export function isInaccessible(element: Element): boolean
9+
export function computeHeadingLevel(element: Element): number | undefined

0 commit comments

Comments
 (0)