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
This commit is contained in:
Brandy Carney
2023-07-31 11:07:00 -04:00
committed by GitHub
parent 5d1ee1646f
commit 9a685882b7
4 changed files with 46 additions and 5 deletions

View File

@ -34,6 +34,14 @@
Present Controller Toast Present Controller Toast
</ion-button> </ion-button>
<ion-button id="aria-label-toast-trigger">Present Aria Label Toast</ion-button>
<ion-toast
id="aria-label-toast"
trigger="aria-label-toast-trigger"
header="Aria Label Toast Header"
message="Aria Label Toast Message"
></ion-toast>
<ion-button onclick="updateContent()">Update Inner Content</ion-button> <ion-button onclick="updateContent()">Update Inner Content</ion-button>
</main> </main>
</ion-app> </ion-app>
@ -41,6 +49,17 @@
const inlineToast = document.querySelector('#inline-toast'); const inlineToast = document.querySelector('#inline-toast');
inlineToast.buttons = ['Ok']; inlineToast.buttons = ['Ok'];
const ariaLabelToast = document.querySelector('#aria-label-toast');
ariaLabelToast.buttons = [
{
icon: 'close',
htmlAttributes: {
'aria-label': 'close button',
'aria-labelledby': 'close-label',
},
},
];
const presentToast = async (opts) => { const presentToast = async (opts) => {
const toast = await toastController.create(opts); const toast = await toastController.create(opts);

View File

@ -11,10 +11,10 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
await page.goto(`/src/components/toast/test/a11y`, config); await page.goto(`/src/components/toast/test/a11y`, config);
}); });
test('should not have any axe violations with inline toasts', async ({ page }) => { test('should not have any axe violations with inline toasts', async ({ page }) => {
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent'); const didPresent = await page.spyOnEvent('ionToastDidPresent');
await page.click('#inline-toast-trigger'); await page.click('#inline-toast-trigger');
await ionToastDidPresent.next(); await didPresent.next();
/** /**
* IonToast overlays the entire screen, so * IonToast overlays the entire screen, so
@ -25,10 +25,10 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
expect(results.violations).toEqual([]); expect(results.violations).toEqual([]);
}); });
test('should not have any axe violations with controller toasts', async ({ page }) => { test('should not have any axe violations with controller toasts', async ({ page }) => {
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent'); const didPresent = await page.spyOnEvent('ionToastDidPresent');
await page.click('#controller-toast-trigger'); await page.click('#controller-toast-trigger');
await ionToastDidPresent.next(); await didPresent.next();
/** /**
* IonToast overlays the entire screen, so * IonToast overlays the entire screen, so
@ -38,5 +38,19 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
const results = await new AxeBuilder({ page }).disableRules('color-contrast').analyze(); const results = await new AxeBuilder({ page }).disableRules('color-contrast').analyze();
expect(results.violations).toEqual([]); expect(results.violations).toEqual([]);
}); });
test('should have aria-labelledby and aria-label added to the button when htmlAttributes is set', async ({
page,
}) => {
const didPresent = await page.spyOnEvent('ionToastDidPresent');
await page.click('#aria-label-toast-trigger');
await didPresent.next();
const toastButton = page.locator('#aria-label-toast .toast-button');
await expect(toastButton).toHaveAttribute('aria-labelledby', 'close-label');
await expect(toastButton).toHaveAttribute('aria-label', 'close button');
});
}); });
}); });

View File

@ -31,6 +31,7 @@ export interface ToastButton {
side?: 'start' | 'end'; side?: 'start' | 'end';
role?: 'cancel' | string; role?: 'cancel' | string;
cssClass?: string | string[]; cssClass?: string | string[];
htmlAttributes?: { [key: string]: any };
handler?: () => boolean | void | Promise<boolean | void>; handler?: () => boolean | void | Promise<boolean | void>;
} }

View File

@ -405,7 +405,14 @@ export class Toast implements ComponentInterface, OverlayInterface {
return ( return (
<div class={buttonGroupsClasses}> <div class={buttonGroupsClasses}>
{buttons.map((b) => ( {buttons.map((b) => (
<button type="button" class={buttonClass(b)} tabIndex={0} onClick={() => this.buttonClick(b)} part="button"> <button
{...b.htmlAttributes}
type="button"
class={buttonClass(b)}
tabIndex={0}
onClick={() => this.buttonClick(b)}
part="button"
>
<div class="toast-button-inner"> <div class="toast-button-inner">
{b.icon && ( {b.icon && (
<ion-icon <ion-icon