Skip to content

Commit 0427531

Browse files
committed
feat(ByRole): Allow filter by selected state
1 parent 9007a67 commit 0427531

File tree

3 files changed

+133
-0
lines changed

3 files changed

+133
-0
lines changed

src/__tests__/ariaAttributes.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import {render} from './helpers/test-utils'
2+
3+
test('`selected` throws on unsupported roles', () => {
4+
const {getByRole} = render(`<input aria-selected="true" type="text">`)
5+
expect(() =>
6+
getByRole('textbox', {selected: true}),
7+
).toThrowErrorMatchingInlineSnapshot(
8+
`"\\"aria-selected\\" is not supported on role \\"textbox\\"."`,
9+
)
10+
})
11+
12+
test('`selected: true` matches `aria-selected="true"` on supported roles', () => {
13+
const {getAllByRole} = render(`
14+
<select>
15+
<option selected id="selected-native-option" />
16+
<option id="unselected-native-option" />
17+
</select>
18+
<div role="listbox">
19+
<div role="option" aria-selected="true" id="selected-listbox-option" />
20+
<div role="option" aria-selected="false" id="unselected-listbox-option" />
21+
<div role="option" id="not-selectable-listbox-option" />
22+
</div>
23+
<div role="tree">
24+
<div role="treeitem" aria-selected="true" id="selected-treeitem" />
25+
<div role="treeitem" aria-selected="false" id="unselected-treeitem" />
26+
<div role="treeitem" id="not-selectable-treeitem" />
27+
</div>
28+
<input type="radio" aria-selected="true" id="selected-native-radio" />
29+
<input type="radio" aria-selected="false" id="unselected-native-radio" />
30+
<div role="radio" aria-selected="true" id="selected-radio" />
31+
<div role="radio" aria-selected="false" id="unselected-radio" />
32+
<table>
33+
<thead>
34+
<tr>
35+
<th scope="col" aria-selected="true" id="selected-native-columnheader" />
36+
<div role="columnheader" aria-selected="true" id="selected-columnheader" />
37+
<th scope="col" id="unselected-native-columnheader" />
38+
</tr>
39+
</thead>
40+
<tbody>
41+
<tr>
42+
<th scope="row" aria-selected="true" id="selected-native-rowheader" />
43+
<td />
44+
<td />
45+
</tr>
46+
<tr>
47+
<div role="rowheader" aria-selected="true" id="selected-rowheader" />
48+
<td />
49+
<td />
50+
</tr>
51+
</tbody>
52+
</table>
53+
<div role="grid">
54+
<div role="gridcell" aria-selected="true" id="selected-gridcell" />
55+
<div role="gridcell" aria-selected="false" id="unselected-gridcell" />
56+
<div role="gridcell" id="not-selectable-gridcell" />
57+
</div>
58+
<div role="tablist">
59+
<div role="tab" aria-selected="true" id="selected-tab" />
60+
<div role="tab" aria-selected="false" id= "unselected-tab" />
61+
<div role="tab" id="unselectable-tab" />
62+
</div>
63+
`)
64+
65+
expect(
66+
getAllByRole('columnheader', {selected: true}).map(({id}) => id),
67+
).toEqual(['selected-native-columnheader', 'selected-columnheader'])
68+
69+
expect(getAllByRole('gridcell', {selected: true}).map(({id}) => id)).toEqual([
70+
'selected-gridcell',
71+
])
72+
73+
expect(getAllByRole('option', {selected: true}).map(({id}) => id)).toEqual([
74+
'selected-native-option',
75+
'selected-listbox-option',
76+
])
77+
78+
expect(getAllByRole('radio', {selected: true}).map(({id}) => id)).toEqual([
79+
'selected-native-radio',
80+
'selected-radio',
81+
])
82+
83+
expect(
84+
getAllByRole('rowheader', {selected: true}).map(({id}) => id),
85+
).toEqual(['selected-rowheader', 'selected-native-rowheader'])
86+
87+
expect(getAllByRole('treeitem', {selected: true}).map(({id}) => id)).toEqual([
88+
'selected-treeitem',
89+
])
90+
91+
expect(getAllByRole('tab', {selected: true}).map(({id}) => id)).toEqual([
92+
'selected-tab',
93+
])
94+
})

src/queries/role.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {computeAccessibleName} from 'dom-accessibility-api'
2+
import {roles as allRoles} from 'aria-query'
23
import {
4+
computeAriaSelected,
35
getImplicitAriaRoles,
46
prettyRoles,
57
isInaccessible,
@@ -24,11 +26,19 @@ function queryAllByRole(
2426
trim,
2527
normalizer,
2628
queryFallbacks = false,
29+
selected,
2730
} = {},
2831
) {
2932
const matcher = exact ? matches : fuzzyMatches
3033
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})
3134

35+
if (selected !== undefined) {
36+
// guard against unknown roles
37+
if (allRoles.get(role)?.props['aria-selected'] === undefined) {
38+
throw new Error(`"aria-selected" is not supported on role "${role}".`)
39+
}
40+
}
41+
3242
const subtreeIsInaccessibleCache = new WeakMap()
3343
function cachedIsSubtreeInaccessible(element) {
3444
if (!subtreeIsInaccessibleCache.has(element)) {
@@ -65,6 +75,13 @@ function queryAllByRole(
6575
matcher(implicitRole, node, role, matchNormalizer),
6676
)
6777
})
78+
.filter(element => {
79+
if (selected !== undefined) {
80+
return selected === computeAriaSelected(element)
81+
}
82+
// don't care if aria attributes are unspecified
83+
return true
84+
})
6885
.filter(element => {
6986
return hidden === false
7087
? isInaccessible(element, {

src/role-helpers.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,33 @@ function prettyRoles(dom, {hidden}) {
168168
const logRoles = (dom, {hidden = false} = {}) =>
169169
console.log(prettyRoles(dom, {hidden}))
170170

171+
/**
172+
* @param {Element} element -
173+
* @returns {boolean | undefined} - false/true if (not)selected, undefined if not selectable
174+
*/
175+
function computeAriaSelected(element) {
176+
// implicit value from html-aam mappings: https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
177+
// https://www.w3.org/TR/html-aam-1.0/#details-id-97
178+
if (element.tagName === 'OPTION') {
179+
return element.selected
180+
}
181+
// explicit value
182+
const attributeValue = element.getAttribute('aria-selected')
183+
if (attributeValue === 'true') {
184+
return true
185+
}
186+
if (attributeValue === 'false') {
187+
return false
188+
}
189+
return undefined
190+
}
191+
171192
export {
172193
getRoles,
173194
logRoles,
174195
getImplicitAriaRoles,
175196
isSubtreeInaccessible,
176197
prettyRoles,
177198
isInaccessible,
199+
computeAriaSelected,
178200
}

0 commit comments

Comments
 (0)