fix(role): use <title> for elements inside svg (#22043)
Follows svg-aam mapping: https://www.w3.org/TR/svg-aam-1.0/#mapping_additional_nd Fixes #21486.
This commit is contained in:
parent
1ba07bcd81
commit
6a2b4ed142
|
|
@ -129,6 +129,11 @@ const kImplicitRoleByTagName: { [tagName: string]: (e: Element) => string | null
|
|||
'STRONG': () => 'strong',
|
||||
'SUB': () => 'subscript',
|
||||
'SUP': () => 'superscript',
|
||||
// For <svg> we default to Chrome behavior:
|
||||
// - Chrome reports 'img'.
|
||||
// - Firefox reports 'diagram' that is not in official ARIA spec yet.
|
||||
// - Safari reports 'no role', but still computes accessible name.
|
||||
'SVG': () => 'img',
|
||||
'TABLE': () => 'table',
|
||||
'TBODY': () => 'rowgroup',
|
||||
'TD': (e: Element) => {
|
||||
|
|
@ -167,7 +172,8 @@ const kPresentationInheritanceParents: { [tagName: string]: string[] } = {
|
|||
};
|
||||
|
||||
function getImplicitAriaRole(element: Element): string | null {
|
||||
const implicitRole = kImplicitRoleByTagName[element.tagName]?.(element) || '';
|
||||
// Elements from the svg namespace do not have uppercase tagName.
|
||||
const implicitRole = kImplicitRoleByTagName[element.tagName.toUpperCase()]?.(element) || '';
|
||||
if (!implicitRole)
|
||||
return null;
|
||||
// Inherit presentation role when required.
|
||||
|
|
@ -578,18 +584,25 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
|
|||
return title;
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/svg-aam-1.0/
|
||||
if (element.tagName === 'SVG' && (element as SVGElement).ownerSVGElement) {
|
||||
// https://www.w3.org/TR/svg-aam-1.0/#mapping_additional_nd
|
||||
if (element.tagName.toUpperCase() === 'SVG' || (element as SVGElement).ownerSVGElement) {
|
||||
options.visitedElements.add(element);
|
||||
for (let child = element.firstElementChild; child; child = child.nextElementSibling) {
|
||||
if (child.tagName === 'TITLE' && (element as SVGElement).ownerSVGElement) {
|
||||
if (child.tagName.toUpperCase() === 'TITLE' && (child as SVGElement).ownerSVGElement) {
|
||||
return getElementAccessibleNameInternal(child, {
|
||||
...childOptions,
|
||||
embeddedInTextAlternativeElement: true,
|
||||
embeddedInLabelledBy: 'self',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((element as SVGElement).ownerSVGElement && element.tagName.toUpperCase() === 'A') {
|
||||
const title = element.getAttribute('xlink:title') || '';
|
||||
if (title.trim()) {
|
||||
options.visitedElements.add(element);
|
||||
return title;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// step 2f + step 2h.
|
||||
|
|
|
|||
|
|
@ -15,10 +15,19 @@
|
|||
*/
|
||||
|
||||
import { contextTest as test, expect } from '../config/browserTest';
|
||||
import type { Page } from 'playwright-core';
|
||||
import fs from 'fs';
|
||||
|
||||
test.skip(({ mode }) => mode !== 'default');
|
||||
|
||||
async function getNameAndRole(page: Page, selector: string) {
|
||||
return await page.$eval(selector, e => {
|
||||
const name = (window as any).__injectedScript.getElementAccessibleName(e);
|
||||
const role = (window as any).__injectedScript.getAriaRole(e);
|
||||
return { name, role };
|
||||
});
|
||||
}
|
||||
|
||||
const ranges = [
|
||||
'name_test_case_539-manual.html',
|
||||
'name_test_case_721-manual.html',
|
||||
|
|
@ -159,8 +168,7 @@ test('accessible name with slots', async ({ page }) => {
|
|||
})();
|
||||
</script>
|
||||
`);
|
||||
const name1 = await page.$eval('button', e => (window as any).__injectedScript.getElementAccessibleName(e));
|
||||
expect.soft(name1).toBe('foo');
|
||||
expect.soft(await getNameAndRole(page, 'button')).toEqual({ role: 'button', name: 'foo' });
|
||||
|
||||
// Text "foo" is assigned to the slot, should be used instead of slot content.
|
||||
await page.setContent(`
|
||||
|
|
@ -179,8 +187,7 @@ test('accessible name with slots', async ({ page }) => {
|
|||
})();
|
||||
</script>
|
||||
`);
|
||||
const name2 = await page.$eval('button', e => (window as any).__injectedScript.getElementAccessibleName(e));
|
||||
expect.soft(name2).toBe('foo');
|
||||
expect.soft(await getNameAndRole(page, 'button')).toEqual({ role: 'button', name: 'foo' });
|
||||
|
||||
// Nothing is assigned to the slot, should use slot content.
|
||||
await page.setContent(`
|
||||
|
|
@ -199,8 +206,7 @@ test('accessible name with slots', async ({ page }) => {
|
|||
})();
|
||||
</script>
|
||||
`);
|
||||
const name3 = await page.$eval('button', e => (window as any).__injectedScript.getElementAccessibleName(e));
|
||||
expect.soft(name3).toBe('pre');
|
||||
expect.soft(await getNameAndRole(page, 'button')).toEqual({ role: 'button', name: 'pre' });
|
||||
});
|
||||
|
||||
test('accessible name nested treeitem', async ({ page }) => {
|
||||
|
|
@ -213,8 +219,25 @@ test('accessible name nested treeitem', async ({ page }) => {
|
|||
</div>
|
||||
</div>
|
||||
`);
|
||||
const name = await page.$eval('#target', e => (window as any).__injectedScript.getElementAccessibleName(e));
|
||||
expect.soft(name).toBe('Top-level');
|
||||
expect.soft(await getNameAndRole(page, '#target')).toEqual({ role: 'treeitem', name: 'Top-level' });
|
||||
});
|
||||
|
||||
test('svg title', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<div>
|
||||
<svg width="162" height="30" viewBox="0 0 162 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Submit</title>
|
||||
<g>
|
||||
<title>Hello</title>
|
||||
</g>
|
||||
<a href="example.com" xlink:title="a link"><circle cx="50" cy="40" r="35" /></a>
|
||||
</svg>
|
||||
</div>
|
||||
`);
|
||||
|
||||
expect.soft(await getNameAndRole(page, 'svg')).toEqual({ role: 'img', name: 'Submit' });
|
||||
expect.soft(await getNameAndRole(page, 'g')).toEqual({ role: null, name: 'Hello' });
|
||||
expect.soft(await getNameAndRole(page, 'a')).toEqual({ role: 'link', name: 'a link' });
|
||||
});
|
||||
|
||||
function toArray(x: any): any[] {
|
||||
|
|
|
|||
Loading…
Reference in a new issue