diff --git a/src/__tests__/ariaAttributes.js b/src/__tests__/ariaAttributes.js index 7984d5e0..74cbb499 100644 --- a/src/__tests__/ariaAttributes.js +++ b/src/__tests__/ariaAttributes.js @@ -379,3 +379,57 @@ test('`expanded: true|false` matches `expanded` elements with proper role', () = expect(getByRole('button', {expanded: true})).toBeInTheDocument() expect(getByRole('button', {expanded: false})).toBeInTheDocument() }) + +test('`disabled` throws on unsupported roles', () => { + const {getByRole} = render( + `<div role="alert" aria-disabled="true">Hello, Dave!</div>`, + ) + expect(() => + getByRole('alert', {disabled: true}), + ).toThrowErrorMatchingInlineSnapshot( + `"aria-disabled" is not supported on role "alert".`, + ) +}) + +test('`disabled: true|false` matches `disabled` buttons', () => { + const {getByRole} = renderIntoDocument( + `<div> + <button disabled="true" /> + <button /> + </div>`, + ) + expect(getByRole('button', {disabled: true})).toBeInTheDocument() + expect(getByRole('button', {disabled: false})).toBeInTheDocument() +}) + +test('`disabled: true|false` matches `aria-disabled` buttons', () => { + const {getByRole} = renderIntoDocument( + `<div> + <button aria-disabled="true" /> + <button aria-disabled="false" /> + </div>`, + ) + expect(getByRole('button', {disabled: true})).toBeInTheDocument() + expect(getByRole('button', {disabled: false})).toBeInTheDocument() +}) + +test('`disabled` attributes overrides `aria-disabled`', () => { + const {getByRole} = renderIntoDocument( + `<div> + <button disabled="true" aria-disabled="false" /> + <button /> + </div>`, + ) + expect(getByRole('button', {disabled: true})).toBeInTheDocument() +}) + +test('consider `disabled` attribute only if supported', () => { + const {getByRole, queryByRole} = renderIntoDocument( + `<div> + <button disabled="true" /> + <div role="slider" disabled="true" /> + </div>`, + ) + expect(getByRole('button', {disabled: true})).toBeInTheDocument() + expect(queryByRole('slider', {disabled: true})).toBe(null) +}) diff --git a/src/queries/role.ts b/src/queries/role.ts index 98b08848..902e5431 100644 --- a/src/queries/role.ts +++ b/src/queries/role.ts @@ -14,6 +14,7 @@ import { computeAriaChecked, computeAriaPressed, computeAriaCurrent, + computeAriaDisabled, computeAriaExpanded, computeAriaValueNow, computeAriaValueMax, @@ -52,6 +53,7 @@ const queryAllByRole: AllByRole = ( checked, pressed, current, + disabled, level, expanded, value: { @@ -174,6 +176,16 @@ const queryAllByRole: AllByRole = ( } } + if (disabled !== undefined) { + // guard against unknown roles + if ( + allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-disabled'] === + undefined + ) { + throw new Error(`"aria-disabled" is not supported on role "${role}".`) + } + } + const subtreeIsInaccessibleCache = new WeakMap<Element, Boolean>() function cachedIsSubtreeInaccessible(element: Element) { if (!subtreeIsInaccessibleCache.has(element)) { @@ -227,6 +239,9 @@ const queryAllByRole: AllByRole = ( if (current !== undefined) { return current === computeAriaCurrent(element) } + if (disabled !== undefined) { + return disabled === computeAriaDisabled(element) + } if (expanded !== undefined) { return expanded === computeAriaExpanded(element) } diff --git a/src/role-helpers.js b/src/role-helpers.js index bc134f27..3a067606 100644 --- a/src/role-helpers.js +++ b/src/role-helpers.js @@ -290,6 +290,29 @@ function computeAriaCurrent(element) { ) } +const elementsSupportingDisabledAttribute = new Set([ + 'button', + 'fieldset', + 'input', + 'optgroup', + 'option', + 'select', + 'textarea', +]) + +/** + * @param {Element} element - + * @returns {boolean} - + */ +function computeAriaDisabled(element) { + return elementsSupportingDisabledAttribute.has(element.localName) && + element.hasAttribute('disabled') + ? // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings + true + : // https://www.w3.org/TR/wai-aria-1.1/#aria-disabled + element.getAttribute('aria-disabled') === 'true' +} + /** * @param {Element} element - * @returns {boolean | undefined} - false/true if (not)expanded, undefined if not expand-able @@ -382,6 +405,7 @@ export { computeAriaChecked, computeAriaPressed, computeAriaCurrent, + computeAriaDisabled, computeAriaExpanded, computeAriaValueNow, computeAriaValueMax, diff --git a/types/queries.d.ts b/types/queries.d.ts index c6ce9054..3848f99d 100644 --- a/types/queries.d.ts +++ b/types/queries.d.ts @@ -99,6 +99,11 @@ export interface ByRoleOptions { * 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. */ current?: boolean | string + /** + * If true only includes elements in the query set that are marked as + * disabled in the accessibility tree, i.e., `aria-disabled="true"` or `disabled="true"`. + */ + disabled?: boolean /** * If true only includes elements in the query set that are marked as * expanded in the accessibility tree, i.e., `aria-expanded="true"`