fix(role): fix native controls text alternative when aria-labelledby is present (#22800)

Fixes #22779.
This commit is contained in:
Dmitry Gozman 2023-05-03 16:09:08 -07:00 committed by GitHub
parent 4edd023644
commit ca3629186c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 58 additions and 14 deletions

View file

@ -398,10 +398,11 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
return '';
}
const labelledBy = getAriaLabelledByElements(element);
// step 2b.
if (options.embeddedInLabelledBy === 'none') {
const refs = getAriaLabelledByElements(element) || [];
const accessibleName = refs.map(ref => getElementAccessibleNameInternal(ref, {
const accessibleName = (labelledBy || []).map(ref => getElementAccessibleNameInternal(ref, {
...options,
embeddedInLabelledBy: 'self',
embeddedInTargetElement: 'none',
@ -417,7 +418,7 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
// step 2c.
if (options.embeddedInLabel !== 'none' || options.embeddedInLabelledBy !== 'none') {
const isOwnLabel = [...(element as (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)).labels || []].includes(element as any);
const isOwnLabelledBy = getIdRefs(element, element.getAttribute('aria-labelledby')).includes(element);
const isOwnLabelledBy = (labelledBy || []).includes(element);
if (!isOwnLabel && !isOwnLabelledBy) {
if (role === 'textbox') {
options.visitedElements.add(element);
@ -464,6 +465,11 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
// step 2e.
if (!['presentation', 'none'].includes(role)) {
// https://w3c.github.io/html-aam/#input-type-button-input-type-submit-and-input-type-reset-accessible-name-computation
//
// SPEC DIFFERENCE.
// Spec says to ignore this when aria-labelledby is defined.
// WebKit follows the spec, while Chromium and Firefox do not.
// We align with Chromium and Firefox here.
if (element.tagName === 'INPUT' && ['button', 'submit', 'reset'].includes((element as HTMLInputElement).type)) {
options.visitedElements.add(element);
const value = (element as HTMLInputElement).value || '';
@ -478,16 +484,13 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
}
// https://w3c.github.io/html-aam/#input-type-image-accessible-name-computation
//
// SPEC DIFFERENCE.
// Spec says to ignore this when aria-labelledby is defined, but all browsers take it into account.
if (element.tagName === 'INPUT' && (element as HTMLInputElement).type === 'image') {
options.visitedElements.add(element);
const alt = element.getAttribute('alt') || '';
if (alt.trim())
return alt;
// SPEC DIFFERENCE.
// Spec does not mention "label" elements, but we account for labels
// to pass "name_test_case_616-manual.html"
const labels = (element as HTMLInputElement).labels || [];
if (labels.length) {
if (labels.length && options.embeddedInLabelledBy === 'none') {
return [...labels].map(label => getElementAccessibleNameInternal(label, {
...options,
embeddedInLabel: 'self',
@ -496,6 +499,9 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
embeddedInTargetElement: 'none',
})).filter(accessibleName => !!accessibleName).join(' ');
}
const alt = element.getAttribute('alt') || '';
if (alt.trim())
return alt;
const title = element.getAttribute('title') || '';
if (title.trim())
return title;
@ -505,7 +511,7 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
}
// https://w3c.github.io/html-aam/#button-element-accessible-name-computation
if (element.tagName === 'BUTTON') {
if (!labelledBy && element.tagName === 'BUTTON') {
options.visitedElements.add(element);
const labels = (element as HTMLButtonElement).labels || [];
if (labels.length) {
@ -523,7 +529,9 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
// 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.
if (element.tagName === 'TEXTAREA' || element.tagName === 'SELECT' || element.tagName === 'INPUT') {
//
// Note: WebKit does not follow the spec and uses placeholder when aria-labelledby is present.
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) {
@ -545,7 +553,7 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
}
// https://w3c.github.io/html-aam/#fieldset-and-legend-elements
if (element.tagName === 'FIELDSET') {
if (!labelledBy && element.tagName === 'FIELDSET') {
options.visitedElements.add(element);
for (let child = element.firstElementChild; child; child = child.nextElementSibling) {
if (child.tagName === 'LEGEND') {
@ -560,7 +568,7 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
}
// https://w3c.github.io/html-aam/#figure-and-figcaption-elements
if (element.tagName === 'FIGURE') {
if (!labelledBy && element.tagName === 'FIGURE') {
options.visitedElements.add(element);
for (let child = element.firstElementChild; child; child = child.nextElementSibling) {
if (child.tagName === 'FIGCAPTION') {
@ -575,6 +583,9 @@ function getElementAccessibleNameInternal(element: Element, options: AccessibleN
}
// https://w3c.github.io/html-aam/#img-element
//
// SPEC DIFFERENCE.
// Spec says to ignore this when aria-labelledby is defined, but all browsers take it into account.
if (element.tagName === 'IMG') {
options.visitedElements.add(element);
const alt = element.getAttribute('alt') || '';

View file

@ -248,6 +248,7 @@ test('native controls', async ({ page }) => {
<label for="image1">IMAGE1</label><input id="image1" type=image>
<input id="image2" type=image alt="IMAGE2">
<label for="image3">IMAGE3</label><input id="image3" type=image alt="MORE3">
<label for="button1">BUTTON1</label><button id="button1" role="combobox">button</button>
<button id="button2" role="combobox">BUTTON2</button>
@ -260,12 +261,44 @@ test('native controls', async ({ page }) => {
expect.soft(await getNameAndRole(page, '#text3')).toEqual({ role: 'textbox', name: 'TEXT3' });
expect.soft(await getNameAndRole(page, '#image1')).toEqual({ role: 'button', name: 'IMAGE1' });
expect.soft(await getNameAndRole(page, '#image2')).toEqual({ role: 'button', name: 'IMAGE2' });
expect.soft(await getNameAndRole(page, '#image3')).toEqual({ role: 'button', name: 'IMAGE3' });
expect.soft(await getNameAndRole(page, '#button1')).toEqual({ role: 'combobox', name: 'BUTTON1' });
expect.soft(await getNameAndRole(page, '#button2')).toEqual({ role: 'combobox', name: '' });
expect.soft(await getNameAndRole(page, '#button3')).toEqual({ role: 'button', name: 'BUTTON3' });
expect.soft(await getNameAndRole(page, '#button4')).toEqual({ role: 'button', name: 'BUTTON4' });
});
test('native controls labelled-by', async ({ page }) => {
await page.setContent(`
<label id="for-text1">TEXT1</label><input aria-labelledby="for-text1" id="text1" type=text>
<label id="for-text2">TEXT2</label><input aria-labelledby="for-text2 text2" id="text2" type=text>
<label id="for-text3" for="text3">TEXT3</label><input aria-labelledby="for-text3 text3" id="text3" type=text>
<label id="for-submit1" for="submit1">SUBMIT1</label><input aria-labelledby="for-submit1 submit1" id="submit1" type=submit>
<label id="for-image1" for="image1">IMAGE1</label><input aria-labelledby="for-image1 image1" id="image1" type=image alt="MORE1">
<label id="for-image2" for="image2">IMAGE2</label><img aria-labelledby="for-image2 image2" id="image2" alt="MORE2" src="data:image/svg,<g></g>">
<label id="for-button1">BUTTON1</label><button aria-labelledby="for-button1" id="button1">MORE1</button>
<label id="for-button2">BUTTON2</label><button aria-labelledby="for-button2 button2" id="button2">MORE2</button>
<label id="for-button3" for="button3">BUTTON3</label><button aria-labelledby="for-button3 button3" id="button3">MORE3</button>
<label id="for-button4" for="button4">BUTTON4</label><button aria-labelledby="for-button4" id="button4">MORE4</button>
<label id="for-textarea1" for="textarea1">TEXTAREA1</label><textarea aria-labelledby="for-textarea1 textarea1" id="textarea1" placeholder="MORE1">MORE2</textarea>
`);
expect.soft(await getNameAndRole(page, '#text1')).toEqual({ role: 'textbox', name: 'TEXT1' });
expect.soft(await getNameAndRole(page, '#text2')).toEqual({ role: 'textbox', name: 'TEXT2' });
expect.soft(await getNameAndRole(page, '#text3')).toEqual({ role: 'textbox', name: 'TEXT3' });
expect.soft(await getNameAndRole(page, '#submit1')).toEqual({ role: 'button', name: 'SUBMIT1 Submit' });
expect.soft(await getNameAndRole(page, '#image1')).toEqual({ role: 'button', name: 'IMAGE1 MORE1' });
expect.soft(await getNameAndRole(page, '#image2')).toEqual({ role: 'img', name: 'IMAGE2 MORE2' });
expect.soft(await getNameAndRole(page, '#button1')).toEqual({ role: 'button', name: 'BUTTON1' });
expect.soft(await getNameAndRole(page, '#button2')).toEqual({ role: 'button', name: 'BUTTON2 MORE2' });
expect.soft(await getNameAndRole(page, '#button3')).toEqual({ role: 'button', name: 'BUTTON3 MORE3' });
expect.soft(await getNameAndRole(page, '#button4')).toEqual({ role: 'button', name: 'BUTTON4' });
expect.soft(await getNameAndRole(page, '#textarea1')).toEqual({ role: 'textbox', name: 'TEXTAREA1 MORE2' });
});
function toArray(x: any): any[] {
return Array.isArray(x) ? x : [x];
}