From 8ef3e4b94724f62eec0113eab02fbc6299445c23 Mon Sep 17 00:00:00 2001 From: Idan Entin Date: Sun, 12 Jul 2020 12:34:51 +0300 Subject: [PATCH 1/8] "feat: support `{selected: true}` for checkbox / radio Closes #691" --- src/__tests__/ariaAttributes.js | 7 ++++++- src/queries/role.js | 5 ++++- src/role-helpers.js | 3 +++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/__tests__/ariaAttributes.js b/src/__tests__/ariaAttributes.js index 56590aac..3eb4af56 100644 --- a/src/__tests__/ariaAttributes.js +++ b/src/__tests__/ariaAttributes.js @@ -1,4 +1,4 @@ -import {render} from './helpers/test-utils' +import {render, renderIntoDocument} from './helpers/test-utils' test('`selected` throws on unsupported roles', () => { const {getByRole} = render(``) @@ -9,6 +9,11 @@ test('`selected` throws on unsupported roles', () => { ) }) +test('`selected: true` matches `checked` checkboxes', () => { + const {getByRole} = renderIntoDocument(``) + expect(getByRole('checkbox', {selected: true})).toBeInTheDocument() +}) + test('`selected: true` matches `aria-selected="true"` on supported roles', () => { const {getAllByRole} = render(` `) + const {getByRole} = renderIntoDocument( + `
+ + +
`, + ) + expect(getByRole('checkbox', {selected: true})).toBeInTheDocument() + expect(getByRole('checkbox', {selected: false})).toBeInTheDocument() +}) + +test('`selected: true` matches `checked` elements with proper role', () => { + const {getByRole} = renderIntoDocument( + `
+ + 𝒙 +
`, + ) expect(getByRole('checkbox', {selected: true})).toBeInTheDocument() + expect(getByRole('checkbox', {selected: false})).toBeInTheDocument() }) test('`selected: true` matches `aria-selected="true"` on supported roles', () => { diff --git a/src/role-helpers.js b/src/role-helpers.js index b507375b..645872b2 100644 --- a/src/role-helpers.js +++ b/src/role-helpers.js @@ -188,8 +188,11 @@ function computeAriaSelected(element) { if ('checked' in element) { return element.checked } + // explicit value - const attributeValue = element.getAttribute('aria-selected') + const attributeValue = + element.getAttribute('aria-selected') || + element.getAttribute('aria-checked') if (attributeValue === 'true') { return true } From ff763af85c254f18aa0b772f14e2557a3918fb21 Mon Sep 17 00:00:00 2001 From: Idan Entin Date: Mon, 13 Jul 2020 17:02:12 +0300 Subject: [PATCH 4/8] feat: support filtering ByRole with `{checked: true}` Closes #691 --- src/__tests__/ariaAttributes.js | 12 ++++++------ src/queries/role.js | 17 +++++++++++++---- src/role-helpers.js | 24 ++++++++++++++++++++++++ types/queries.d.ts | 7 ++++++- 4 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/__tests__/ariaAttributes.js b/src/__tests__/ariaAttributes.js index eaff6177..4b1d257c 100644 --- a/src/__tests__/ariaAttributes.js +++ b/src/__tests__/ariaAttributes.js @@ -9,26 +9,26 @@ test('`selected` throws on unsupported roles', () => { ) }) -test('`selected: true` matches `checked` checkboxes', () => { +test('`checked: true|false` matches `checked` checkboxes', () => { const {getByRole} = renderIntoDocument( `
`, ) - expect(getByRole('checkbox', {selected: true})).toBeInTheDocument() - expect(getByRole('checkbox', {selected: false})).toBeInTheDocument() + expect(getByRole('checkbox', {checked: true})).toBeInTheDocument() + expect(getByRole('checkbox', {checked: false})).toBeInTheDocument() }) -test('`selected: true` matches `checked` elements with proper role', () => { +test('`checked: true|false` matches `checked` elements with proper role', () => { const {getByRole} = renderIntoDocument( `
𝒙
`, ) - expect(getByRole('checkbox', {selected: true})).toBeInTheDocument() - expect(getByRole('checkbox', {selected: false})).toBeInTheDocument() + expect(getByRole('checkbox', {checked: true})).toBeInTheDocument() + expect(getByRole('checkbox', {checked: false})).toBeInTheDocument() }) test('`selected: true` matches `aria-selected="true"` on supported roles', () => { diff --git a/src/queries/role.js b/src/queries/role.js index cda47a51..f9be4564 100644 --- a/src/queries/role.js +++ b/src/queries/role.js @@ -2,6 +2,7 @@ import {computeAccessibleName} from 'dom-accessibility-api' import {roles as allRoles} from 'aria-query' import { computeAriaSelected, + computeAriaChecked, getImplicitAriaRoles, prettyRoles, isInaccessible, @@ -29,6 +30,7 @@ function queryAllByRole( normalizer, queryFallbacks = false, selected, + checked, } = {}, ) { checkContainerType(container) @@ -37,14 +39,18 @@ function queryAllByRole( if (selected !== undefined) { // guard against unknown roles - if ( - allRoles.get(role)?.props['aria-selected'] === undefined && - allRoles.get(role)?.props['aria-checked'] === undefined - ) { + if (allRoles.get(role)?.props['aria-selected'] === undefined) { throw new Error(`"aria-selected" is not supported on role "${role}".`) } } + if (checked !== undefined) { + // guard against unknown roles + if (allRoles.get(role)?.props['aria-checked'] === undefined) { + throw new Error(`"aria-checked" is not supported on role "${role}".`) + } + } + const subtreeIsInaccessibleCache = new WeakMap() function cachedIsSubtreeInaccessible(element) { if (!subtreeIsInaccessibleCache.has(element)) { @@ -85,6 +91,9 @@ function queryAllByRole( if (selected !== undefined) { return selected === computeAriaSelected(element) } + if (checked !== undefined) { + return checked === computeAriaChecked(element) + } // don't care if aria attributes are unspecified return true }) diff --git a/src/role-helpers.js b/src/role-helpers.js index 645872b2..a8f2f05c 100644 --- a/src/role-helpers.js +++ b/src/role-helpers.js @@ -202,6 +202,29 @@ function computeAriaSelected(element) { return undefined } +/** + * @param {Element} element - + * @returns {boolean | undefined} - false/true if (not)checked, undefined if not checked-able + */ +function computeAriaChecked(element) { + // implicit value from html-aam mappings: https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings + // https://www.w3.org/TR/html-aam-1.0/#details-id-56 + // https://www.w3.org/TR/html-aam-1.0/#details-id-67 + if ('checked' in element) { + return element.checked + } + + // explicit value + const attributeValue = element.getAttribute('aria-checked') + if (attributeValue === 'true') { + return true + } + if (attributeValue === 'false') { + return false + } + return undefined +} + export { getRoles, logRoles, @@ -210,4 +233,5 @@ export { prettyRoles, isInaccessible, computeAriaSelected, + computeAriaChecked, } diff --git a/types/queries.d.ts b/types/queries.d.ts index f583b260..85bfc41d 100644 --- a/types/queries.d.ts +++ b/types/queries.d.ts @@ -76,9 +76,14 @@ export interface ByRoleOptions extends MatcherOptions { hidden?: boolean /** * If true only includes elements in the query set that are marked as - * selected or checked in the accessibility tree, i.e., `aria-selected="true"` + * selected in the accessibility tree, i.e., `aria-selected="true"` */ selected?: boolean + /** + * If true only includes elements in the query set that are marked as + * checked in the accessibility tree, i.e., `aria-checked="true"` + */ + checked?: boolean /** * Includes every role used in the `role` attribute * For example *ByRole('progressbar', {queryFallbacks: true})` will find
`. From cdfebc424938fd52094ffc50ef208f47db70ddae Mon Sep 17 00:00:00 2001 From: Idan Entin Date: Mon, 13 Jul 2020 20:22:09 +0300 Subject: [PATCH 5/8] fix: remove unused --- src/role-helpers.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/role-helpers.js b/src/role-helpers.js index a8f2f05c..02eeceee 100644 --- a/src/role-helpers.js +++ b/src/role-helpers.js @@ -185,14 +185,9 @@ function computeAriaSelected(element) { if (element.tagName === 'OPTION') { return element.selected } - if ('checked' in element) { - return element.checked - } // explicit value - const attributeValue = - element.getAttribute('aria-selected') || - element.getAttribute('aria-checked') + const attributeValue = element.getAttribute('aria-selected') if (attributeValue === 'true') { return true } From 9b01b082b252035a59ae3ffa2be199ff49988ec3 Mon Sep 17 00:00:00 2001 From: Idan Entin Date: Mon, 13 Jul 2020 22:57:00 +0300 Subject: [PATCH 6/8] test: add coverage --- src/__tests__/ariaAttributes.js | 9 +++++++++ src/role-helpers.js | 15 ++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/__tests__/ariaAttributes.js b/src/__tests__/ariaAttributes.js index 4b1d257c..2bdb0883 100644 --- a/src/__tests__/ariaAttributes.js +++ b/src/__tests__/ariaAttributes.js @@ -9,6 +9,15 @@ test('`selected` throws on unsupported roles', () => { ) }) +test('`checked` throws on unsupported roles', () => { + const {getByRole} = render(``) + expect(() => + getByRole('textbox', {checked: true}), + ).toThrowErrorMatchingInlineSnapshot( + `"\\"aria-checked\\" is not supported on role \\"textbox\\"."`, + ) +}) + test('`checked: true|false` matches `checked` checkboxes', () => { const {getByRole} = renderIntoDocument( `
diff --git a/src/role-helpers.js b/src/role-helpers.js index 02eeceee..ac62a809 100644 --- a/src/role-helpers.js +++ b/src/role-helpers.js @@ -187,14 +187,7 @@ function computeAriaSelected(element) { } // explicit value - const attributeValue = element.getAttribute('aria-selected') - if (attributeValue === 'true') { - return true - } - if (attributeValue === 'false') { - return false - } - return undefined + return checkBooleanAttribute(element, 'aria-selected') } /** @@ -210,7 +203,11 @@ function computeAriaChecked(element) { } // explicit value - const attributeValue = element.getAttribute('aria-checked') + return checkBooleanAttribute(element, 'aria-checked') +} + +function checkBooleanAttribute(element, attribute) { + const attributeValue = element.getAttribute(attribute) if (attributeValue === 'true') { return true } From ace09a373df18eb43a13911340e5a92638abfa1f Mon Sep 17 00:00:00 2001 From: Idan Entin Date: Tue, 14 Jul 2020 14:01:43 +0300 Subject: [PATCH 7/8] fix(ByRole): exclude `indeterminate` state --- src/__tests__/ariaAttributes.js | 22 ++++++++++++++++++++++ src/role-helpers.js | 3 +++ 2 files changed, 25 insertions(+) diff --git a/src/__tests__/ariaAttributes.js b/src/__tests__/ariaAttributes.js index 2bdb0883..981bf8ba 100644 --- a/src/__tests__/ariaAttributes.js +++ b/src/__tests__/ariaAttributes.js @@ -40,6 +40,28 @@ test('`checked: true|false` matches `checked` elements with proper role', () => expect(getByRole('checkbox', {checked: false})).toBeInTheDocument() }) +test('`checked: true|false` does not match element in `indeterminate` state', () => { + const {queryByRole, getByLabelText} = renderIntoDocument( + `
+ not so much + + +
`, + ) + getByLabelText(/indeteminate yes/i).indeterminate = true + getByLabelText(/indeteminate no/i).indeterminate = true + + expect( + queryByRole('checkbox', {checked: true, name: /indeteminate yes/i}), + ).toBeNull() + expect( + queryByRole('checkbox', {checked: true, name: /indeteminate no/i}), + ).toBeNull() + expect( + queryByRole('checkbox', {checked: true, name: /not so much/i}), + ).toBeNull() +}) + test('`selected: true` matches `aria-selected="true"` on supported roles', () => { const {getAllByRole} = render(`