mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-04 16:44:11 +03:00
fix(role): account for unslotted elements being hidden for aria (#22070)
When element is not assigned to any slot in the shadow root, it is not rendered and is considered hidden for ARIA in all browsers. In Chromium/Firefox we use `Element.checkVisibility` that already handles this, but in WebKit we have to check it manually. Fixes #21487.
This commit is contained in:
parent
0c4eedbabe
commit
548e4a0c0f
@ -232,6 +232,8 @@ function getAriaBoolean(attr: string | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion, but including "none" and "presentation" roles
|
// 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
|
// https://www.w3.org/TR/wai-aria-1.2/#aria-hidden
|
||||||
export function isElementHiddenForAria(element: Element, cache: Map<Element, boolean>): boolean {
|
export function isElementHiddenForAria(element: Element, cache: Map<Element, boolean>): boolean {
|
||||||
if (['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(element.tagName))
|
if (['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(element.tagName))
|
||||||
@ -242,17 +244,30 @@ export function isElementHiddenForAria(element: Element, cache: Map<Element, boo
|
|||||||
const isSlot = element.nodeName === 'SLOT';
|
const isSlot = element.nodeName === 'SLOT';
|
||||||
if (!isOptionInsideSelect && !isSlot && !isElementStyleVisibilityVisible(element))
|
if (!isOptionInsideSelect && !isSlot && !isElementStyleVisibilityVisible(element))
|
||||||
return true;
|
return true;
|
||||||
return belongsToDisplayNoneOrAriaHidden(element, cache);
|
return belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element, cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
function belongsToDisplayNoneOrAriaHidden(element: Element, cache: Map<Element, boolean>): boolean {
|
function belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element: Element, cache: Map<Element, boolean>): boolean {
|
||||||
if (!cache.has(element)) {
|
if (!cache.has(element)) {
|
||||||
|
let hidden = false;
|
||||||
|
|
||||||
|
// When parent has a shadow root, all light dom children must be assigned to a slot,
|
||||||
|
// otherwise they are not rendered and considered hidden for aria.
|
||||||
|
// Note: we can remove this logic once WebKit supports `Element.checkVisibility`.
|
||||||
|
if (element.parentElement && element.parentElement.shadowRoot && !element.assignedSlot)
|
||||||
|
hidden = true;
|
||||||
|
|
||||||
|
// display:none and aria-hidden=true are considered hidden for aria.
|
||||||
|
if (!hidden) {
|
||||||
const style = getElementComputedStyle(element);
|
const style = getElementComputedStyle(element);
|
||||||
let hidden = !style || style.display === 'none' || getAriaBoolean(element.getAttribute('aria-hidden')) === true;
|
hidden = !style || style.display === 'none' || getAriaBoolean(element.getAttribute('aria-hidden')) === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check recursively.
|
||||||
if (!hidden) {
|
if (!hidden) {
|
||||||
const parent = parentElementOrShadowHost(element);
|
const parent = parentElementOrShadowHost(element);
|
||||||
if (parent)
|
if (parent)
|
||||||
hidden = hidden || belongsToDisplayNoneOrAriaHidden(parent, cache);
|
hidden = belongsToDisplayNoneOrAriaHiddenOrNonSlotted(parent, cache);
|
||||||
}
|
}
|
||||||
cache.set(element, hidden);
|
cache.set(element, hidden);
|
||||||
}
|
}
|
||||||
|
@ -446,3 +446,36 @@ test('errors', async ({ page }) => {
|
|||||||
const e8 = await page.$('role=treeitem[expanded="none"]').catch(e => e);
|
const e8 = await page.$('role=treeitem[expanded="none"]').catch(e => e);
|
||||||
expect(e8.message).toContain(`"expanded" must be one of true, false`);
|
expect(e8.message).toContain(`"expanded" must be one of true, false`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('hidden with shadow dom slots', async ({ page }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<div make-hidden>
|
||||||
|
<button>hidden1</button>
|
||||||
|
</div>
|
||||||
|
<div make-hidden>
|
||||||
|
<span><button>hidden2</button></v>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button>visible1</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span><button>visible2</button></span>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
for (const div of document.querySelectorAll('div')) {
|
||||||
|
const hidden = div.hasAttribute('make-hidden');
|
||||||
|
div.attachShadow({ mode: 'open' }).innerHTML = hidden ? 'nothing to see here' : '<slot></slot>';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
`);
|
||||||
|
expect(await page.locator(`role=button`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||||
|
`<button>visible1</button>`,
|
||||||
|
`<button>visible2</button>`,
|
||||||
|
]);
|
||||||
|
expect(await page.locator(`role=button[include-hidden]`).evaluateAll(els => els.map(e => e.outerHTML))).toEqual([
|
||||||
|
`<button>hidden1</button>`,
|
||||||
|
`<button>hidden2</button>`,
|
||||||
|
`<button>visible1</button>`,
|
||||||
|
`<button>visible2</button>`,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user