Skip to content

chore: Migrate ByRole to TypeScript #1186

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Feb 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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[]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: what would the type be without the cast`

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

role-helpers need migration to typescript so it return any[]


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