fix(role): make sure to ignore style/script/noscript/template (#32231)

Even when these are a part of a hidden `aria-labelledby` traversal, all
browsers ignore them anyway.
This commit is contained in:
Dmitry Gozman 2024-08-20 09:02:23 -07:00 committed by GitHub
parent b599335404
commit b4a9b247b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 29 additions and 9 deletions

View file

@ -261,12 +261,16 @@ function getAriaBoolean(attr: string | null) {
return attr === null ? undefined : attr.toLowerCase() === 'true';
}
function isElementIgnoredForAria(element: Element) {
return ['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(elementSafeTagName(element));
}
// https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion, but including "none" and "presentation" roles
// Not implemented:
// `Any descendants of elements that have the characteristic "Children Presentational: True"`
// https://www.w3.org/TR/wai-aria-1.2/#aria-hidden
export function isElementHiddenForAria(element: Element): boolean {
if (['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(elementSafeTagName(element)))
if (isElementIgnoredForAria(element))
return true;
const style = getElementComputedStyle(element);
const isSlot = element.nodeName === 'SLOT';
@ -496,14 +500,17 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
// step 2a. Hidden Not Referenced: If the current node is hidden and is:
// Not part of an aria-labelledby or aria-describedby traversal, where the node directly referenced by that relation was hidden.
// Nor part of a native host language text alternative element (e.g. label in HTML) or attribute traversal, where the root of that traversal was hidden.
if (!options.includeHidden &&
!options.embeddedInLabelledBy?.hidden &&
!options.embeddedInDescribedBy?.hidden &&
!options?.embeddedInNativeTextAlternative?.hidden &&
!options?.embeddedInLabel?.hidden &&
isElementHiddenForAria(element)) {
options.visitedElements.add(element);
return '';
if (!options.includeHidden) {
const isEmbeddedInHiddenReferenceTraversal =
!!options.embeddedInLabelledBy?.hidden ||
!!options.embeddedInDescribedBy?.hidden ||
!!options.embeddedInNativeTextAlternative?.hidden ||
!!options.embeddedInLabel?.hidden;
if (isElementIgnoredForAria(element) ||
(!isEmbeddedInHiddenReferenceTraversal && isElementHiddenForAria(element))) {
options.visitedElements.add(element);
return '';
}
}
const labelledBy = getAriaLabelledByElements(element);

View file

@ -462,6 +462,19 @@ test('should work with form and tricky input names', async ({ page }) => {
expect.soft(await getNameAndRole(page, 'form')).toEqual({ role: 'form', name: 'my form' });
});
test('should ignore stylesheet from hidden aria-labelledby subtree', async ({ page }) => {
await page.setContent(`
<div id=mylabel style="display:none">
<template shadowrootmode=open>
<style>span { color: red; }</style>
<span>hello</span>
</template>
</div>
<input aria-labelledby=mylabel type=text>
`);
expect.soft(await getNameAndRole(page, 'input')).toEqual({ role: 'textbox', name: 'hello' });
});
function toArray(x: any): any[] {
return Array.isArray(x) ? x : [x];
}