Skip to content

Commit eadf748

Browse files
authored
feat(ByRole): Allow filter by value state (#1223)
* feat(ByRole): All filter by value state * Fix coverage
1 parent 8c40a21 commit eadf748

File tree

4 files changed

+223
-0
lines changed

4 files changed

+223
-0
lines changed

Diff for: src/__tests__/ariaAttributes.js

+99
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,105 @@ test('`level` throws on unsupported roles', () => {
259259
)
260260
})
261261

262+
test('`value.now` throws on unsupported roles', () => {
263+
const {getByRole} = render(`<button aria-valuenow="1">Button</button>`)
264+
expect(() =>
265+
getByRole('button', {value: {now: 1}}),
266+
).toThrowErrorMatchingInlineSnapshot(
267+
`"aria-valuenow" is not supported on role "button".`,
268+
)
269+
})
270+
271+
test('`value.now: number` matches `aria-valuenow` on widgets', () => {
272+
const {getByRole} = renderIntoDocument(
273+
`<div>
274+
<button role="spinbutton" />
275+
<button role="spinbutton" aria-valuenow="5" />
276+
<button role="spinbutton" aria-valuenow="10" />
277+
</div>`,
278+
)
279+
expect(getByRole('spinbutton', {value: {now: 5}})).toBeInTheDocument()
280+
expect(getByRole('spinbutton', {value: {now: 10}})).toBeInTheDocument()
281+
})
282+
283+
test('`value.max` throws on unsupported roles', () => {
284+
const {getByRole} = render(`<button aria-valuemax="1">Button</button>`)
285+
expect(() =>
286+
getByRole('button', {value: {max: 1}}),
287+
).toThrowErrorMatchingInlineSnapshot(
288+
`"aria-valuemax" is not supported on role "button".`,
289+
)
290+
})
291+
292+
test('`value.max: number` matches `aria-valuemax` on widgets', () => {
293+
const {getByRole} = renderIntoDocument(
294+
`<div>
295+
<button role="spinbutton" />
296+
<button role="spinbutton" aria-valuemax="5" />
297+
<button role="spinbutton" aria-valuemax="10" />
298+
</div>`,
299+
)
300+
expect(getByRole('spinbutton', {value: {max: 5}})).toBeInTheDocument()
301+
expect(getByRole('spinbutton', {value: {max: 10}})).toBeInTheDocument()
302+
})
303+
304+
test('`value.min` throws on unsupported roles', () => {
305+
const {getByRole} = render(`<button aria-valuemin="1">Button</button>`)
306+
expect(() =>
307+
getByRole('button', {value: {min: 1}}),
308+
).toThrowErrorMatchingInlineSnapshot(
309+
`"aria-valuemin" is not supported on role "button".`,
310+
)
311+
})
312+
313+
test('`value.min: number` matches `aria-valuemin` on widgets', () => {
314+
const {getByRole} = renderIntoDocument(
315+
`<div>
316+
<button role="spinbutton" />
317+
<button role="spinbutton" aria-valuemin="5" />
318+
<button role="spinbutton" aria-valuemin="10" />
319+
</div>`,
320+
)
321+
expect(getByRole('spinbutton', {value: {min: 5}})).toBeInTheDocument()
322+
expect(getByRole('spinbutton', {value: {min: 10}})).toBeInTheDocument()
323+
})
324+
325+
test('`value.text` throws on unsupported roles', () => {
326+
const {getByRole} = render(`<button aria-valuetext="one">Button</button>`)
327+
expect(() =>
328+
getByRole('button', {value: {text: 'one'}}),
329+
).toThrowErrorMatchingInlineSnapshot(
330+
`"aria-valuetext" is not supported on role "button".`,
331+
)
332+
})
333+
334+
test('`value.text: Matcher` matches `aria-valuetext` on widgets', () => {
335+
const {getAllByRole, getByRole} = renderIntoDocument(
336+
`<div>
337+
<button role="spinbutton" />
338+
<button role="spinbutton" aria-valuetext="zero" />
339+
<button role="spinbutton" aria-valuetext="few" />
340+
<button role="spinbutton" aria-valuetext="many" />
341+
</div>`,
342+
)
343+
expect(getByRole('spinbutton', {value: {text: 'zero'}})).toBeInTheDocument()
344+
expect(getAllByRole('spinbutton', {value: {text: /few|many/}})).toHaveLength(
345+
2,
346+
)
347+
})
348+
349+
test('`value.*` must all match if specified', () => {
350+
const {getByRole} = renderIntoDocument(
351+
`<div>
352+
<button role="spinbutton" aria-valuemin="0" aria-valuenow="1" aria-valuemax="10" aria-valuetext="eins" />
353+
<button role="spinbutton" aria-valuemin="0" aria-valuenow="1" aria-valuemax="10" aria-valuetext="one" />
354+
</div>`,
355+
)
356+
expect(
357+
getByRole('spinbutton', {value: {now: 1, text: 'one'}}),
358+
).toBeInTheDocument()
359+
})
360+
262361
test('`expanded: true|false` matches `expanded` buttons', () => {
263362
const {getByRole} = renderIntoDocument(
264363
`<div>

Diff for: src/queries/role.ts

+78
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable complexity */
12
import {
23
computeAccessibleDescription,
34
computeAccessibleName,
@@ -14,6 +15,10 @@ import {
1415
computeAriaPressed,
1516
computeAriaCurrent,
1617
computeAriaExpanded,
18+
computeAriaValueNow,
19+
computeAriaValueMax,
20+
computeAriaValueMin,
21+
computeAriaValueText,
1722
computeHeadingLevel,
1823
getImplicitAriaRoles,
1924
prettyRoles,
@@ -49,6 +54,12 @@ const queryAllByRole: AllByRole = (
4954
current,
5055
level,
5156
expanded,
57+
value: {
58+
now: valueNow,
59+
min: valueMin,
60+
max: valueMax,
61+
text: valueText,
62+
} = {} as NonNullable<ByRoleOptions['value']>,
5263
} = {},
5364
) => {
5465
checkContainerType(container)
@@ -113,6 +124,46 @@ const queryAllByRole: AllByRole = (
113124
}
114125
}
115126

127+
if (valueNow !== undefined) {
128+
// guard against unknown roles
129+
if (
130+
allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-valuenow'] ===
131+
undefined
132+
) {
133+
throw new Error(`"aria-valuenow" is not supported on role "${role}".`)
134+
}
135+
}
136+
137+
if (valueMax !== undefined) {
138+
// guard against unknown roles
139+
if (
140+
allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-valuemax'] ===
141+
undefined
142+
) {
143+
throw new Error(`"aria-valuemax" is not supported on role "${role}".`)
144+
}
145+
}
146+
147+
if (valueMin !== undefined) {
148+
// guard against unknown roles
149+
if (
150+
allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-valuemin'] ===
151+
undefined
152+
) {
153+
throw new Error(`"aria-valuemin" is not supported on role "${role}".`)
154+
}
155+
}
156+
157+
if (valueText !== undefined) {
158+
// guard against unknown roles
159+
if (
160+
allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-valuetext'] ===
161+
undefined
162+
) {
163+
throw new Error(`"aria-valuetext" is not supported on role "${role}".`)
164+
}
165+
}
166+
116167
if (expanded !== undefined) {
117168
// guard against unknown roles
118169
if (
@@ -182,6 +233,33 @@ const queryAllByRole: AllByRole = (
182233
if (level !== undefined) {
183234
return level === computeHeadingLevel(element)
184235
}
236+
if (
237+
valueNow !== undefined ||
238+
valueMax !== undefined ||
239+
valueMin !== undefined ||
240+
valueText !== undefined
241+
) {
242+
let valueMatches = true
243+
if (valueNow !== undefined) {
244+
valueMatches &&= valueNow === computeAriaValueNow(element)
245+
}
246+
if (valueMax !== undefined) {
247+
valueMatches &&= valueMax === computeAriaValueMax(element)
248+
}
249+
if (valueMin !== undefined) {
250+
valueMatches &&= valueMin === computeAriaValueMin(element)
251+
}
252+
if (valueText !== undefined) {
253+
valueMatches &&= matches(
254+
computeAriaValueText(element) ?? null,
255+
element,
256+
valueText,
257+
text => text,
258+
)
259+
}
260+
261+
return valueMatches
262+
}
185263
// don't care if aria attributes are unspecified
186264
return true
187265
})

Diff for: src/role-helpers.js

+40
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,42 @@ function computeHeadingLevel(element) {
334334
return ariaLevelAttribute || implicitHeadingLevels[element.tagName]
335335
}
336336

337+
/**
338+
* @param {Element} element -
339+
* @returns {number | undefined} -
340+
*/
341+
function computeAriaValueNow(element) {
342+
const valueNow = element.getAttribute('aria-valuenow')
343+
return valueNow === null ? undefined : +valueNow
344+
}
345+
346+
/**
347+
* @param {Element} element -
348+
* @returns {number | undefined} -
349+
*/
350+
function computeAriaValueMax(element) {
351+
const valueMax = element.getAttribute('aria-valuemax')
352+
return valueMax === null ? undefined : +valueMax
353+
}
354+
355+
/**
356+
* @param {Element} element -
357+
* @returns {number | undefined} -
358+
*/
359+
function computeAriaValueMin(element) {
360+
const valueMin = element.getAttribute('aria-valuemin')
361+
return valueMin === null ? undefined : +valueMin
362+
}
363+
364+
/**
365+
* @param {Element} element -
366+
* @returns {string | undefined} -
367+
*/
368+
function computeAriaValueText(element) {
369+
const valueText = element.getAttribute('aria-valuetext')
370+
return valueText === null ? undefined : valueText
371+
}
372+
337373
export {
338374
getRoles,
339375
logRoles,
@@ -347,5 +383,9 @@ export {
347383
computeAriaPressed,
348384
computeAriaCurrent,
349385
computeAriaExpanded,
386+
computeAriaValueNow,
387+
computeAriaValueMax,
388+
computeAriaValueMin,
389+
computeAriaValueText,
350390
computeHeadingLevel,
351391
}

Diff for: types/queries.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ export interface ByRoleOptions {
110110
* the `aria-level` attribute.
111111
*/
112112
level?: number
113+
value?: {
114+
now?: number
115+
min?: number
116+
max?: number
117+
text?: Matcher
118+
}
113119
/**
114120
* Includes every role used in the `role` attribute
115121
* For example *ByRole('progressbar', {queryFallbacks: true})` will find <div role="meter progressbar">`.

0 commit comments

Comments
 (0)