fix(alert): use aria-labelledby and aria-describedby instead of aria-label (#25805)

Co-authored-by: Liam DeBeasi <liamdebeasi@users.noreply.github.com>
This commit is contained in:
Amanda Johnston
2022-08-23 15:31:00 -05:00
committed by GitHub
parent 65af865db7
commit 27318d75df
3 changed files with 64 additions and 35 deletions

View File

@ -578,20 +578,26 @@ export class Alert implements ComponentInterface, OverlayInterface {
} }
render() { render() {
const { overlayIndex, header, subHeader, htmlAttributes } = this; const { overlayIndex, header, subHeader, message, htmlAttributes } = this;
const mode = getIonMode(this); const mode = getIonMode(this);
const hdrId = `alert-${overlayIndex}-hdr`; const hdrId = `alert-${overlayIndex}-hdr`;
const subHdrId = `alert-${overlayIndex}-sub-hdr`; const subHdrId = `alert-${overlayIndex}-sub-hdr`;
const msgId = `alert-${overlayIndex}-msg`; const msgId = `alert-${overlayIndex}-msg`;
const role = this.inputs.length > 0 || this.buttons.length > 0 ? 'alertdialog' : 'alert'; const role = this.inputs.length > 0 || this.buttons.length > 0 ? 'alertdialog' : 'alert';
const defaultAriaLabel = header || subHeader || 'Alert';
/**
* If the header is defined, use that. Otherwise, fall back to the subHeader.
* If neither is defined, don't set aria-labelledby.
*/
const ariaLabelledBy = header ? hdrId : subHeader ? subHdrId : null;
return ( return (
<Host <Host
role={role} role={role}
aria-modal="true" aria-modal="true"
aria-labelledby={ariaLabelledBy}
aria-describedby={message ? msgId : null}
tabindex="-1" tabindex="-1"
aria-label={defaultAriaLabel}
{...(htmlAttributes as any)} {...(htmlAttributes as any)}
style={{ style={{
zIndex: `${20000 + overlayIndex}`, zIndex: `${20000 + overlayIndex}`,
@ -623,7 +629,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
)} )}
</div> </div>
<div id={msgId} class="alert-message" innerHTML={sanitizeDOMString(this.message)}></div> <div id={msgId} class="alert-message" innerHTML={sanitizeDOMString(message)}></div>
{this.renderAlertInputs()} {this.renderAlertInputs()}
{this.renderAlertButtons()} {this.renderAlertButtons()}

View File

@ -3,12 +3,29 @@ import { expect } from '@playwright/test';
import type { E2EPage } from '@utils/test/playwright'; import type { E2EPage } from '@utils/test/playwright';
import { test } from '@utils/test/playwright'; import { test } from '@utils/test/playwright';
const testAriaLabel = async (page: E2EPage, buttonID: string, expectedAriaLabel: string) => { const testAria = async (
page: E2EPage,
buttonID: string,
expectedAriaLabelledBy: string | null,
expectedAriaDescribedBy: string | null
) => {
const didPresent = await page.spyOnEvent('ionAlertDidPresent');
const button = page.locator(`#${buttonID}`); const button = page.locator(`#${buttonID}`);
await button.click(); await button.click();
await didPresent.next();
const alert = page.locator('ion-alert'); const alert = page.locator('ion-alert');
await expect(alert).toHaveAttribute('aria-label', expectedAriaLabel);
/**
* expect().toHaveAttribute() can't check for a null value, so grab and check
* the values manually instead.
*/
const ariaLabelledBy = await alert.getAttribute('aria-labelledby');
const ariaDescribedBy = await alert.getAttribute('aria-describedby');
expect(ariaLabelledBy).toBe(expectedAriaLabelledBy);
expect(ariaDescribedBy).toBe(expectedAriaDescribedBy);
}; };
test.describe('alert: a11y', () => { test.describe('alert: a11y', () => {
@ -17,27 +34,27 @@ test.describe('alert: a11y', () => {
await page.goto(`/src/components/alert/test/a11y`); await page.goto(`/src/components/alert/test/a11y`);
}); });
test('should not have accessibility violations', async ({ page }) => { test('should not have accessibility violations when header and message are defined', async ({ page }) => {
const button = page.locator('#customHeader'); const button = page.locator('#bothHeaders');
await button.click(); await button.click();
const results = await new AxeBuilder({ page }).analyze(); const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]); expect(results.violations).toEqual([]);
}); });
test('should have fallback aria-label when no header or subheader is specified', async ({ page }) => { test('should have aria-labelledby when header is set', async ({ page }) => {
await testAriaLabel(page, 'noHeader', 'Alert'); await testAria(page, 'noMessage', 'alert-1-hdr', null);
}); });
test('should inherit aria-label from header', async ({ page }) => { test('should have aria-describedby when message is set', async ({ page }) => {
await testAriaLabel(page, 'customHeader', 'Header'); await testAria(page, 'noHeaders', null, 'alert-1-msg');
}); });
test('should inherit aria-label from subheader if no header is specified', async ({ page }) => { test('should fall back to subHeader for aria-labelledby if header is not defined', async ({ page }) => {
await testAriaLabel(page, 'subHeaderOnly', 'Subtitle'); await testAria(page, 'subHeaderOnly', 'alert-1-sub-hdr', 'alert-1-msg');
}); });
test('should allow for manually specifying aria-label', async ({ page }) => { test('should allow for manually specifying aria attributes', async ({ page }) => {
await testAriaLabel(page, 'customAriaLabel', 'Custom alert'); await testAria(page, 'customAria', 'Custom title', 'Custom description');
}); });
}); });

View File

@ -19,14 +19,11 @@
<main class="ion-padding"> <main class="ion-padding">
<h1>Alert - A11y</h1> <h1>Alert - A11y</h1>
<ion-button id="noHeader" expand="block" onclick="presentNoHeader()">Alert With No Header</ion-button> <ion-button id="bothHeaders" expand="block" onclick="presentBothHeaders()">Both Headers</ion-button>
<ion-button id="customHeader" expand="block" onclick="presentCustomHeader()">Alert With Custom Header</ion-button> <ion-button id="subHeaderOnly" expand="block" onclick="presentSubHeaderOnly()">Subheader Only</ion-button>
<ion-button id="subHeaderOnly" expand="block" onclick="presentSubHeaderOnly()" <ion-button id="noHeaders" expand="block" onclick="presentNoHeaders()">No Headers</ion-button>
>Alert With Subheader Only</ion-button <ion-button id="noMessage" expand="block" onclick="presentNoMessage()">No Message</ion-button>
> <ion-button id="customAria" expand="block" onclick="presentCustomAria()">Custom Aria</ion-button>
<ion-button id="customAriaLabel" expand="block" onclick="presentCustomAriaLabel()"
>Alert With Custom Aria Label</ion-button
>
</main> </main>
<script> <script>
@ -35,14 +32,7 @@
await alert.present(); await alert.present();
} }
function presentNoHeader() { function presentBothHeaders() {
openAlert({
message: 'This is an alert message.',
buttons: ['OK'],
});
}
function presentCustomHeader() {
openAlert({ openAlert({
header: 'Header', header: 'Header',
subHeader: 'Subtitle', subHeader: 'Subtitle',
@ -59,14 +49,30 @@
}); });
} }
function presentCustomAriaLabel() { function presentNoHeaders() {
openAlert({
message: 'This is an alert message.',
buttons: ['OK'],
});
}
function presentNoMessage() {
openAlert({ openAlert({
header: 'Header', header: 'Header',
subHeader: 'Subtitle', subHeader: 'Subtitle',
message: 'This is an alert message with a custom aria-label.', buttons: ['OK'],
});
}
function presentCustomAria() {
openAlert({
header: 'Header',
subHeader: 'Subtitle',
message: 'This is an alert message with custom aria attributes.',
buttons: ['OK'], buttons: ['OK'],
htmlAttributes: { htmlAttributes: {
'aria-label': 'Custom alert', 'aria-labelledby': 'Custom title',
'aria-describedby': 'Custom description',
}, },
}); });
} }