Skip to content

Commit 7ab14b8

Browse files
fix(alert): use correct heading structure for subHeader when header exists (#29964)
- The `header` will continue to always render as an `<h2>` element. - If there is no `header` defined, the `subHeader` will continue to render as an `<h2>` element. - If there is a `header` defined, the `subHeader` will render as an `<h3>` element. - Updates `ariaLabelledBy` to include both `header` and `subHeader` ids when both are defined. - Updates the `a11y` e2e test to use new values & check for tag names.
1 parent 12ff29c commit 7ab14b8

File tree

3 files changed

+59
-14
lines changed

3 files changed

+59
-14
lines changed

core/src/components/alert/alert.tsx

+13-4
Original file line numberDiff line numberDiff line change
@@ -730,10 +730,12 @@ export class Alert implements ComponentInterface, OverlayInterface {
730730
const role = this.inputs.length > 0 || this.buttons.length > 0 ? 'alertdialog' : 'alert';
731731

732732
/**
733-
* If the header is defined, use that. Otherwise, fall back to the subHeader.
734-
* If neither is defined, don't set aria-labelledby.
733+
* Use both the header and subHeader ids if they are defined.
734+
* If only the header is defined, use the header id.
735+
* If only the subHeader is defined, use the subHeader id.
736+
* If neither are defined, do not set aria-labelledby.
735737
*/
736-
const ariaLabelledBy = header ? hdrId : subHeader ? subHdrId : null;
738+
const ariaLabelledBy = header && subHeader ? `${hdrId} ${subHdrId}` : header ? hdrId : subHeader ? subHdrId : null;
737739

738740
return (
739741
<Host
@@ -766,11 +768,18 @@ export class Alert implements ComponentInterface, OverlayInterface {
766768
{header}
767769
</h2>
768770
)}
769-
{subHeader && (
771+
{/* If no header exists, subHeader should be the highest heading level, h2 */}
772+
{subHeader && !header && (
770773
<h2 id={subHdrId} class="alert-sub-title">
771774
{subHeader}
772775
</h2>
773776
)}
777+
{/* If a header exists, subHeader should be one level below it, h3 */}
778+
{subHeader && header && (
779+
<h3 id={subHdrId} class="alert-sub-title">
780+
{subHeader}
781+
</h3>
782+
)}
774783
</div>
775784

776785
{this.renderAlertMessage(msgId)}

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

+33-4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,27 @@ const testAria = async (
1717

1818
const alert = page.locator('ion-alert');
1919

20+
const header = alert.locator('.alert-title');
21+
const subHeader = alert.locator('.alert-sub-title');
22+
23+
// If a header exists, it should be an h2 element
24+
if ((await header.count()) > 0) {
25+
const headerTagName = await header.evaluate((el) => el.tagName);
26+
expect(headerTagName).toBe('H2');
27+
}
28+
29+
// If a header and subHeader exist, the subHeader should be an h3 element
30+
if ((await header.count()) > 0 && (await subHeader.count()) > 0) {
31+
const subHeaderTagName = await subHeader.evaluate((el) => el.tagName);
32+
expect(subHeaderTagName).toBe('H3');
33+
}
34+
35+
// If a subHeader exists without a header, the subHeader should be an h2 element
36+
if ((await header.count()) === 0 && (await subHeader.count()) > 0) {
37+
const subHeaderTagName = await subHeader.evaluate((el) => el.tagName);
38+
expect(subHeaderTagName).toBe('H2');
39+
}
40+
2041
/**
2142
* expect().toHaveAttribute() can't check for a null value, so grab and check
2243
* the values manually instead.
@@ -124,16 +145,24 @@ configs({ directions: ['ltr'] }).forEach(({ config, title }) => {
124145
await page.goto(`/src/components/alert/test/a11y`, config);
125146
});
126147

127-
test('should have aria-labelledby when header is set', async ({ page }) => {
128-
await testAria(page, 'noMessage', 'alert-1-hdr', null);
148+
test('should have aria-labelledby set to both when header and subHeader are set', async ({ page }) => {
149+
await testAria(page, 'bothHeadersOnly', 'alert-1-hdr alert-1-sub-hdr', null);
150+
});
151+
152+
test('should have aria-labelledby set when only header is set', async ({ page }) => {
153+
await testAria(page, 'headerOnly', 'alert-1-hdr', null);
154+
});
155+
156+
test('should fall back to subHeader for aria-labelledby if header is not defined', async ({ page }) => {
157+
await testAria(page, 'subHeaderOnly', 'alert-1-sub-hdr', null);
129158
});
130159

131160
test('should have aria-describedby when message is set', async ({ page }) => {
132161
await testAria(page, 'noHeaders', null, 'alert-1-msg');
133162
});
134163

135-
test('should fall back to subHeader for aria-labelledby if header is not defined', async ({ page }) => {
136-
await testAria(page, 'subHeaderOnly', 'alert-1-sub-hdr', 'alert-1-msg');
164+
test('should have aria-labelledby and aria-describedby when headers and message are set', async ({ page }) => {
165+
await testAria(page, 'headersAndMessage', 'alert-1-hdr alert-1-sub-hdr', 'alert-1-msg');
137166
});
138167

139168
test('should allow for manually specifying aria attributes', async ({ page }) => {

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

+13-6
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@
1919
<main class="ion-padding">
2020
<h1>Alert - A11y</h1>
2121

22-
<button class="expand" id="bothHeaders" onclick="presentBothHeaders()">Both Headers</button>
22+
<button class="expand" id="bothHeadersOnly" onclick="presentBothHeadersOnly()">Both Headers Only</button>
23+
<button class="expand" id="headerOnly" onclick="presentHeaderOnly()">Header Only</button>
2324
<button class="expand" id="subHeaderOnly" onclick="presentSubHeaderOnly()">Subheader Only</button>
2425
<button class="expand" id="noHeaders" onclick="presentNoHeaders()">No Headers</button>
25-
<button class="expand" id="noMessage" onclick="presentNoMessage()">No Message</button>
26+
<button class="expand" id="headersAndMessage" onclick="presentHeadersAndMessage()">Headers and Message</button>
2627
<button class="expand" id="customAria" onclick="presentCustomAria()">Custom Aria</button>
2728
<button class="expand" id="ariaLabelButton" onclick="presentAriaLabelButton()">Aria Label Button</button>
2829
<button class="expand" id="checkbox" onclick="presentAlertCheckbox()">Checkbox</button>
@@ -34,19 +35,24 @@ <h1>Alert - A11y</h1>
3435
await alert.present();
3536
}
3637

37-
function presentBothHeaders() {
38+
function presentBothHeadersOnly() {
3839
openAlert({
3940
header: 'Header',
4041
subHeader: 'Subtitle',
41-
message: 'This is an alert message.',
42+
buttons: ['OK'],
43+
});
44+
}
45+
46+
function presentHeaderOnly() {
47+
openAlert({
48+
header: 'Header',
4249
buttons: ['OK'],
4350
});
4451
}
4552

4653
function presentSubHeaderOnly() {
4754
openAlert({
4855
subHeader: 'Subtitle',
49-
message: 'This is an alert message.',
5056
buttons: ['OK'],
5157
});
5258
}
@@ -58,10 +64,11 @@ <h1>Alert - A11y</h1>
5864
});
5965
}
6066

61-
function presentNoMessage() {
67+
function presentHeadersAndMessage() {
6268
openAlert({
6369
header: 'Header',
6470
subHeader: 'Subtitle',
71+
message: 'This is an alert message.',
6572
buttons: ['OK'],
6673
});
6774
}

0 commit comments

Comments
 (0)