fix(role): compute <output> accessible name from labels (#27415)
Fixes: https://github.com/microsoft/playwright/issues/27403
This commit is contained in:
parent
13cca1db3d
commit
ae08d03d75
|
|
@ -508,15 +508,8 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
|
||||||
if (element.tagName === 'INPUT' && (element as HTMLInputElement).type === 'image') {
|
if (element.tagName === 'INPUT' && (element as HTMLInputElement).type === 'image') {
|
||||||
options.visitedElements.add(element);
|
options.visitedElements.add(element);
|
||||||
const labels = (element as HTMLInputElement).labels || [];
|
const labels = (element as HTMLInputElement).labels || [];
|
||||||
if (labels.length && options.embeddedInLabelledBy === 'none') {
|
if (labels.length && options.embeddedInLabelledBy === 'none')
|
||||||
return [...labels].map(label => getElementAccessibleNameInternal(label, {
|
return getAccessibleNameFromAssociatedLabels(labels, options);
|
||||||
...options,
|
|
||||||
embeddedInLabel: 'self',
|
|
||||||
embeddedInTextAlternativeElement: false,
|
|
||||||
embeddedInLabelledBy: 'none',
|
|
||||||
embeddedInTargetElement: 'none',
|
|
||||||
})).filter(accessibleName => !!accessibleName).join(' ');
|
|
||||||
}
|
|
||||||
const alt = element.getAttribute('alt') || '';
|
const alt = element.getAttribute('alt') || '';
|
||||||
if (alt.trim())
|
if (alt.trim())
|
||||||
return alt;
|
return alt;
|
||||||
|
|
@ -532,18 +525,20 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
|
||||||
if (!labelledBy && element.tagName === 'BUTTON') {
|
if (!labelledBy && element.tagName === 'BUTTON') {
|
||||||
options.visitedElements.add(element);
|
options.visitedElements.add(element);
|
||||||
const labels = (element as HTMLButtonElement).labels || [];
|
const labels = (element as HTMLButtonElement).labels || [];
|
||||||
if (labels.length) {
|
if (labels.length)
|
||||||
return [...labels].map(label => getElementAccessibleNameInternal(label, {
|
return getAccessibleNameFromAssociatedLabels(labels, options);
|
||||||
...options,
|
|
||||||
embeddedInLabel: 'self',
|
|
||||||
embeddedInTextAlternativeElement: false,
|
|
||||||
embeddedInLabelledBy: 'none',
|
|
||||||
embeddedInTargetElement: 'none',
|
|
||||||
})).filter(accessibleName => !!accessibleName).join(' ');
|
|
||||||
}
|
|
||||||
// From here, fallthrough to step 2f.
|
// 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/#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
|
// https://w3c.github.io/html-aam/#other-form-elements-accessible-name-computation
|
||||||
// For "other form elements", we count select and any other input.
|
// 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')) {
|
if (!labelledBy && (element.tagName === 'TEXTAREA' || element.tagName === 'SELECT' || element.tagName === 'INPUT')) {
|
||||||
options.visitedElements.add(element);
|
options.visitedElements.add(element);
|
||||||
const labels = (element as (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)).labels || [];
|
const labels = (element as (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)).labels || [];
|
||||||
if (labels.length) {
|
if (labels.length)
|
||||||
return [...labels].map(label => getElementAccessibleNameInternal(label, {
|
return getAccessibleNameFromAssociatedLabels(labels, options);
|
||||||
...options,
|
|
||||||
embeddedInLabel: 'self',
|
|
||||||
embeddedInTextAlternativeElement: false,
|
|
||||||
embeddedInLabelledBy: 'none',
|
|
||||||
embeddedInTargetElement: 'none',
|
|
||||||
})).filter(accessibleName => !!accessibleName).join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
const usePlaceholder = (element.tagName === 'INPUT' && ['text', 'password', 'search', 'tel', 'email', 'url'].includes((element as HTMLInputElement).type)) || element.tagName === 'TEXTAREA';
|
const usePlaceholder = (element.tagName === 'INPUT' && ['text', 'password', 'search', 'tel', 'email', 'url'].includes((element as HTMLInputElement).type)) || element.tagName === 'TEXTAREA';
|
||||||
const placeholder = element.getAttribute('placeholder') || '';
|
const placeholder = element.getAttribute('placeholder') || '';
|
||||||
|
|
@ -836,6 +824,16 @@ function hasExplicitAriaDisabled(element: Element | undefined): boolean {
|
||||||
return hasExplicitAriaDisabled(parentElementOrShadowHost(element));
|
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 cacheAccessibleName: Map<Element, string> | undefined;
|
||||||
let cacheAccessibleNameHidden: Map<Element, string> | undefined;
|
let cacheAccessibleNameHidden: Map<Element, string> | undefined;
|
||||||
let cacheIsHidden: Map<Element, boolean> | undefined;
|
let cacheIsHidden: Map<Element, boolean> | undefined;
|
||||||
|
|
|
||||||
|
|
@ -479,3 +479,8 @@ test('hidden with shadow dom slots', async ({ page }) => {
|
||||||
`<button>visible2</button>`,
|
`<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();
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue