Skip to content

Commit eb7ad05

Browse files
committed
feat(ByRole): All filter by value state
1 parent d1ff495 commit eb7ad05

File tree

4 files changed

+219
-0
lines changed

4 files changed

+219
-0
lines changed

src/__tests__/ariaAttributes.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,101 @@ test('`level` throws on unsupported roles', () => {
237237
)
238238
})
239239

240+
test('`value.now` throws on unsupported roles', () => {
241+
const {getByRole} = render(`<button aria-valuenow="1">Button</button>`)
242+
expect(() =>
243+
getByRole('button', {value: {now: 1}}),
244+
).toThrowErrorMatchingInlineSnapshot(
245+
`"aria-valuenow" is not supported on role "button".`,
246+
)
247+
})
248+
249+
test('`value.now: number` matches `aria-valuenow` on widgets', () => {
250+
const {getByRole} = renderIntoDocument(
251+
`<div>
252+
<button role="spinbutton" aria-valuenow="5" />
253+
<button role="spinbutton" aria-valuenow="10" />
254+
</div>`,
255+
)
256+
expect(getByRole('spinbutton', {value: {now: 5}})).toBeInTheDocument()
257+
expect(getByRole('spinbutton', {value: {now: 10}})).toBeInTheDocument()
258+
})
259+
260+
test('`value.max` throws on unsupported roles', () => {
261+
const {getByRole} = render(`<button aria-valuemax="1">Button</button>`)
262+
expect(() =>
263+
getByRole('button', {value: {max: 1}}),
264+
).toThrowErrorMatchingInlineSnapshot(
265+
`"aria-valuemax" is not supported on role "button".`,
266+
)
267+
})
268+
269+
test('`value.max: number` matches `aria-valuemax` on widgets', () => {
270+
const {getByRole} = renderIntoDocument(
271+
`<div>
272+
<button role="spinbutton" aria-valuemax="5" />
273+
<button role="spinbutton" aria-valuemax="10" />
274+
</div>`,
275+
)
276+
expect(getByRole('spinbutton', {value: {max: 5}})).toBeInTheDocument()
277+
expect(getByRole('spinbutton', {value: {max: 10}})).toBeInTheDocument()
278+
})
279+
280+
test('`value.min` throws on unsupported roles', () => {
281+
const {getByRole} = render(`<button aria-valuemin="1">Button</button>`)
282+
expect(() =>
283+
getByRole('button', {value: {min: 1}}),
284+
).toThrowErrorMatchingInlineSnapshot(
285+
`"aria-valuemin" is not supported on role "button".`,
286+
)
287+
})
288+
289+
test('`value.min: number` matches `aria-valuemin` on widgets', () => {
290+
const {getByRole} = renderIntoDocument(
291+
`<div>
292+
<button role="spinbutton" aria-valuemin="5" />
293+
<button role="spinbutton" aria-valuemin="10" />
294+
</div>`,
295+
)
296+
expect(getByRole('spinbutton', {value: {min: 5}})).toBeInTheDocument()
297+
expect(getByRole('spinbutton', {value: {min: 10}})).toBeInTheDocument()
298+
})
299+
300+
test('`value.text` throws on unsupported roles', () => {
301+
const {getByRole} = render(`<button aria-valuetext="one">Button</button>`)
302+
expect(() =>
303+
getByRole('button', {value: {text: 'one'}}),
304+
).toThrowErrorMatchingInlineSnapshot(
305+
`"aria-valuetext" is not supported on role "button".`,
306+
)
307+
})
308+
309+
test('`value.text: Matcher` matches `aria-valuetext` on widgets', () => {
310+
const {getAllByRole, getByRole} = renderIntoDocument(
311+
`<div>
312+
<button role="spinbutton" aria-valuetext="zero" />
313+
<button role="spinbutton" aria-valuetext="few" />
314+
<button role="spinbutton" aria-valuetext="many" />
315+
</div>`,
316+
)
317+
expect(getByRole('spinbutton', {value: {text: 'zero'}})).toBeInTheDocument()
318+
expect(getAllByRole('spinbutton', {value: {text: /few|many/}})).toHaveLength(
319+
2,
320+
)
321+
})
322+
323+
test('`value.*` must all match if specified', () => {
324+
const {getByRole} = renderIntoDocument(
325+
`<div>
326+
<button role="spinbutton" aria-valuemin="0" aria-valuenow="1" aria-valuemax="10" aria-valuetext="eins" />
327+
<button role="spinbutton" aria-valuemin="0" aria-valuenow="1" aria-valuemax="10" aria-valuetext="one" />
328+
</div>`,
329+
)
330+
expect(
331+
getByRole('spinbutton', {value: {now: 1, text: 'one'}}),
332+
).toBeInTheDocument()
333+
})
334+
240335
test('`expanded: true|false` matches `expanded` buttons', () => {
241336
const {getByRole} = renderIntoDocument(
242337
`<div>

src/queries/role.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable complexity */
12
import {
23
computeAccessibleDescription,
34
computeAccessibleName,
@@ -13,6 +14,10 @@ import {
1314
computeAriaPressed,
1415
computeAriaCurrent,
1516
computeAriaExpanded,
17+
computeAriaValueNow,
18+
computeAriaValueMax,
19+
computeAriaValueMin,
20+
computeAriaValueText,
1621
computeHeadingLevel,
1722
getImplicitAriaRoles,
1823
prettyRoles,
@@ -47,6 +52,12 @@ const queryAllByRole: AllByRole = (
4752
current,
4853
level,
4954
expanded,
55+
value: {
56+
now: valueNow,
57+
min: valueMin,
58+
max: valueMax,
59+
text: valueText,
60+
} = {} as NonNullable<ByRoleOptions['value']>,
5061
} = {},
5162
) => {
5263
checkContainerType(container)
@@ -101,6 +112,46 @@ const queryAllByRole: AllByRole = (
101112
}
102113
}
103114

115+
if (valueNow !== undefined) {
116+
// guard against unknown roles
117+
if (
118+
allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-valuenow'] ===
119+
undefined
120+
) {
121+
throw new Error(`"aria-valuenow" is not supported on role "${role}".`)
122+
}
123+
}
124+
125+
if (valueMax !== undefined) {
126+
// guard against unknown roles
127+
if (
128+
allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-valuemax'] ===
129+
undefined
130+
) {
131+
throw new Error(`"aria-valuemax" is not supported on role "${role}".`)
132+
}
133+
}
134+
135+
if (valueMin !== undefined) {
136+
// guard against unknown roles
137+
if (
138+
allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-valuemin'] ===
139+
undefined
140+
) {
141+
throw new Error(`"aria-valuemin" is not supported on role "${role}".`)
142+
}
143+
}
144+
145+
if (valueText !== undefined) {
146+
// guard against unknown roles
147+
if (
148+
allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-valuetext'] ===
149+
undefined
150+
) {
151+
throw new Error(`"aria-valuetext" is not supported on role "${role}".`)
152+
}
153+
}
154+
104155
if (expanded !== undefined) {
105156
// guard against unknown roles
106157
if (
@@ -167,6 +218,33 @@ const queryAllByRole: AllByRole = (
167218
if (level !== undefined) {
168219
return level === computeHeadingLevel(element)
169220
}
221+
if (
222+
valueNow !== undefined ||
223+
valueMax !== undefined ||
224+
valueMin !== undefined ||
225+
valueText !== undefined
226+
) {
227+
let valueMatches = true
228+
if (valueNow !== undefined) {
229+
valueMatches &&= valueNow === computeAriaValueNow(element)
230+
}
231+
if (valueMax !== undefined) {
232+
valueMatches &&= valueMax === computeAriaValueMax(element)
233+
}
234+
if (valueMin !== undefined) {
235+
valueMatches &&= valueMin === computeAriaValueMin(element)
236+
}
237+
if (valueText !== undefined) {
238+
valueMatches &&= matches(
239+
computeAriaValueText(element) ?? null,
240+
element,
241+
valueText,
242+
text => text,
243+
)
244+
}
245+
246+
return valueMatches
247+
}
170248
// don't care if aria attributes are unspecified
171249
return true
172250
})

src/role-helpers.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,42 @@ function computeHeadingLevel(element) {
325325
return ariaLevelAttribute || implicitHeadingLevels[element.tagName]
326326
}
327327

328+
/**
329+
* @param {Element} element -
330+
* @returns {number | undefined} -
331+
*/
332+
function computeAriaValueNow(element) {
333+
const valueNow = element.getAttribute('aria-valuenow')
334+
return valueNow === null ? undefined : +valueNow
335+
}
336+
337+
/**
338+
* @param {Element} element -
339+
* @returns {number | undefined} -
340+
*/
341+
function computeAriaValueMax(element) {
342+
const valueMax = element.getAttribute('aria-valuemax')
343+
return valueMax === null ? undefined : +valueMax
344+
}
345+
346+
/**
347+
* @param {Element} element -
348+
* @returns {number | undefined} -
349+
*/
350+
function computeAriaValueMin(element) {
351+
const valueMin = element.getAttribute('aria-valuemin')
352+
return valueMin === null ? undefined : +valueMin
353+
}
354+
355+
/**
356+
* @param {Element} element -
357+
* @returns {string | undefined} -
358+
*/
359+
function computeAriaValueText(element) {
360+
const valueText = element.getAttribute('aria-valuetext')
361+
return valueText === null ? undefined : valueText
362+
}
363+
328364
export {
329365
getRoles,
330366
logRoles,
@@ -337,5 +373,9 @@ export {
337373
computeAriaPressed,
338374
computeAriaCurrent,
339375
computeAriaExpanded,
376+
computeAriaValueNow,
377+
computeAriaValueMax,
378+
computeAriaValueMin,
379+
computeAriaValueText,
340380
computeHeadingLevel,
341381
}

types/queries.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ export interface ByRoleOptions {
105105
* the `aria-level` attribute.
106106
*/
107107
level?: number
108+
value?: {
109+
now?: number
110+
min?: number
111+
max?: number
112+
text?: Matcher
113+
}
108114
/**
109115
* Includes every role used in the `role` attribute
110116
* For example *ByRole('progressbar', {queryFallbacks: true})` will find <div role="meter progressbar">`.

0 commit comments

Comments
 (0)