chore(aria): do not generate refs for pointer-events none (#35819)

This commit is contained in:
Pavel Feldman 2025-04-30 17:25:06 -07:00 committed by GitHub
parent cb99e260fa
commit eda5a9efeb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 75 additions and 5 deletions

View File

@ -29,6 +29,7 @@ export type AriaNode = AriaProps & {
children: (AriaNode | string)[]; children: (AriaNode | string)[];
element: Element; element: Element;
box: Box; box: Box;
receivesPointerEvents: boolean;
props: Record<string, string>; props: Record<string, string>;
}; };
@ -43,7 +44,7 @@ export function generateAriaTree(rootElement: Element, generation: number, optio
const visited = new Set<Node>(); const visited = new Set<Node>();
const snapshot: AriaSnapshot = { const snapshot: AriaSnapshot = {
root: { role: 'fragment', name: '', children: [], element: rootElement, props: {}, box: box(rootElement) }, root: { role: 'fragment', name: '', children: [], element: rootElement, props: {}, box: box(rootElement), receivesPointerEvents: true },
elements: new Map<number, Element>(), elements: new Map<number, Element>(),
generation, generation,
ids: new Map<Element, number>(), ids: new Map<Element, number>(),
@ -148,7 +149,7 @@ export function generateAriaTree(rootElement: Element, generation: number, optio
function toAriaNode(element: Element, options?: { emitGeneric?: boolean }): AriaNode | null { function toAriaNode(element: Element, options?: { emitGeneric?: boolean }): AriaNode | null {
if (element.nodeName === 'IFRAME') if (element.nodeName === 'IFRAME')
return { role: 'iframe', name: '', children: [], props: {}, element, box: box(element) }; return { role: 'iframe', name: '', children: [], props: {}, element, box: box(element), receivesPointerEvents: true };
const defaultRole = options?.emitGeneric ? 'generic' : null; const defaultRole = options?.emitGeneric ? 'generic' : null;
const role = roleUtils.getAriaRole(element) ?? defaultRole; const role = roleUtils.getAriaRole(element) ?? defaultRole;
@ -156,7 +157,8 @@ function toAriaNode(element: Element, options?: { emitGeneric?: boolean }): Aria
return null; return null;
const name = normalizeWhiteSpace(roleUtils.getElementAccessibleName(element, false) || ''); const name = normalizeWhiteSpace(roleUtils.getElementAccessibleName(element, false) || '');
const result: AriaNode = { role, name, children: [], props: {}, element, box: box(element) }; const receivesPointerEvents = roleUtils.receivesPointerEvents(element);
const result: AriaNode = { role, name, children: [], props: {}, element, box: box(element), receivesPointerEvents };
if (roleUtils.kAriaCheckedRoles.includes(role)) if (roleUtils.kAriaCheckedRoles.includes(role))
result.checked = roleUtils.getAriaChecked(element); result.checked = roleUtils.getAriaChecked(element);
@ -406,7 +408,7 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, options?: { mode?: 'r
key += ` [pressed]`; key += ` [pressed]`;
if (ariaNode.selected === true) if (ariaNode.selected === true)
key += ` [selected]`; key += ` [selected]`;
if (options?.ref && ariaNode.box.visible) { if (options?.ref && ariaNode.box.visible && ariaNode.receivesPointerEvents) {
const id = ariaSnapshot.ids.get(ariaNode.element); const id = ariaSnapshot.ids.get(ariaNode.element);
if (id) if (id)
key += ` [ref=s${ariaSnapshot.generation}e${id}]`; key += ` [ref=s${ariaSnapshot.generation}e${id}]`;

View File

@ -1051,6 +1051,40 @@ function getAccessibleNameFromAssociatedLabels(labels: Iterable<HTMLLabelElement
})).filter(accessibleName => !!accessibleName).join(' '); })).filter(accessibleName => !!accessibleName).join(' ');
} }
export function receivesPointerEvents(element: Element): boolean {
const cache = cachePointerEvents!;
let e: Element | undefined = element;
let result: boolean | undefined;
const parents: Element[] = [];
for (; e; e = parentElementOrShadowHost(e!)) {
const cached = cache.get(e);
if (cached !== undefined) {
result = cached;
break;
}
parents.push(e);
const style = getElementComputedStyle(e);
if (!style) {
result = true;
break;
}
const value = style.pointerEvents;
if (value) {
result = value !== 'none';
break;
}
}
if (result === undefined)
result = true;
for (const parent of parents)
cache.set(parent, result);
return result;
}
let cacheAccessibleName: Map<Element, string> | undefined; let cacheAccessibleName: Map<Element, string> | undefined;
let cacheAccessibleNameHidden: Map<Element, string> | undefined; let cacheAccessibleNameHidden: Map<Element, string> | undefined;
let cacheAccessibleDescription: Map<Element, string> | undefined; let cacheAccessibleDescription: Map<Element, string> | undefined;
@ -1059,6 +1093,7 @@ let cacheAccessibleErrorMessage: Map<Element, string> | undefined;
let cacheIsHidden: Map<Element, boolean> | undefined; let cacheIsHidden: Map<Element, boolean> | undefined;
let cachePseudoContentBefore: Map<Element, string> | undefined; let cachePseudoContentBefore: Map<Element, string> | undefined;
let cachePseudoContentAfter: Map<Element, string> | undefined; let cachePseudoContentAfter: Map<Element, string> | undefined;
let cachePointerEvents: Map<Element, boolean> | undefined;
let cachesCounter = 0; let cachesCounter = 0;
export function beginAriaCaches() { export function beginAriaCaches() {
@ -1071,6 +1106,7 @@ export function beginAriaCaches() {
cacheIsHidden ??= new Map(); cacheIsHidden ??= new Map();
cachePseudoContentBefore ??= new Map(); cachePseudoContentBefore ??= new Map();
cachePseudoContentAfter ??= new Map(); cachePseudoContentAfter ??= new Map();
cachePointerEvents ??= new Map();
} }
export function endAriaCaches() { export function endAriaCaches() {
@ -1083,6 +1119,7 @@ export function endAriaCaches() {
cacheIsHidden = undefined; cacheIsHidden = undefined;
cachePseudoContentBefore = undefined; cachePseudoContentBefore = undefined;
cachePseudoContentAfter = undefined; cachePseudoContentAfter = undefined;
cachePointerEvents = undefined;
} }
} }

View File

@ -731,7 +731,7 @@ it('ref mode can be used to stitch all frame snapshots', async ({ page, server }
`.trim()); `.trim());
}); });
it('should not include hidden input elements', async ({ page }) => { it('should not generate refs for hidden elements', async ({ page }) => {
await page.setContent(` await page.setContent(`
<button>One</button> <button>One</button>
<button style="width: 0; height: 0; appearance: none; border: 0; padding: 0;">Two</button> <button style="width: 0; height: 0; appearance: none; border: 0; padding: 0;">Two</button>
@ -744,6 +744,37 @@ it('should not include hidden input elements', async ({ page }) => {
- button "Three" [ref=s1e5]`); - button "Three" [ref=s1e5]`);
}); });
it('should not generate refs for elements with pointer-events:none', async ({ page }) => {
await page.setContent(`
<button style="pointer-events: none">no-ref</button>
<div style="pointer-events: none">
<button style="pointer-events: auto">with-ref</button>
</div>
<div style="pointer-events: none">
<div style="pointer-events: initial">
<button>with-ref</button>
</div>
</div>
<div style="pointer-events: none">
<div style="pointer-events: auto">
<button>with-ref</button>
</div>
</div>
<div style="pointer-events: auto">
<div style="pointer-events: none">
<button>no-ref</button>
</div>
</div>
`);
const snapshot = await page.locator('body').ariaSnapshot({ ref: true });
expect(snapshot).toContain(`- button "no-ref"
- button "with-ref" [ref=s1e5]
- button "with-ref" [ref=s1e8]
- button "with-ref" [ref=s1e11]
- button "no-ref"`);
});
it('emit generic roles for nodes w/o roles', async ({ page }) => { it('emit generic roles for nodes w/o roles', async ({ page }) => {
await page.setContent(` await page.setContent(`
<style> <style>