fix(role): fix native controls text alternative when aria-labelledby is present (#22800)
Fixes #22779.
This commit is contained in:
parent
4edd023644
commit
ca3629186c
|
|
@ -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') || '';
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue