Skip to content

Commit

Permalink
chore: Migrate ByRole to TypeScript (#1186)
Browse files Browse the repository at this point in the history
* fix: migrate role to ts

* fix: remove role model abstraction

* fix: prettier

* fix: force have role that are included in type

* fix: type

* fix: error and add commet ts

* fix: any type

* fix: any type

* fix: type assertion

* fix: ignore branch coverage

* fix: typo

* fix: add hints in all by role

* fix: types and comments

* fix: restore types

* fix: restore types

* fix: lint

* fix: ts error

* fix: ts error

* fix: type tests

* Fix lint

* Don't couple query-helpers with ByRole

* Revert change to ByRoleMatcher type

---------

Co-authored-by: eps1lon <silbermann.sebastian@gmail.com>
  • Loading branch information
DaniAcu and eps1lon committed Feb 6, 2023
1 parent 25dc8a9 commit 42809fe
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 39 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"@typescript-eslint/prefer-optional-chain": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "off",
"@typescript-eslint/prefer-includes": "off",
"import/prefer-default-export": "off",
"import/no-unassigned-import": "off",
Expand Down
100 changes: 69 additions & 31 deletions src/queries/role.js → src/queries/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import {
computeAccessibleDescription,
computeAccessibleName,
} from 'dom-accessibility-api'
import {roles as allRoles, roleElements} from 'aria-query'
import {
roles as allRoles,
roleElements,
ARIARoleDefinitionKey,
} from 'aria-query'
import {
computeAriaSelected,
computeAriaChecked,
Expand All @@ -17,6 +21,17 @@ import {
} from '../role-helpers'
import {wrapAllByQueryWithSuggestion} from '../query-helpers'
import {checkContainerType} from '../helpers'
import {
AllByRole,
ByRoleMatcher,
ByRoleOptions,
GetErrorFunction,
Matcher,
MatcherFunction,
MatcherOptions,
NormalizerFn,
} from '../../types'

import {
buildQueries,
fuzzyMatches,
Expand All @@ -25,7 +40,7 @@ import {
matches,
} from './all-utils'

function queryAllByRole(
const queryAllByRole: AllByRole = (
container,
role,
{
Expand All @@ -44,28 +59,37 @@ function queryAllByRole(
level,
expanded,
} = {},
) {
) => {
checkContainerType(container)
const matcher = exact ? matches : fuzzyMatches
const matchNormalizer = makeNormalizer({collapseWhitespace, trim, normalizer})

if (selected !== undefined) {
// guard against unknown roles
if (allRoles.get(role)?.props['aria-selected'] === undefined) {
if (
allRoles.get(role as ARIARoleDefinitionKey)?.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) {
if (
allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-checked'] ===
undefined
) {
throw new Error(`"aria-checked" is not supported on role "${role}".`)
}
}

if (pressed !== undefined) {
// guard against unknown roles
if (allRoles.get(role)?.props['aria-pressed'] === undefined) {
if (
allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-pressed'] ===
undefined
) {
throw new Error(`"aria-pressed" is not supported on role "${role}".`)
}
}
Expand All @@ -75,7 +99,10 @@ function queryAllByRole(
// guard against unknown roles
// All currently released ARIA versions support `aria-current` on all roles.
// Leaving this for symetry and forward compatibility
if (allRoles.get(role)?.props['aria-current'] === undefined) {
if (
allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-current'] ===
undefined
) {
throw new Error(`"aria-current" is not supported on role "${role}".`)
}
}
Expand All @@ -89,22 +116,25 @@ function queryAllByRole(

if (expanded !== undefined) {
// guard against unknown roles
if (allRoles.get(role)?.props['aria-expanded'] === undefined) {
if (
allRoles.get(role as ARIARoleDefinitionKey)?.props['aria-expanded'] ===
undefined
) {
throw new Error(`"aria-expanded" is not supported on role "${role}".`)
}
}

const subtreeIsInaccessibleCache = new WeakMap()
function cachedIsSubtreeInaccessible(element) {
const subtreeIsInaccessibleCache = new WeakMap<Element, Boolean>()
function cachedIsSubtreeInaccessible(element: Element) {
if (!subtreeIsInaccessibleCache.has(element)) {
subtreeIsInaccessibleCache.set(element, isSubtreeInaccessible(element))
}

return subtreeIsInaccessibleCache.get(element)
return subtreeIsInaccessibleCache.get(element) as boolean
}

return Array.from(
container.querySelectorAll(
container.querySelectorAll<HTMLElement>(
// Only query elements that can be matched by the following filters
makeRoleSelector(role, exact, normalizer ? matchNormalizer : undefined),
),
Expand All @@ -113,26 +143,26 @@ function queryAllByRole(
const isRoleSpecifiedExplicitly = node.hasAttribute('role')

if (isRoleSpecifiedExplicitly) {
const roleValue = node.getAttribute('role')
const roleValue = node.getAttribute('role') as string
if (queryFallbacks) {
return roleValue
.split(' ')
.filter(Boolean)
.some(text => matcher(text, node, role, matchNormalizer))
.some(text => matcher(text, node, role as Matcher, matchNormalizer))
}
// if a custom normalizer is passed then let normalizer handle the role value
if (normalizer) {
return matcher(roleValue, node, role, matchNormalizer)
return matcher(roleValue, node, role as Matcher, matchNormalizer)
}
// other wise only send the first word to match
const [firstWord] = roleValue.split(' ')
return matcher(firstWord, node, role, matchNormalizer)
return matcher(firstWord, node, role as Matcher, matchNormalizer)
}

const implicitRoles = getImplicitAriaRoles(node)
const implicitRoles = getImplicitAriaRoles(node) as string[]

return implicitRoles.some(implicitRole =>
matcher(implicitRole, node, role, matchNormalizer),
matcher(implicitRole, node, role as Matcher, matchNormalizer),
)
})
.filter(element => {
Expand Down Expand Up @@ -169,7 +199,7 @@ function queryAllByRole(
getConfig().computedStyleSupportsPseudoElements,
}),
element,
name,
name as MatcherFunction,
text => text,
)
})
Expand All @@ -185,7 +215,7 @@ function queryAllByRole(
getConfig().computedStyleSupportsPseudoElements,
}),
element,
description,
description as Matcher,
text => text,
)
})
Expand All @@ -198,7 +228,11 @@ function queryAllByRole(
})
}

function makeRoleSelector(role, exact, customNormalizer) {
function makeRoleSelector(
role: ByRoleMatcher,
exact: boolean,
customNormalizer?: NormalizerFn,
) {
if (typeof role !== 'string') {
// For non-string role parameters we can not determine the implicitRoleSelectors.
return '*'
Expand All @@ -207,7 +241,8 @@ function makeRoleSelector(role, exact, customNormalizer) {
const explicitRoleSelector =
exact && !customNormalizer ? `*[role~="${role}"]` : '*[role]'

const roleRelations = roleElements.get(role) ?? new Set()
const roleRelations =
roleElements.get(role as ARIARoleDefinitionKey) ?? new Set()
const implicitRoleSelectors = new Set(
Array.from(roleRelations).map(({name}) => name),
)
Expand All @@ -220,7 +255,7 @@ function makeRoleSelector(role, exact, customNormalizer) {
.join(',')
}

const getNameHint = name => {
const getNameHint = (name: ByRoleOptions['name']): string => {
let nameHint = ''
if (name === undefined) {
nameHint = ''
Expand All @@ -233,11 +268,15 @@ const getNameHint = name => {
return nameHint
}

const getMultipleError = (c, role, {name} = {}) => {
const getMultipleError: GetErrorFunction<
[matcher: ByRoleMatcher, options: ByRoleOptions]
> = (c, role, {name} = {}) => {
return `Found multiple elements with the role "${role}"${getNameHint(name)}`
}

const getMissingError = (
const getMissingError: GetErrorFunction<
[matcher: ByRoleMatcher, options: ByRoleOptions]
> = (
container,
role,
{hidden = getConfig().defaultHidden, name, description} = {},
Expand All @@ -247,7 +286,7 @@ const getMissingError = (
}

let roles = ''
Array.from(container.children).forEach(childElement => {
Array.from((container as Element).children).forEach(childElement => {
roles += prettyRoles(childElement, {
hidden,
includeDescription: description !== undefined,
Expand Down Expand Up @@ -297,11 +336,10 @@ Unable to find an ${
${roleMessage}`.trim()
}
const queryAllByRoleWithSuggestions = wrapAllByQueryWithSuggestion(
queryAllByRole,
queryAllByRole.name,
'queryAll',
)
const queryAllByRoleWithSuggestions = wrapAllByQueryWithSuggestion<
// @ts-expect-error -- See `wrapAllByQueryWithSuggestion` Argument constraint comment
[labelText: Matcher, options?: MatcherOptions]
>(queryAllByRole, queryAllByRole.name, 'queryAll')
const [queryByRole, getAllByRole, getByRole, findAllByRole, findByRole] =
buildQueries(queryAllByRole, getMultipleError, getMissingError)

Expand Down
14 changes: 7 additions & 7 deletions src/query-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,16 @@ function makeGetAllQuery<Arguments extends unknown[]>(

// this accepts a getter query function and returns a function which calls
// waitFor and passing a function which invokes the getter.
function makeFindQuery<QueryFor>(
function makeFindQuery<QueryFor, QueryMatcher>(
getter: (
container: HTMLElement,
text: Matcher,
text: QueryMatcher,
options: MatcherOptions,
) => QueryFor,
) {
return (
container: HTMLElement,
text: Matcher,
text: QueryMatcher,
options: MatcherOptions,
waitForOptions: WaitForOptions,
) => {
Expand Down Expand Up @@ -209,16 +209,16 @@ const wrapAllByQueryWithSuggestion =
// TODO: This deviates from the published declarations
// However, the implementation always required a dyadic (after `container`) not variadic `queryAllBy` considering the implementation of `makeFindQuery`
// This is at least statically true and can be verified by accepting `QueryMethod<Arguments, HTMLElement[]>`
function buildQueries(
function buildQueries<QueryMatcher>(
queryAllBy: QueryMethod<
[matcher: Matcher, options: MatcherOptions],
[matcher: QueryMatcher, options: MatcherOptions],
HTMLElement[]
>,
getMultipleError: GetErrorFunction<
[matcher: Matcher, options: MatcherOptions]
[matcher: QueryMatcher, options: MatcherOptions]
>,
getMissingError: GetErrorFunction<
[matcher: Matcher, options: MatcherOptions]
[matcher: QueryMatcher, options: MatcherOptions]
>,
) {
const queryBy = wrapSingleQueryWithSuggestion(
Expand Down
1 change: 0 additions & 1 deletion types/__tests__/type-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,6 @@ export async function testByRole() {
}) === null,
)

// allow to query for a role that isn't included in the types
console.assert(queryByRole(element, 'foo') === null)
console.assert(queryByRole(element, /foo/) === null)
console.assert(screen.queryByRole('foo') === null)
Expand Down

0 comments on commit 42809fe

Please sign in to comment.