Skip to content

Commit fcdeb31

Browse files
idaneneps1lon
andauthored
feat(role): support {checked: true} for checkbox / radio (#692)
Co-authored-by: Sebastian Silbermann <[email protected]>
1 parent 2215770 commit fcdeb31

File tree

4 files changed

+97
-2
lines changed

4 files changed

+97
-2
lines changed

src/__tests__/ariaAttributes.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {render} from './helpers/test-utils'
1+
import {render, renderIntoDocument} from './helpers/test-utils'
22

33
test('`selected` throws on unsupported roles', () => {
44
const {getByRole} = render(`<input aria-selected="true" type="text">`)
@@ -9,6 +9,59 @@ test('`selected` throws on unsupported roles', () => {
99
)
1010
})
1111

12+
test('`checked` throws on unsupported roles', () => {
13+
const {getByRole} = render(`<input aria-checked="true" type="text">`)
14+
expect(() =>
15+
getByRole('textbox', {checked: true}),
16+
).toThrowErrorMatchingInlineSnapshot(
17+
`"\\"aria-checked\\" is not supported on role \\"textbox\\"."`,
18+
)
19+
})
20+
21+
test('`checked: true|false` matches `checked` checkboxes', () => {
22+
const {getByRole} = renderIntoDocument(
23+
`<div>
24+
<input type="checkbox" checked />
25+
<input type="checkbox" />
26+
</div>`,
27+
)
28+
expect(getByRole('checkbox', {checked: true})).toBeInTheDocument()
29+
expect(getByRole('checkbox', {checked: false})).toBeInTheDocument()
30+
})
31+
32+
test('`checked: true|false` matches `checked` elements with proper role', () => {
33+
const {getByRole} = renderIntoDocument(
34+
`<div>
35+
<span role="checkbox" aria-checked="true">✔</span>
36+
<span role="checkbox" aria-checked="false">𝒙</span>
37+
</div>`,
38+
)
39+
expect(getByRole('checkbox', {checked: true})).toBeInTheDocument()
40+
expect(getByRole('checkbox', {checked: false})).toBeInTheDocument()
41+
})
42+
43+
test('`checked: true|false` does not match element in `indeterminate` state', () => {
44+
const {queryByRole, getByLabelText} = renderIntoDocument(
45+
`<div>
46+
<span role="checkbox" aria-checked="mixed">not so much</span>
47+
<input type="checkbox" checked aria-label="indeteminate yes" />
48+
<input type="checkbox" aria-label="indeteminate no" />
49+
</div>`,
50+
)
51+
getByLabelText(/indeteminate yes/i).indeterminate = true
52+
getByLabelText(/indeteminate no/i).indeterminate = true
53+
54+
expect(
55+
queryByRole('checkbox', {checked: true, name: /indeteminate yes/i}),
56+
).toBeNull()
57+
expect(
58+
queryByRole('checkbox', {checked: false, name: /indeteminate no/i}),
59+
).toBeNull()
60+
expect(
61+
queryByRole('checkbox', {checked: true, name: /not so much/i}),
62+
).toBeNull()
63+
})
64+
1265
test('`selected: true` matches `aria-selected="true"` on supported roles', () => {
1366
const {getAllByRole} = render(`
1467
<select>

src/queries/role.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {computeAccessibleName} from 'dom-accessibility-api'
22
import {roles as allRoles} from 'aria-query'
33
import {
44
computeAriaSelected,
5+
computeAriaChecked,
56
getImplicitAriaRoles,
67
prettyRoles,
78
isInaccessible,
@@ -29,6 +30,7 @@ function queryAllByRole(
2930
normalizer,
3031
queryFallbacks = false,
3132
selected,
33+
checked,
3234
} = {},
3335
) {
3436
checkContainerType(container)
@@ -42,6 +44,13 @@ function queryAllByRole(
4244
}
4345
}
4446

47+
if (checked !== undefined) {
48+
// guard against unknown roles
49+
if (allRoles.get(role)?.props['aria-checked'] === undefined) {
50+
throw new Error(`"aria-checked" is not supported on role "${role}".`)
51+
}
52+
}
53+
4554
const subtreeIsInaccessibleCache = new WeakMap()
4655
function cachedIsSubtreeInaccessible(element) {
4756
if (!subtreeIsInaccessibleCache.has(element)) {
@@ -82,6 +91,9 @@ function queryAllByRole(
8291
if (selected !== undefined) {
8392
return selected === computeAriaSelected(element)
8493
}
94+
if (checked !== undefined) {
95+
return checked === computeAriaChecked(element)
96+
}
8597
// don't care if aria attributes are unspecified
8698
return true
8799
})

src/role-helpers.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,32 @@ function computeAriaSelected(element) {
185185
if (element.tagName === 'OPTION') {
186186
return element.selected
187187
}
188+
188189
// explicit value
189-
const attributeValue = element.getAttribute('aria-selected')
190+
return checkBooleanAttribute(element, 'aria-selected')
191+
}
192+
193+
/**
194+
* @param {Element} element -
195+
* @returns {boolean | undefined} - false/true if (not)checked, undefined if not checked-able
196+
*/
197+
function computeAriaChecked(element) {
198+
// implicit value from html-aam mappings: https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
199+
// https://www.w3.org/TR/html-aam-1.0/#details-id-56
200+
// https://www.w3.org/TR/html-aam-1.0/#details-id-67
201+
if ('indeterminate' in element && element.indeterminate) {
202+
return undefined
203+
}
204+
if ('checked' in element) {
205+
return element.checked
206+
}
207+
208+
// explicit value
209+
return checkBooleanAttribute(element, 'aria-checked')
210+
}
211+
212+
function checkBooleanAttribute(element, attribute) {
213+
const attributeValue = element.getAttribute(attribute)
190214
if (attributeValue === 'true') {
191215
return true
192216
}
@@ -204,4 +228,5 @@ export {
204228
prettyRoles,
205229
isInaccessible,
206230
computeAriaSelected,
231+
computeAriaChecked,
207232
}

types/queries.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ export interface ByRoleOptions extends MatcherOptions {
7979
* selected in the accessibility tree, i.e., `aria-selected="true"`
8080
*/
8181
selected?: boolean
82+
/**
83+
* If true only includes elements in the query set that are marked as
84+
* checked in the accessibility tree, i.e., `aria-checked="true"`
85+
*/
86+
checked?: boolean
8287
/**
8388
* Includes every role used in the `role` attribute
8489
* For example *ByRole('progressbar', {queryFallbacks: true})` will find <div role="meter progressbar">`.

0 commit comments

Comments
 (0)