diff --git a/src/__tests__/ariaAttributes.js b/src/__tests__/ariaAttributes.js
index 6ee4bb2c..84334230 100644
--- a/src/__tests__/ariaAttributes.js
+++ b/src/__tests__/ariaAttributes.js
@@ -27,6 +27,15 @@ test('`checked` throws on unsupported roles', () => {
)
})
+test('`expanded` throws on unsupported roles', () => {
+ const {getByRole} = render(`
@@ -202,3 +211,25 @@ test('`level` throws on unsupported roles', () => {
`"Role \\"button\\" cannot have \\"level\\" property."`,
)
})
+
+test('`expanded: true|false` matches `expanded` buttons', () => {
+ const {getByRole} = renderIntoDocument(
+ `
+
+
+
`,
+ )
+ expect(getByRole('button', {expanded: true})).toBeInTheDocument()
+ expect(getByRole('button', {expanded: false})).toBeInTheDocument()
+})
+
+test('`expanded: true|false` matches `expanded` elements with proper role', () => {
+ const {getByRole} = renderIntoDocument(
+ `
+ ✔
+ 𝒙
+
`,
+ )
+ expect(getByRole('button', {expanded: true})).toBeInTheDocument()
+ expect(getByRole('button', {expanded: false})).toBeInTheDocument()
+})
diff --git a/src/queries/role.js b/src/queries/role.js
index 3102de9f..747e2ca0 100644
--- a/src/queries/role.js
+++ b/src/queries/role.js
@@ -4,6 +4,7 @@ import {
computeAriaSelected,
computeAriaChecked,
computeAriaPressed,
+ computeAriaExpanded,
computeHeadingLevel,
getImplicitAriaRoles,
prettyRoles,
@@ -35,6 +36,7 @@ function queryAllByRole(
checked,
pressed,
level,
+ expanded,
} = {},
) {
checkContainerType(container)
@@ -69,6 +71,13 @@ function queryAllByRole(
}
}
+ if (expanded !== undefined) {
+ // guard against unknown roles
+ if (allRoles.get(role)?.props['aria-expanded'] === undefined) {
+ throw new Error(`"aria-expanded" is not supported on role "${role}".`)
+ }
+ }
+
const subtreeIsInaccessibleCache = new WeakMap()
function cachedIsSubtreeInaccessible(element) {
if (!subtreeIsInaccessibleCache.has(element)) {
@@ -115,6 +124,9 @@ function queryAllByRole(
if (pressed !== undefined) {
return pressed === computeAriaPressed(element)
}
+ if (expanded !== undefined) {
+ return expanded === computeAriaExpanded(element)
+ }
if (level !== undefined) {
return level === computeHeadingLevel(element)
}
diff --git a/src/role-helpers.js b/src/role-helpers.js
index c7d64076..25fa88e6 100644
--- a/src/role-helpers.js
+++ b/src/role-helpers.js
@@ -247,6 +247,15 @@ function computeAriaPressed(element) {
return checkBooleanAttribute(element, 'aria-pressed')
}
+/**
+ * @param {Element} element -
+ * @returns {boolean | undefined} - false/true if (not)expanded, undefined if not expand-able
+ */
+function computeAriaExpanded(element) {
+ // https://www.w3.org/TR/wai-aria-1.1/#aria-expanded
+ return checkBooleanAttribute(element, 'aria-expanded')
+}
+
function checkBooleanAttribute(element, attribute) {
const attributeValue = element.getAttribute(attribute)
if (attributeValue === 'true') {
@@ -292,5 +301,6 @@ export {
computeAriaSelected,
computeAriaChecked,
computeAriaPressed,
+ computeAriaExpanded,
computeHeadingLevel,
}
diff --git a/types/queries.d.ts b/types/queries.d.ts
index 8418d99c..d81fdcef 100644
--- a/types/queries.d.ts
+++ b/types/queries.d.ts
@@ -88,6 +88,11 @@ export interface ByRoleOptions extends MatcherOptions {
* pressed in the accessibility tree, i.e., `aria-pressed="true"`
*/
pressed?: boolean
+ /**
+ * If true only includes elements in the query set that are marked as
+ * expanded in the accessibility tree, i.e., `aria-expanded="true"`
+ */
+ expanded?: boolean
/**
* Includes elements with the `"heading"` role matching the indicated level,
* either by the semantic HTML heading elements `
-` or matching