Skip to content

Commit

Permalink
feat(ByRole): Allow filter by value state (#1223)
Browse files Browse the repository at this point in the history
* feat(ByRole): All filter by value state

* Fix coverage
  • Loading branch information
eps1lon authored Mar 24, 2023
1 parent 8c40a21 commit eadf748
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 0 deletions.
99 changes: 99 additions & 0 deletions src/__tests__/ariaAttributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,105 @@ test('`level` throws on unsupported roles', () => {
)
})

test('`value.now` throws on unsupported roles', () => {
const {getByRole} = render(`<button aria-valuenow="1">Button</button>`)
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(
`<div>
<button role="spinbutton" />
<button role="spinbutton" aria-valuenow="5" />
<button role="spinbutton" aria-valuenow="10" />
</div>`,
)
expect(getByRole('spinbutton', {value: {now: 5}})).toBeInTheDocument()
expect(getByRole('spinbutton', {value: {now: 10}})).toBeInTheDocument()
})

test('`value.max` throws on unsupported roles', () => {
const {getByRole} = render(`<button aria-valuemax="1">Button</button>`)
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(
`<div>
<button role="spinbutton" />
<button role="spinbutton" aria-valuemax="5" />
<button role="spinbutton" aria-valuemax="10" />
</div>`,
)
expect(getByRole('spinbutton', {value: {max: 5}})).toBeInTheDocument()
expect(getByRole('spinbutton', {value: {max: 10}})).toBeInTheDocument()
})

test('`value.min` throws on unsupported roles', () => {
const {getByRole} = render(`<button aria-valuemin="1">Button</button>`)
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(
`<div>
<button role="spinbutton" />
<button role="spinbutton" aria-valuemin="5" />
<button role="spinbutton" aria-valuemin="10" />
</div>`,
)
expect(getByRole('spinbutton', {value: {min: 5}})).toBeInTheDocument()
expect(getByRole('spinbutton', {value: {min: 10}})).toBeInTheDocument()
})

test('`value.text` throws on unsupported roles', () => {
const {getByRole} = render(`<button aria-valuetext="one">Button</button>`)
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(
`<div>
<button role="spinbutton" />
<button role="spinbutton" aria-valuetext="zero" />
<button role="spinbutton" aria-valuetext="few" />
<button role="spinbutton" aria-valuetext="many" />
</div>`,
)
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(
`<div>
<button role="spinbutton" aria-valuemin="0" aria-valuenow="1" aria-valuemax="10" aria-valuetext="eins" />
<button role="spinbutton" aria-valuemin="0" aria-valuenow="1" aria-valuemax="10" aria-valuetext="one" />
</div>`,
)
expect(
getByRole('spinbutton', {value: {now: 1, text: 'one'}}),
).toBeInTheDocument()
})

test('`expanded: true|false` matches `expanded` buttons', () => {
const {getByRole} = renderIntoDocument(
`<div>
Expand Down
78 changes: 78 additions & 0 deletions src/queries/role.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable complexity */
import {
computeAccessibleDescription,
computeAccessibleName,
Expand All @@ -14,6 +15,10 @@ import {
computeAriaPressed,
computeAriaCurrent,
computeAriaExpanded,
computeAriaValueNow,
computeAriaValueMax,
computeAriaValueMin,
computeAriaValueText,
computeHeadingLevel,
getImplicitAriaRoles,
prettyRoles,
Expand Down Expand Up @@ -49,6 +54,12 @@ const queryAllByRole: AllByRole = (
current,
level,
expanded,
value: {
now: valueNow,
min: valueMin,
max: valueMax,
text: valueText,
} = {} as NonNullable<ByRoleOptions['value']>,
} = {},
) => {
checkContainerType(container)
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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
})
Expand Down
40 changes: 40 additions & 0 deletions src/role-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -347,5 +383,9 @@ export {
computeAriaPressed,
computeAriaCurrent,
computeAriaExpanded,
computeAriaValueNow,
computeAriaValueMax,
computeAriaValueMin,
computeAriaValueText,
computeHeadingLevel,
}
6 changes: 6 additions & 0 deletions types/queries.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <div role="meter progressbar">`.
Expand Down

0 comments on commit eadf748

Please sign in to comment.