Skip to content

Commit 27318d7

Browse files
fix(alert): use aria-labelledby and aria-describedby instead of aria-label (#25805)
Co-authored-by: Liam DeBeasi <liamdebeasi@users.noreply.github.com>
1 parent 65af865 commit 27318d7

File tree

3 files changed

+61
-32
lines changed

3 files changed

+61
-32
lines changed

core/src/components/alert/alert.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -578,20 +578,26 @@ export class Alert implements ComponentInterface, OverlayInterface {
578578
}
579579

580580
render() {
581-
const { overlayIndex, header, subHeader, htmlAttributes } = this;
581+
const { overlayIndex, header, subHeader, message, htmlAttributes } = this;
582582
const mode = getIonMode(this);
583583
const hdrId = `alert-${overlayIndex}-hdr`;
584584
const subHdrId = `alert-${overlayIndex}-sub-hdr`;
585585
const msgId = `alert-${overlayIndex}-msg`;
586586
const role = this.inputs.length > 0 || this.buttons.length > 0 ? 'alertdialog' : 'alert';
587-
const defaultAriaLabel = header || subHeader || 'Alert';
587+
588+
/**
589+
* If the header is defined, use that. Otherwise, fall back to the subHeader.
590+
* If neither is defined, don't set aria-labelledby.
591+
*/
592+
const ariaLabelledBy = header ? hdrId : subHeader ? subHdrId : null;
588593

589594
return (
590595
<Host
591596
role={role}
592597
aria-modal="true"
598+
aria-labelledby={ariaLabelledBy}
599+
aria-describedby={message ? msgId : null}
593600
tabindex="-1"
594-
aria-label={defaultAriaLabel}
595601
{...(htmlAttributes as any)}
596602
style={{
597603
zIndex: `${20000 + overlayIndex}`,
@@ -623,7 +629,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
623629
)}
624630
</div>
625631

626-
<div id={msgId} class="alert-message" innerHTML={sanitizeDOMString(this.message)}></div>
632+
<div id={msgId} class="alert-message" innerHTML={sanitizeDOMString(message)}></div>
627633

628634
{this.renderAlertInputs()}
629635
{this.renderAlertButtons()}

core/src/components/alert/test/a11y/alert.e2e.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,29 @@ import { expect } from '@playwright/test';
33
import type { E2EPage } from '@utils/test/playwright';
44
import { test } from '@utils/test/playwright';
55

6-
const testAriaLabel = async (page: E2EPage, buttonID: string, expectedAriaLabel: string) => {
6+
const testAria = async (
7+
page: E2EPage,
8+
buttonID: string,
9+
expectedAriaLabelledBy: string | null,
10+
expectedAriaDescribedBy: string | null
11+
) => {
12+
const didPresent = await page.spyOnEvent('ionAlertDidPresent');
713
const button = page.locator(`#${buttonID}`);
14+
815
await button.click();
16+
await didPresent.next();
917

1018
const alert = page.locator('ion-alert');
11-
await expect(alert).toHaveAttribute('aria-label', expectedAriaLabel);
19+
20+
/**
21+
* expect().toHaveAttribute() can't check for a null value, so grab and check
22+
* the values manually instead.
23+
*/
24+
const ariaLabelledBy = await alert.getAttribute('aria-labelledby');
25+
const ariaDescribedBy = await alert.getAttribute('aria-describedby');
26+
27+
expect(ariaLabelledBy).toBe(expectedAriaLabelledBy);
28+
expect(ariaDescribedBy).toBe(expectedAriaDescribedBy);
1229
};
1330

1431
test.describe('alert: a11y', () => {
@@ -17,27 +34,27 @@ test.describe('alert: a11y', () => {
1734
await page.goto(`/src/components/alert/test/a11y`);
1835
});
1936

20-
test('should not have accessibility violations', async ({ page }) => {
21-
const button = page.locator('#customHeader');
37+
test('should not have accessibility violations when header and message are defined', async ({ page }) => {
38+
const button = page.locator('#bothHeaders');
2239
await button.click();
2340

2441
const results = await new AxeBuilder({ page }).analyze();
2542
expect(results.violations).toEqual([]);
2643
});
2744

28-
test('should have fallback aria-label when no header or subheader is specified', async ({ page }) => {
29-
await testAriaLabel(page, 'noHeader', 'Alert');
45+
test('should have aria-labelledby when header is set', async ({ page }) => {
46+
await testAria(page, 'noMessage', 'alert-1-hdr', null);
3047
});
3148

32-
test('should inherit aria-label from header', async ({ page }) => {
33-
await testAriaLabel(page, 'customHeader', 'Header');
49+
test('should have aria-describedby when message is set', async ({ page }) => {
50+
await testAria(page, 'noHeaders', null, 'alert-1-msg');
3451
});
3552

36-
test('should inherit aria-label from subheader if no header is specified', async ({ page }) => {
37-
await testAriaLabel(page, 'subHeaderOnly', 'Subtitle');
53+
test('should fall back to subHeader for aria-labelledby if header is not defined', async ({ page }) => {
54+
await testAria(page, 'subHeaderOnly', 'alert-1-sub-hdr', 'alert-1-msg');
3855
});
3956

40-
test('should allow for manually specifying aria-label', async ({ page }) => {
41-
await testAriaLabel(page, 'customAriaLabel', 'Custom alert');
57+
test('should allow for manually specifying aria attributes', async ({ page }) => {
58+
await testAria(page, 'customAria', 'Custom title', 'Custom description');
4259
});
4360
});

core/src/components/alert/test/a11y/index.html

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,11 @@
1919
<main class="ion-padding">
2020
<h1>Alert - A11y</h1>
2121

22-
<ion-button id="noHeader" expand="block" onclick="presentNoHeader()">Alert With No Header</ion-button>
23-
<ion-button id="customHeader" expand="block" onclick="presentCustomHeader()">Alert With Custom Header</ion-button>
24-
<ion-button id="subHeaderOnly" expand="block" onclick="presentSubHeaderOnly()"
25-
>Alert With Subheader Only</ion-button
26-
>
27-
<ion-button id="customAriaLabel" expand="block" onclick="presentCustomAriaLabel()"
28-
>Alert With Custom Aria Label</ion-button
29-
>
22+
<ion-button id="bothHeaders" expand="block" onclick="presentBothHeaders()">Both Headers</ion-button>
23+
<ion-button id="subHeaderOnly" expand="block" onclick="presentSubHeaderOnly()">Subheader Only</ion-button>
24+
<ion-button id="noHeaders" expand="block" onclick="presentNoHeaders()">No Headers</ion-button>
25+
<ion-button id="noMessage" expand="block" onclick="presentNoMessage()">No Message</ion-button>
26+
<ion-button id="customAria" expand="block" onclick="presentCustomAria()">Custom Aria</ion-button>
3027
</main>
3128

3229
<script>
@@ -35,38 +32,47 @@ <h1>Alert - A11y</h1>
3532
await alert.present();
3633
}
3734

38-
function presentNoHeader() {
35+
function presentBothHeaders() {
3936
openAlert({
37+
header: 'Header',
38+
subHeader: 'Subtitle',
4039
message: 'This is an alert message.',
4140
buttons: ['OK'],
4241
});
4342
}
4443

45-
function presentCustomHeader() {
44+
function presentSubHeaderOnly() {
4645
openAlert({
47-
header: 'Header',
4846
subHeader: 'Subtitle',
4947
message: 'This is an alert message.',
5048
buttons: ['OK'],
5149
});
5250
}
5351

54-
function presentSubHeaderOnly() {
52+
function presentNoHeaders() {
5553
openAlert({
56-
subHeader: 'Subtitle',
5754
message: 'This is an alert message.',
5855
buttons: ['OK'],
5956
});
6057
}
6158

62-
function presentCustomAriaLabel() {
59+
function presentNoMessage() {
60+
openAlert({
61+
header: 'Header',
62+
subHeader: 'Subtitle',
63+
buttons: ['OK'],
64+
});
65+
}
66+
67+
function presentCustomAria() {
6368
openAlert({
6469
header: 'Header',
6570
subHeader: 'Subtitle',
66-
message: 'This is an alert message with a custom aria-label.',
71+
message: 'This is an alert message with custom aria attributes.',
6772
buttons: ['OK'],
6873
htmlAttributes: {
69-
'aria-label': 'Custom alert',
74+
'aria-labelledby': 'Custom title',
75+
'aria-describedby': 'Custom description',
7076
},
7177
});
7278
}

0 commit comments

Comments
 (0)