fix(role): accessibleName computation should walk the flat dom tree (#19301)

- When visiting `<slot>` element, descend into assigned nodes.
- When node has `assignedSlot`, skip it during regular traversal.

Fixes #18989.
This commit is contained in:
Dmitry Gozman 2022-12-06 09:31:26 -08:00 committed by GitHub
parent 8660288518
commit 4784dae10f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 76 additions and 8 deletions

View File

@ -579,7 +579,9 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
if (allowsNameFromContent || options.embeddedInLabelledBy !== 'none' || options.embeddedInLabel !== 'none' || options.embeddedInTextAlternativeElement || options.embeddedInTargetElement === 'descendant') {
options.visitedElements.add(element);
const tokens: string[] = [];
const visit = (node: Node) => {
const visit = (node: Node, skipSlotted: boolean) => {
if (skipSlotted && (node as Element | Text).assignedSlot)
return;
if (node.nodeType === 1 /* Node.ELEMENT_NODE */) {
const display = getComputedStyle(node as Element)?.getPropertyValue('display') || 'inline';
let token = getElementAccessibleNameInternal(node as Element, childOptions);
@ -596,14 +598,20 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
}
};
tokens.push(getPseudoContent(getComputedStyle(element, '::before')));
for (let child = element.firstChild; child; child = child.nextSibling)
visit(child);
if (element.shadowRoot) {
for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling)
visit(child);
const assignedNodes = element.nodeName === 'SLOT' ? (element as HTMLSlotElement).assignedNodes() : [];
if (assignedNodes.length) {
for (const child of assignedNodes)
visit(child, false);
} else {
for (let child = element.firstChild; child; child = child.nextSibling)
visit(child, true);
if (element.shadowRoot) {
for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling)
visit(child, true);
}
for (const owned of getIdRefs(element, element.getAttribute('aria-owns')))
visit(owned, true);
}
for (const owned of getIdRefs(element, element.getAttribute('aria-owns')))
visit(owned);
tokens.push(getPseudoContent(getComputedStyle(element, '::after')));
const accessibleName = tokens.join('');
if (accessibleName.trim())

View File

@ -423,3 +423,63 @@ test('errors', async ({ page }) => {
const e8 = await page.$('role=treeitem[expanded="none"]').catch(e => e);
expect(e8.message).toContain(`"expanded" must be one of true, false`);
});
test('should detect accessible name with slots', async ({ page }) => {
// Text "foo" is assigned to the slot, should not be used twice.
await page.setContent(`
<button><div>foo</div></button>
<script>
(() => {
const container = document.querySelector('div');
const shadow = container.attachShadow({ mode: 'open' });
const slot = document.createElement('slot');
shadow.appendChild(slot);
})();
</script>
`);
expect(await page.locator(`role=button[name="foo"]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
`<button><div>foo</div></button>`,
]);
// Text "foo" is assigned to the slot, should be used instead of slot content.
await page.setContent(`
<div>foo</div>
<script>
(() => {
const container = document.querySelector('div');
const shadow = container.attachShadow({ mode: 'open' });
const button = document.createElement('button');
shadow.appendChild(button);
const slot = document.createElement('slot');
button.appendChild(slot);
const span = document.createElement('span');
span.textContent = 'pre';
slot.appendChild(span);
})();
</script>
`);
expect(await page.locator(`role=button[name="foo"]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
`<button><slot><span>pre</span></slot></button>`,
]);
// Nothing is assigned to the slot, should use slot content.
await page.setContent(`
<div></div>
<script>
(() => {
const container = document.querySelector('div');
const shadow = container.attachShadow({ mode: 'open' });
const button = document.createElement('button');
shadow.appendChild(button);
const slot = document.createElement('slot');
button.appendChild(slot);
const span = document.createElement('span');
span.textContent = 'pre';
slot.appendChild(span);
})();
</script>
`);
expect(await page.locator(`role=button[name="pre"]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
`<button><slot><span>pre</span></slot></button>`,
]);
});