Skip to content

Commit eeebfc2

Browse files
committed
feat(ByRole): Allow filter by disabled state
1 parent d1ff495 commit eeebfc2

File tree

4 files changed

+98
-0
lines changed

4 files changed

+98
-0
lines changed

src/__tests__/ariaAttributes.js

+54
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,57 @@ test('`expanded: true|false` matches `expanded` elements with proper role', () =
258258
expect(getByRole('button', {expanded: true})).toBeInTheDocument()
259259
expect(getByRole('button', {expanded: false})).toBeInTheDocument()
260260
})
261+
262+
test('`disabled` throws on unsupported roles', () => {
263+
const {getByRole} = render(
264+
`<div role="alert" aria-disabled="true">Hello, Dave!</div>`,
265+
)
266+
expect(() =>
267+
getByRole('alert', {disabled: true}),
268+
).toThrowErrorMatchingInlineSnapshot(
269+
`"aria-disabled" is not supported on role "alert".`,
270+
)
271+
})
272+
273+
test('`disabled: true|false` matches `disabled` buttons', () => {
274+
const {getByRole} = renderIntoDocument(
275+
`<div>
276+
<button disabled="true" />
277+
<button />
278+
</div>`,
279+
)
280+
expect(getByRole('button', {disabled: true})).toBeInTheDocument()
281+
expect(getByRole('button', {disabled: false})).toBeInTheDocument()
282+
})
283+
284+
test('`disabled: true|false` matches `aria-disabled` buttons', () => {
285+
const {getByRole} = renderIntoDocument(
286+
`<div>
287+
<button aria-disabled="true" />
288+
<button aria-disabled="false" />
289+
</div>`,
290+
)
291+
expect(getByRole('button', {disabled: true})).toBeInTheDocument()
292+
expect(getByRole('button', {disabled: false})).toBeInTheDocument()
293+
})
294+
295+
test('`disabled` attributes overrides `aria-dsiabled`', () => {
296+
const {getByRole} = renderIntoDocument(
297+
`<div>
298+
<button disabled="true" aria-disabled="false" />
299+
<button />
300+
</div>`,
301+
)
302+
expect(getByRole('button', {disabled: true})).toBeInTheDocument()
303+
})
304+
305+
test('consider `disabled` attribute only if supported', () => {
306+
const {getByRole, queryByRole} = renderIntoDocument(
307+
`<div>
308+
<button disabled="true" />
309+
<div role="slider" disabled="true" />
310+
</div>`,
311+
)
312+
expect(getByRole('button', {disabled: true})).toBeInTheDocument()
313+
expect(queryByRole('slider', {disabled: true})).toBe(null)
314+
})

src/queries/role.ts

+15
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
computeAriaChecked,
1313
computeAriaPressed,
1414
computeAriaCurrent,
15+
computeAriaDisabled,
1516
computeAriaExpanded,
1617
computeHeadingLevel,
1718
getImplicitAriaRoles,
@@ -45,6 +46,7 @@ const queryAllByRole: AllByRole = (
4546
checked,
4647
pressed,
4748
current,
49+
disabled,
4850
level,
4951
expanded,
5052
} = {},
@@ -111,6 +113,16 @@ const queryAllByRole: AllByRole = (
111113
}
112114
}
113115

116+
if (disabled !== undefined) {
117+
// guard against unknown roles
118+
if (
119+
allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-disabled'] ===
120+
undefined
121+
) {
122+
throw new Error(`"aria-disabled" is not supported on role "${role}".`)
123+
}
124+
}
125+
114126
const subtreeIsInaccessibleCache = new WeakMap<Element, Boolean>()
115127
function cachedIsSubtreeInaccessible(element: Element) {
116128
if (!subtreeIsInaccessibleCache.has(element)) {
@@ -161,6 +173,9 @@ const queryAllByRole: AllByRole = (
161173
if (current !== undefined) {
162174
return current === computeAriaCurrent(element)
163175
}
176+
if (disabled !== undefined) {
177+
return disabled === computeAriaDisabled(element)
178+
}
164179
if (expanded !== undefined) {
165180
return expanded === computeAriaExpanded(element)
166181
}

src/role-helpers.js

+24
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,29 @@ function computeAriaCurrent(element) {
281281
)
282282
}
283283

284+
const elementsSupportingDisabledAttribute = new Set([
285+
'button',
286+
'fieldset',
287+
'input',
288+
'optgroup',
289+
'option',
290+
'select',
291+
'textarea',
292+
])
293+
294+
/**
295+
* @param {Element} element -
296+
* @returns {boolean} -
297+
*/
298+
function computeAriaDisabled(element) {
299+
return elementsSupportingDisabledAttribute.has(element.localName) &&
300+
element.hasAttribute('disabled')
301+
? // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
302+
true
303+
: // https://www.w3.org/TR/wai-aria-1.1/#aria-disabled
304+
element.getAttribute('aria-disabled') === 'true'
305+
}
306+
284307
/**
285308
* @param {Element} element -
286309
* @returns {boolean | undefined} - false/true if (not)expanded, undefined if not expand-able
@@ -336,6 +359,7 @@ export {
336359
computeAriaChecked,
337360
computeAriaPressed,
338361
computeAriaCurrent,
362+
computeAriaDisabled,
339363
computeAriaExpanded,
340364
computeHeadingLevel,
341365
}

types/queries.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ export interface ByRoleOptions {
9494
* Filters elements by their `aria-current` state. `true` and `false` match `aria-current="true"` and `aria-current="false"` (as well as a missing `aria-current` attribute) respectively.
9595
*/
9696
current?: boolean | string
97+
/**
98+
* If true only includes elements in the query set that are marked as
99+
* disabled in the accessibility tree, i.e., `aria-disabled="true"` or `disabled="true"`.
100+
*/
101+
disabled?: boolean
97102
/**
98103
* If true only includes elements in the query set that are marked as
99104
* expanded in the accessibility tree, i.e., `aria-expanded="true"`

0 commit comments

Comments
 (0)