Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions core/src/components/alert/alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -578,20 +578,26 @@ export class Alert implements ComponentInterface, OverlayInterface {
}

render() {
const { overlayIndex, header, subHeader, htmlAttributes } = this;
const { overlayIndex, header, subHeader, message, htmlAttributes } = this;
const mode = getIonMode(this);
const hdrId = `alert-${overlayIndex}-hdr`;
const subHdrId = `alert-${overlayIndex}-sub-hdr`;
const msgId = `alert-${overlayIndex}-msg`;
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 (
<Host
role={role}
aria-modal="true"
aria-labelledby={ariaLabelledBy}
aria-describedby={message ? msgId : null}
tabindex="-1"
aria-label={defaultAriaLabel}
{...(htmlAttributes as any)}
style={{
zIndex: `${20000 + overlayIndex}`,
Expand Down Expand Up @@ -623,7 +629,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
)}
</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.renderAlertButtons()}
Expand Down
41 changes: 29 additions & 12 deletions core/src/components/alert/test/a11y/alert.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,29 @@ import { expect } from '@playwright/test';
import type { E2EPage } 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}`);

await button.click();
await didPresent.next();

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', () => {
Expand All @@ -17,27 +34,27 @@ test.describe('alert: a11y', () => {
await page.goto(`/src/components/alert/test/a11y`);
});

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

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

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

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

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

test('should allow for manually specifying aria-label', async ({ page }) => {
await testAriaLabel(page, 'customAriaLabel', 'Custom alert');
test('should allow for manually specifying aria attributes', async ({ page }) => {
await testAria(page, 'customAria', 'Custom title', 'Custom description');
});
});
38 changes: 22 additions & 16 deletions core/src/components/alert/test/a11y/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,11 @@
<main class="ion-padding">
<h1>Alert - A11y</h1>

<ion-button id="noHeader" expand="block" onclick="presentNoHeader()">Alert With No Header</ion-button>
<ion-button id="customHeader" expand="block" onclick="presentCustomHeader()">Alert With Custom Header</ion-button>
<ion-button id="subHeaderOnly" expand="block" onclick="presentSubHeaderOnly()"
>Alert With Subheader Only</ion-button
>
<ion-button id="customAriaLabel" expand="block" onclick="presentCustomAriaLabel()"
>Alert With Custom Aria Label</ion-button
>
<ion-button id="bothHeaders" expand="block" onclick="presentBothHeaders()">Both Headers</ion-button>
<ion-button id="subHeaderOnly" expand="block" onclick="presentSubHeaderOnly()">Subheader Only</ion-button>
<ion-button id="noHeaders" expand="block" onclick="presentNoHeaders()">No Headers</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>
</main>

<script>
Expand All @@ -35,38 +32,47 @@ <h1>Alert - A11y</h1>
await alert.present();
}

function presentNoHeader() {
function presentBothHeaders() {
openAlert({
header: 'Header',
subHeader: 'Subtitle',
message: 'This is an alert message.',
buttons: ['OK'],
});
}

function presentCustomHeader() {
function presentSubHeaderOnly() {
openAlert({
header: 'Header',
subHeader: 'Subtitle',
message: 'This is an alert message.',
buttons: ['OK'],
});
}

function presentSubHeaderOnly() {
function presentNoHeaders() {
openAlert({
subHeader: 'Subtitle',
message: 'This is an alert message.',
buttons: ['OK'],
});
}

function presentCustomAriaLabel() {
function presentNoMessage() {
openAlert({
header: 'Header',
subHeader: 'Subtitle',
buttons: ['OK'],
});
}

function presentCustomAria() {
openAlert({
header: 'Header',
subHeader: 'Subtitle',
message: 'This is an alert message with a custom aria-label.',
message: 'This is an alert message with custom aria attributes.',
buttons: ['OK'],
htmlAttributes: {
'aria-label': 'Custom alert',
'aria-labelledby': 'Custom title',
'aria-describedby': 'Custom description',
},
});
}
Expand Down