Skip to content

Commit 9a68588

Browse files
feat(toast): add htmlAttributes property for passing attributes to buttons (#27855)
Issue number: N/A --------- ## What is the current behavior? Buttons containing only icons are not accessible as there is no way to pass an `aria-label` attribute (or any other html attribute). ## What is the new behavior? - Adds the `htmlAttributes` property on the `ToastButton` interface - Passes the `htmlAttributes` to the buttons - Adds a test to verify `aria-label` and `aria-labelled-by` are passed to the button ## Does this introduce a breaking change? - [ ] Yes - [x] No
1 parent 5d1ee16 commit 9a68588

File tree

4 files changed

+46
-5
lines changed

4 files changed

+46
-5
lines changed

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,32 @@ <h1 style="background-color: white">Toast - a11y</h1>
3434
Present Controller Toast
3535
</ion-button>
3636

37+
<ion-button id="aria-label-toast-trigger">Present Aria Label Toast</ion-button>
38+
<ion-toast
39+
id="aria-label-toast"
40+
trigger="aria-label-toast-trigger"
41+
header="Aria Label Toast Header"
42+
message="Aria Label Toast Message"
43+
></ion-toast>
44+
3745
<ion-button onclick="updateContent()">Update Inner Content</ion-button>
3846
</main>
3947
</ion-app>
4048
<script>
4149
const inlineToast = document.querySelector('#inline-toast');
4250
inlineToast.buttons = ['Ok'];
4351

52+
const ariaLabelToast = document.querySelector('#aria-label-toast');
53+
ariaLabelToast.buttons = [
54+
{
55+
icon: 'close',
56+
htmlAttributes: {
57+
'aria-label': 'close button',
58+
'aria-labelledby': 'close-label',
59+
},
60+
},
61+
];
62+
4463
const presentToast = async (opts) => {
4564
const toast = await toastController.create(opts);
4665

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
1111
await page.goto(`/src/components/toast/test/a11y`, config);
1212
});
1313
test('should not have any axe violations with inline toasts', async ({ page }) => {
14-
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent');
14+
const didPresent = await page.spyOnEvent('ionToastDidPresent');
1515

1616
await page.click('#inline-toast-trigger');
17-
await ionToastDidPresent.next();
17+
await didPresent.next();
1818

1919
/**
2020
* IonToast overlays the entire screen, so
@@ -25,10 +25,10 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
2525
expect(results.violations).toEqual([]);
2626
});
2727
test('should not have any axe violations with controller toasts', async ({ page }) => {
28-
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent');
28+
const didPresent = await page.spyOnEvent('ionToastDidPresent');
2929

3030
await page.click('#controller-toast-trigger');
31-
await ionToastDidPresent.next();
31+
await didPresent.next();
3232

3333
/**
3434
* IonToast overlays the entire screen, so
@@ -38,5 +38,19 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
3838
const results = await new AxeBuilder({ page }).disableRules('color-contrast').analyze();
3939
expect(results.violations).toEqual([]);
4040
});
41+
42+
test('should have aria-labelledby and aria-label added to the button when htmlAttributes is set', async ({
43+
page,
44+
}) => {
45+
const didPresent = await page.spyOnEvent('ionToastDidPresent');
46+
47+
await page.click('#aria-label-toast-trigger');
48+
await didPresent.next();
49+
50+
const toastButton = page.locator('#aria-label-toast .toast-button');
51+
52+
await expect(toastButton).toHaveAttribute('aria-labelledby', 'close-label');
53+
await expect(toastButton).toHaveAttribute('aria-label', 'close button');
54+
});
4155
});
4256
});

core/src/components/toast/toast-interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface ToastButton {
3131
side?: 'start' | 'end';
3232
role?: 'cancel' | string;
3333
cssClass?: string | string[];
34+
htmlAttributes?: { [key: string]: any };
3435
handler?: () => boolean | void | Promise<boolean | void>;
3536
}
3637

core/src/components/toast/toast.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,14 @@ export class Toast implements ComponentInterface, OverlayInterface {
405405
return (
406406
<div class={buttonGroupsClasses}>
407407
{buttons.map((b) => (
408-
<button type="button" class={buttonClass(b)} tabIndex={0} onClick={() => this.buttonClick(b)} part="button">
408+
<button
409+
{...b.htmlAttributes}
410+
type="button"
411+
class={buttonClass(b)}
412+
tabIndex={0}
413+
onClick={() => this.buttonClick(b)}
414+
part="button"
415+
>
409416
<div class="toast-button-inner">
410417
{b.icon && (
411418
<ion-icon

0 commit comments

Comments
 (0)