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)[];
element: Element;
box: Box;
receivesPointerEvents: boolean;
props: Record<string, string>;
};
@ -43,7 +44,7 @@ export function generateAriaTree(rootElement: Element, generation: number, optio
const visited = new Set<Node>();
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>(),
generation,
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 {
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 role = roleUtils.getAriaRole(element) ?? defaultRole;
@ -156,7 +157,8 @@ function toAriaNode(element: Element, options?: { emitGeneric?: boolean }): Aria
return null;
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))
result.checked = roleUtils.getAriaChecked(element);
@ -406,7 +408,7 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, options?: { mode?: 'r
key += ` [pressed]`;
if (ariaNode.selected === true)
key += ` [selected]`;
if (options?.ref && ariaNode.box.visible) {
if (options?.ref && ariaNode.box.visible && ariaNode.receivesPointerEvents) {
const id = ariaSnapshot.ids.get(ariaNode.element);
if (id)
key += ` [ref=s${ariaSnapshot.generation}e${id}]`;

View File

@ -1051,6 +1051,40 @@ function getAccessibleNameFromAssociatedLabels(labels: Iterable<HTMLLabelElement
})).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 cacheAccessibleNameHidden: 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 cachePseudoContentBefore: Map<Element, string> | undefined;
let cachePseudoContentAfter: Map<Element, string> | undefined;
let cachePointerEvents: Map<Element, boolean> | undefined;
let cachesCounter = 0;
export function beginAriaCaches() {
@ -1071,6 +1106,7 @@ export function beginAriaCaches() {
cacheIsHidden ??= new Map();
cachePseudoContentBefore ??= new Map();
cachePseudoContentAfter ??= new Map();
cachePointerEvents ??= new Map();
}
export function endAriaCaches() {
@ -1083,6 +1119,7 @@ export function endAriaCaches() {
cacheIsHidden = undefined;
cachePseudoContentBefore = 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());
});
it('should not include hidden input elements', async ({ page }) => {
it('should not generate refs for hidden elements', async ({ page }) => {
await page.setContent(`
<button>One</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]`);
});
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 }) => {
await page.setContent(`
<style>