fix(role): compute <output> accessible name from labels (#27415)

Fixes: https://github.com/microsoft/playwright/issues/27403
This commit is contained in:
Pavel Feldman 2023-10-03 13:01:13 -07:00 committed by GitHub
parent 13cca1db3d
commit ae08d03d75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 30 additions and 27 deletions

View File

@ -508,15 +508,8 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
if (element.tagName === 'INPUT' && (element as HTMLInputElement).type === 'image') {
options.visitedElements.add(element);
const labels = (element as HTMLInputElement).labels || [];
if (labels.length && options.embeddedInLabelledBy === 'none') {
return [...labels].map(label => getElementAccessibleNameInternal(label, {
...options,
embeddedInLabel: 'self',
embeddedInTextAlternativeElement: false,
embeddedInLabelledBy: 'none',
embeddedInTargetElement: 'none',
})).filter(accessibleName => !!accessibleName).join(' ');
}
if (labels.length && options.embeddedInLabelledBy === 'none')
return getAccessibleNameFromAssociatedLabels(labels, options);
const alt = element.getAttribute('alt') || '';
if (alt.trim())
return alt;
@ -532,18 +525,20 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
if (!labelledBy && element.tagName === 'BUTTON') {
options.visitedElements.add(element);
const labels = (element as HTMLButtonElement).labels || [];
if (labels.length) {
return [...labels].map(label => getElementAccessibleNameInternal(label, {
...options,
embeddedInLabel: 'self',
embeddedInTextAlternativeElement: false,
embeddedInLabelledBy: 'none',
embeddedInTargetElement: 'none',
})).filter(accessibleName => !!accessibleName).join(' ');
}
if (labels.length)
return getAccessibleNameFromAssociatedLabels(labels, options);
// From here, fallthrough to step 2f.
}
// https://w3c.github.io/html-aam/#output-element-accessible-name-computation
if (!labelledBy && element.tagName === 'OUTPUT') {
options.visitedElements.add(element);
const labels = (element as HTMLOutputElement).labels || [];
if (labels.length)
return getAccessibleNameFromAssociatedLabels(labels, options);
return element.getAttribute('title') || '';
}
// https://w3c.github.io/html-aam/#input-type-text-input-type-password-input-type-number-input-type-search-input-type-tel-input-type-email-input-type-url-and-textarea-element-accessible-name-computation
// https://w3c.github.io/html-aam/#other-form-elements-accessible-name-computation
// For "other form elements", we count select and any other input.
@ -552,15 +547,8 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
if (!labelledBy && (element.tagName === 'TEXTAREA' || element.tagName === 'SELECT' || element.tagName === 'INPUT')) {
options.visitedElements.add(element);
const labels = (element as (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)).labels || [];
if (labels.length) {
return [...labels].map(label => getElementAccessibleNameInternal(label, {
...options,
embeddedInLabel: 'self',
embeddedInTextAlternativeElement: false,
embeddedInLabelledBy: 'none',
embeddedInTargetElement: 'none',
})).filter(accessibleName => !!accessibleName).join(' ');
}
if (labels.length)
return getAccessibleNameFromAssociatedLabels(labels, options);
const usePlaceholder = (element.tagName === 'INPUT' && ['text', 'password', 'search', 'tel', 'email', 'url'].includes((element as HTMLInputElement).type)) || element.tagName === 'TEXTAREA';
const placeholder = element.getAttribute('placeholder') || '';
@ -836,6 +824,16 @@ function hasExplicitAriaDisabled(element: Element | undefined): boolean {
return hasExplicitAriaDisabled(parentElementOrShadowHost(element));
}
function getAccessibleNameFromAssociatedLabels(labels: Iterable<HTMLLabelElement>, options: AccessibleNameOptions) {
return [...labels].map(label => getElementAccessibleNameInternal(label, {
...options,
embeddedInLabel: 'self',
embeddedInTextAlternativeElement: false,
embeddedInLabelledBy: 'none',
embeddedInTargetElement: 'none',
})).filter(accessibleName => !!accessibleName).join(' ');
}
let cacheAccessibleName: Map<Element, string> | undefined;
let cacheAccessibleNameHidden: Map<Element, string> | undefined;
let cacheIsHidden: Map<Element, boolean> | undefined;

View File

@ -479,3 +479,8 @@ test('hidden with shadow dom slots', async ({ page }) => {
`<button>visible2</button>`,
]);
});
test('should support output accessible name', async ({ page }) => {
await page.setContent(`<label>Output1<output>output</output></label>`);
await expect(page.getByRole('status', { name: 'Output1' })).toBeVisible();
});