diff --git a/src/__tests__/ariaAttributes.js b/src/__tests__/ariaAttributes.js
index 1ceb210d..2f19840c 100644
--- a/src/__tests__/ariaAttributes.js
+++ b/src/__tests__/ariaAttributes.js
@@ -258,3 +258,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-dsiabled`', () => {
+ 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 f8e976a4..df67dcaf 100644
--- a/src/queries/role.ts
+++ b/src/queries/role.ts
@@ -12,6 +12,7 @@ import {
computeAriaChecked,
computeAriaPressed,
computeAriaCurrent,
+ computeAriaDisabled,
computeAriaExpanded,
computeHeadingLevel,
getImplicitAriaRoles,
@@ -45,6 +46,7 @@ const queryAllByRole: AllByRole = (
checked,
pressed,
current,
+ disabled,
level,
expanded,
} = {},
@@ -111,6 +113,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)) {
@@ -161,6 +173,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 500bcdd2..47655a4a 100644
--- a/src/role-helpers.js
+++ b/src/role-helpers.js
@@ -281,6 +281,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
@@ -336,6 +359,7 @@ export {
computeAriaChecked,
computeAriaPressed,
computeAriaCurrent,
+ computeAriaDisabled,
computeAriaExpanded,
computeHeadingLevel,
}
diff --git a/types/queries.d.ts b/types/queries.d.ts
index cea02365..f6291dd8 100644
--- a/types/queries.d.ts
+++ b/types/queries.d.ts
@@ -94,6 +94,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"`