feat(action-sheet): add htmlAttributes property for passing attributes to buttons (#27863)

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 `ActionSheetButton`
interface
- Passes the `htmlAttributes` to the buttons (both the buttons array and
the cancelButton)
- Adds two tests to verify `aria-label` and `aria-labelled-by` are
passed to a button with and without the cancel role - this was done
because action sheet breaks these buttons up when rendering

## Does this introduce a breaking change?

- [ ] Yes
- [x] No
This commit is contained in:
Brandy Carney
2023-07-31 12:23:46 -04:00
committed by GitHub
parent 9a685882b7
commit 5ce4ec0439
4 changed files with 85 additions and 4 deletions

View File

@ -23,6 +23,7 @@ export interface ActionSheetButton<T = any> {
icon?: string;
cssClass?: string | string[];
id?: string;
htmlAttributes?: { [key: string]: any };
handler?: () => boolean | void | Promise<boolean | void>;
data?: T;
}

View File

@ -386,7 +386,13 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
</div>
)}
{buttons.map((b) => (
<button type="button" id={b.id} class={buttonClass(b)} onClick={() => this.buttonClick(b)}>
<button
{...b.htmlAttributes}
type="button"
id={b.id}
class={buttonClass(b)}
onClick={() => this.buttonClick(b)}
>
<span class="action-sheet-button-inner">
{b.icon && <ion-icon icon={b.icon} aria-hidden="true" lazy={false} class="action-sheet-icon" />}
{b.text}
@ -398,7 +404,12 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
{cancelButton && (
<div class="action-sheet-group action-sheet-group-cancel">
<button type="button" class={buttonClass(cancelButton)} onClick={() => this.buttonClick(cancelButton)}>
<button
{...cancelButton.htmlAttributes}
type="button"
class={buttonClass(cancelButton)}
onClick={() => this.buttonClick(cancelButton)}
>
<span class="action-sheet-button-inner">
{cancelButton.icon && (
<ion-icon icon={cancelButton.icon} aria-hidden="true" lazy={false} class="action-sheet-icon" />

View File

@ -10,16 +10,36 @@ const testAria = async (page: E2EPage, buttonID: string, expectedAriaLabelledBy:
await button.click();
await didPresent.next();
const alert = page.locator('ion-action-sheet');
const actionSheet = page.locator('ion-action-sheet');
/**
* expect().toHaveAttribute() can't check for a null value, so grab and check
* the value manually instead.
*/
const ariaLabelledBy = await alert.getAttribute('aria-labelledby');
const ariaLabelledBy = await actionSheet.getAttribute('aria-labelledby');
expect(ariaLabelledBy).toBe(expectedAriaLabelledBy);
};
const testAriaButton = async (
page: E2EPage,
buttonID: string,
expectedAriaLabelledBy: string,
expectedAriaLabel: string
) => {
const didPresent = await page.spyOnEvent('ionActionSheetDidPresent');
const button = page.locator(`#${buttonID}`);
await button.click();
await didPresent.next();
const actionSheetButton = page.locator('ion-action-sheet .action-sheet-button');
await expect(actionSheetButton).toHaveAttribute('aria-labelledby', expectedAriaLabelledBy);
await expect(actionSheetButton).toHaveAttribute('aria-label', expectedAriaLabel);
};
configs({ directions: ['ltr'] }).forEach(({ config, title }) => {
test.describe(title('action-sheet: a11y'), () => {
test.beforeEach(async ({ page }) => {
@ -52,5 +72,17 @@ configs({ directions: ['ltr'] }).forEach(({ config, title }) => {
test('should allow for manually specifying aria attributes', async ({ page }) => {
await testAria(page, 'customAria', 'Custom title');
});
test('should have aria-labelledby and aria-label added to the button when htmlAttributes is set', async ({
page,
}) => {
await testAriaButton(page, 'ariaLabelButton', 'close-label', 'close button');
});
test('should have aria-labelledby and aria-label added to the cancel button when htmlAttributes is set', async ({
page,
}) => {
await testAriaButton(page, 'ariaLabelCancelButton', 'cancel-label', 'cancel button');
});
});
});

View File

@ -23,6 +23,10 @@
<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="customAria" expand="block" onclick="presentCustomAria()">Custom Aria</ion-button>
<ion-button id="ariaLabelButton" expand="block" onclick="presentAriaLabelButton()">Aria Label Button</ion-button>
<ion-button id="ariaLabelCancelButton" expand="block" onclick="presentAriaLabelCancelButton()"
>Aria Label Cancel Button</ion-button
>
</main>
<script>
@ -63,6 +67,39 @@
},
});
}
function presentAriaLabelButton() {
openActionSheet({
header: 'Header',
subHeader: 'Subtitle',
buttons: [
{
text: 'Close',
htmlAttributes: {
'aria-label': 'close button',
'aria-labelledby': 'close-label',
},
},
],
});
}
function presentAriaLabelCancelButton() {
openActionSheet({
header: 'Header',
subHeader: 'Subtitle',
buttons: [
{
text: 'Cancel',
role: 'cancel',
htmlAttributes: {
'aria-label': 'cancel button',
'aria-labelledby': 'cancel-label',
},
},
],
});
}
</script>
</body>
</html>