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; icon?: string;
cssClass?: string | string[]; cssClass?: string | string[];
id?: string; id?: string;
htmlAttributes?: { [key: string]: any };
handler?: () => boolean | void | Promise<boolean | void>; handler?: () => boolean | void | Promise<boolean | void>;
data?: T; data?: T;
} }

View File

@ -386,7 +386,13 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
</div> </div>
)} )}
{buttons.map((b) => ( {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"> <span class="action-sheet-button-inner">
{b.icon && <ion-icon icon={b.icon} aria-hidden="true" lazy={false} class="action-sheet-icon" />} {b.icon && <ion-icon icon={b.icon} aria-hidden="true" lazy={false} class="action-sheet-icon" />}
{b.text} {b.text}
@ -398,7 +404,12 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
{cancelButton && ( {cancelButton && (
<div class="action-sheet-group action-sheet-group-cancel"> <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"> <span class="action-sheet-button-inner">
{cancelButton.icon && ( {cancelButton.icon && (
<ion-icon icon={cancelButton.icon} aria-hidden="true" lazy={false} class="action-sheet-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 button.click();
await didPresent.next(); 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 * expect().toHaveAttribute() can't check for a null value, so grab and check
* the value manually instead. * the value manually instead.
*/ */
const ariaLabelledBy = await alert.getAttribute('aria-labelledby'); const ariaLabelledBy = await actionSheet.getAttribute('aria-labelledby');
expect(ariaLabelledBy).toBe(expectedAriaLabelledBy); 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 }) => { configs({ directions: ['ltr'] }).forEach(({ config, title }) => {
test.describe(title('action-sheet: a11y'), () => { test.describe(title('action-sheet: a11y'), () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
@ -52,5 +72,17 @@ configs({ directions: ['ltr'] }).forEach(({ config, title }) => {
test('should allow for manually specifying aria attributes', async ({ page }) => { test('should allow for manually specifying aria attributes', async ({ page }) => {
await testAria(page, 'customAria', 'Custom title'); 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="subHeaderOnly" expand="block" onclick="presentSubHeaderOnly()">Subheader Only</ion-button>
<ion-button id="noHeaders" expand="block" onclick="presentNoHeaders()">No Headers</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="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> </main>
<script> <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> </script>
</body> </body>
</html> </html>