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(
+ `
Hello, Dave!
`,
+ )
+ expect(() =>
+ getByRole('alert', {disabled: true}),
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"aria-disabled" is not supported on role "alert".`,
+ )
+})
+
+test('`disabled: true|false` matches `disabled` buttons', () => {
+ const {getByRole} = renderIntoDocument(
+ `
+
+
+
`,
+ )
+ expect(getByRole('button', {disabled: true})).toBeInTheDocument()
+ expect(getByRole('button', {disabled: false})).toBeInTheDocument()
+})
+
+test('`disabled: true|false` matches `aria-disabled` buttons', () => {
+ const {getByRole} = renderIntoDocument(
+ `
+
+
+
`,
+ )
+ expect(getByRole('button', {disabled: true})).toBeInTheDocument()
+ expect(getByRole('button', {disabled: false})).toBeInTheDocument()
+})
+
+test('`disabled` attributes overrides `aria-disabled`', () => {
+ const {getByRole} = renderIntoDocument(
+ `
+
+
+
`,
+ )
+ expect(getByRole('button', {disabled: true})).toBeInTheDocument()
+})
+
+test('consider `disabled` attribute only if supported', () => {
+ const {getByRole, queryByRole} = renderIntoDocument(
+ ``,
+ )
+ 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()
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"`