diff --git a/src/__tests__/ariaAttributes.js b/src/__tests__/ariaAttributes.js index 498f21b3..7984d5e0 100644 --- a/src/__tests__/ariaAttributes.js +++ b/src/__tests__/ariaAttributes.js @@ -259,6 +259,105 @@ test('`level` throws on unsupported roles', () => { ) }) +test('`value.now` throws on unsupported roles', () => { + const {getByRole} = render(``) + expect(() => + getByRole('button', {value: {now: 1}}), + ).toThrowErrorMatchingInlineSnapshot( + `"aria-valuenow" is not supported on role "button".`, + ) +}) + +test('`value.now: number` matches `aria-valuenow` on widgets', () => { + const {getByRole} = renderIntoDocument( + `
+
`, + ) + expect(getByRole('spinbutton', {value: {now: 5}})).toBeInTheDocument() + expect(getByRole('spinbutton', {value: {now: 10}})).toBeInTheDocument() +}) + +test('`value.max` throws on unsupported roles', () => { + const {getByRole} = render(``) + expect(() => + getByRole('button', {value: {max: 1}}), + ).toThrowErrorMatchingInlineSnapshot( + `"aria-valuemax" is not supported on role "button".`, + ) +}) + +test('`value.max: number` matches `aria-valuemax` on widgets', () => { + const {getByRole} = renderIntoDocument( + `
+
`, + ) + expect(getByRole('spinbutton', {value: {max: 5}})).toBeInTheDocument() + expect(getByRole('spinbutton', {value: {max: 10}})).toBeInTheDocument() +}) + +test('`value.min` throws on unsupported roles', () => { + const {getByRole} = render(``) + expect(() => + getByRole('button', {value: {min: 1}}), + ).toThrowErrorMatchingInlineSnapshot( + `"aria-valuemin" is not supported on role "button".`, + ) +}) + +test('`value.min: number` matches `aria-valuemin` on widgets', () => { + const {getByRole} = renderIntoDocument( + `
+
`, + ) + expect(getByRole('spinbutton', {value: {min: 5}})).toBeInTheDocument() + expect(getByRole('spinbutton', {value: {min: 10}})).toBeInTheDocument() +}) + +test('`value.text` throws on unsupported roles', () => { + const {getByRole} = render(``) + expect(() => + getByRole('button', {value: {text: 'one'}}), + ).toThrowErrorMatchingInlineSnapshot( + `"aria-valuetext" is not supported on role "button".`, + ) +}) + +test('`value.text: Matcher` matches `aria-valuetext` on widgets', () => { + const {getAllByRole, getByRole} = renderIntoDocument( + `
+
`, + ) + expect(getByRole('spinbutton', {value: {text: 'zero'}})).toBeInTheDocument() + expect(getAllByRole('spinbutton', {value: {text: /few|many/}})).toHaveLength( + 2, + ) +}) + +test('`value.*` must all match if specified', () => { + const {getByRole} = renderIntoDocument( + `
+
`, + ) + expect( + getByRole('spinbutton', {value: {now: 1, text: 'one'}}), + ).toBeInTheDocument() +}) + test('`expanded: true|false` matches `expanded` buttons', () => { const {getByRole} = renderIntoDocument( `
diff --git a/src/queries/role.ts b/src/queries/role.ts index 5655e2ba..98b08848 100644 --- a/src/queries/role.ts +++ b/src/queries/role.ts @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ import { computeAccessibleDescription, computeAccessibleName, @@ -14,6 +15,10 @@ import { computeAriaPressed, computeAriaCurrent, computeAriaExpanded, + computeAriaValueNow, + computeAriaValueMax, + computeAriaValueMin, + computeAriaValueText, computeHeadingLevel, getImplicitAriaRoles, prettyRoles, @@ -49,6 +54,12 @@ const queryAllByRole: AllByRole = ( current, level, expanded, + value: { + now: valueNow, + min: valueMin, + max: valueMax, + text: valueText, + } = {} as NonNullable, } = {}, ) => { checkContainerType(container) @@ -113,6 +124,46 @@ const queryAllByRole: AllByRole = ( } } + if (valueNow !== undefined) { + // guard against unknown roles + if ( + allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-valuenow'] === + undefined + ) { + throw new Error(`"aria-valuenow" is not supported on role "${role}".`) + } + } + + if (valueMax !== undefined) { + // guard against unknown roles + if ( + allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-valuemax'] === + undefined + ) { + throw new Error(`"aria-valuemax" is not supported on role "${role}".`) + } + } + + if (valueMin !== undefined) { + // guard against unknown roles + if ( + allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-valuemin'] === + undefined + ) { + throw new Error(`"aria-valuemin" is not supported on role "${role}".`) + } + } + + if (valueText !== undefined) { + // guard against unknown roles + if ( + allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-valuetext'] === + undefined + ) { + throw new Error(`"aria-valuetext" is not supported on role "${role}".`) + } + } + if (expanded !== undefined) { // guard against unknown roles if ( @@ -182,6 +233,33 @@ const queryAllByRole: AllByRole = ( if (level !== undefined) { return level === computeHeadingLevel(element) } + if ( + valueNow !== undefined || + valueMax !== undefined || + valueMin !== undefined || + valueText !== undefined + ) { + let valueMatches = true + if (valueNow !== undefined) { + valueMatches &&= valueNow === computeAriaValueNow(element) + } + if (valueMax !== undefined) { + valueMatches &&= valueMax === computeAriaValueMax(element) + } + if (valueMin !== undefined) { + valueMatches &&= valueMin === computeAriaValueMin(element) + } + if (valueText !== undefined) { + valueMatches &&= matches( + computeAriaValueText(element) ?? null, + element, + valueText, + text => text, + ) + } + + return valueMatches + } // don't care if aria attributes are unspecified return true }) diff --git a/src/role-helpers.js b/src/role-helpers.js index 7a8f3c82..bc134f27 100644 --- a/src/role-helpers.js +++ b/src/role-helpers.js @@ -334,6 +334,42 @@ function computeHeadingLevel(element) { return ariaLevelAttribute || implicitHeadingLevels[element.tagName] } +/** + * @param {Element} element - + * @returns {number | undefined} - + */ +function computeAriaValueNow(element) { + const valueNow = element.getAttribute('aria-valuenow') + return valueNow === null ? undefined : +valueNow +} + +/** + * @param {Element} element - + * @returns {number | undefined} - + */ +function computeAriaValueMax(element) { + const valueMax = element.getAttribute('aria-valuemax') + return valueMax === null ? undefined : +valueMax +} + +/** + * @param {Element} element - + * @returns {number | undefined} - + */ +function computeAriaValueMin(element) { + const valueMin = element.getAttribute('aria-valuemin') + return valueMin === null ? undefined : +valueMin +} + +/** + * @param {Element} element - + * @returns {string | undefined} - + */ +function computeAriaValueText(element) { + const valueText = element.getAttribute('aria-valuetext') + return valueText === null ? undefined : valueText +} + export { getRoles, logRoles, @@ -347,5 +383,9 @@ export { computeAriaPressed, computeAriaCurrent, computeAriaExpanded, + computeAriaValueNow, + computeAriaValueMax, + computeAriaValueMin, + computeAriaValueText, computeHeadingLevel, } diff --git a/types/queries.d.ts b/types/queries.d.ts index c6568abf..c6ce9054 100644 --- a/types/queries.d.ts +++ b/types/queries.d.ts @@ -110,6 +110,12 @@ export interface ByRoleOptions { * the `aria-level` attribute. */ level?: number + value?: { + now?: number + min?: number + max?: number + text?: Matcher + } /** * Includes every role used in the `role` attribute * For example *ByRole('progressbar', {queryFallbacks: true})` will find
`.