Skip to content

Commit 5ce4ec0

Browse files
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
1 parent 9a68588 commit 5ce4ec0

File tree

4 files changed

+85
-4
lines changed

4 files changed

+85
-4
lines changed

core/src/components/action-sheet/action-sheet-interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface ActionSheetButton<T = any> {
2323
icon?: string;
2424
cssClass?: string | string[];
2525
id?: string;
26+
htmlAttributes?: { [key: string]: any };
2627
handler?: () => boolean | void | Promise<boolean | void>;
2728
data?: T;
2829
}

core/src/components/action-sheet/action-sheet.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,13 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
386386
</div>
387387
)}
388388
{buttons.map((b) => (
389-
<button type="button" id={b.id} class={buttonClass(b)} onClick={() => this.buttonClick(b)}>
389+
<button
390+
{...b.htmlAttributes}
391+
type="button"
392+
id={b.id}
393+
class={buttonClass(b)}
394+
onClick={() => this.buttonClick(b)}
395+
>
390396
<span class="action-sheet-button-inner">
391397
{b.icon && <ion-icon icon={b.icon} aria-hidden="true" lazy={false} class="action-sheet-icon" />}
392398
{b.text}
@@ -398,7 +404,12 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
398404

399405
{cancelButton && (
400406
<div class="action-sheet-group action-sheet-group-cancel">
401-
<button type="button" class={buttonClass(cancelButton)} onClick={() => this.buttonClick(cancelButton)}>
407+
<button
408+
{...cancelButton.htmlAttributes}
409+
type="button"
410+
class={buttonClass(cancelButton)}
411+
onClick={() => this.buttonClick(cancelButton)}
412+
>
402413
<span class="action-sheet-button-inner">
403414
{cancelButton.icon && (
404415
<ion-icon icon={cancelButton.icon} aria-hidden="true" lazy={false} class="action-sheet-icon" />

core/src/components/action-sheet/test/a11y/action-sheet.e2e.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,36 @@ const testAria = async (page: E2EPage, buttonID: string, expectedAriaLabelledBy:
1010
await button.click();
1111
await didPresent.next();
1212

13-
const alert = page.locator('ion-action-sheet');
13+
const actionSheet = page.locator('ion-action-sheet');
1414

1515
/**
1616
* expect().toHaveAttribute() can't check for a null value, so grab and check
1717
* the value manually instead.
1818
*/
19-
const ariaLabelledBy = await alert.getAttribute('aria-labelledby');
19+
const ariaLabelledBy = await actionSheet.getAttribute('aria-labelledby');
2020

2121
expect(ariaLabelledBy).toBe(expectedAriaLabelledBy);
2222
};
23+
24+
const testAriaButton = async (
25+
page: E2EPage,
26+
buttonID: string,
27+
expectedAriaLabelledBy: string,
28+
expectedAriaLabel: string
29+
) => {
30+
const didPresent = await page.spyOnEvent('ionActionSheetDidPresent');
31+
32+
const button = page.locator(`#${buttonID}`);
33+
await button.click();
34+
35+
await didPresent.next();
36+
37+
const actionSheetButton = page.locator('ion-action-sheet .action-sheet-button');
38+
39+
await expect(actionSheetButton).toHaveAttribute('aria-labelledby', expectedAriaLabelledBy);
40+
await expect(actionSheetButton).toHaveAttribute('aria-label', expectedAriaLabel);
41+
};
42+
2343
configs({ directions: ['ltr'] }).forEach(({ config, title }) => {
2444
test.describe(title('action-sheet: a11y'), () => {
2545
test.beforeEach(async ({ page }) => {
@@ -52,5 +72,17 @@ configs({ directions: ['ltr'] }).forEach(({ config, title }) => {
5272
test('should allow for manually specifying aria attributes', async ({ page }) => {
5373
await testAria(page, 'customAria', 'Custom title');
5474
});
75+
76+
test('should have aria-labelledby and aria-label added to the button when htmlAttributes is set', async ({
77+
page,
78+
}) => {
79+
await testAriaButton(page, 'ariaLabelButton', 'close-label', 'close button');
80+
});
81+
82+
test('should have aria-labelledby and aria-label added to the cancel button when htmlAttributes is set', async ({
83+
page,
84+
}) => {
85+
await testAriaButton(page, 'ariaLabelCancelButton', 'cancel-label', 'cancel button');
86+
});
5587
});
5688
});

core/src/components/action-sheet/test/a11y/index.html

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ <h1>Action Sheet - A11y</h1>
2323
<ion-button id="subHeaderOnly" expand="block" onclick="presentSubHeaderOnly()">Subheader Only</ion-button>
2424
<ion-button id="noHeaders" expand="block" onclick="presentNoHeaders()">No Headers</ion-button>
2525
<ion-button id="customAria" expand="block" onclick="presentCustomAria()">Custom Aria</ion-button>
26+
<ion-button id="ariaLabelButton" expand="block" onclick="presentAriaLabelButton()">Aria Label Button</ion-button>
27+
<ion-button id="ariaLabelCancelButton" expand="block" onclick="presentAriaLabelCancelButton()"
28+
>Aria Label Cancel Button</ion-button
29+
>
2630
</main>
2731

2832
<script>
@@ -63,6 +67,39 @@ <h1>Action Sheet - A11y</h1>
6367
},
6468
});
6569
}
70+
71+
function presentAriaLabelButton() {
72+
openActionSheet({
73+
header: 'Header',
74+
subHeader: 'Subtitle',
75+
buttons: [
76+
{
77+
text: 'Close',
78+
htmlAttributes: {
79+
'aria-label': 'close button',
80+
'aria-labelledby': 'close-label',
81+
},
82+
},
83+
],
84+
});
85+
}
86+
87+
function presentAriaLabelCancelButton() {
88+
openActionSheet({
89+
header: 'Header',
90+
subHeader: 'Subtitle',
91+
buttons: [
92+
{
93+
text: 'Cancel',
94+
role: 'cancel',
95+
htmlAttributes: {
96+
'aria-label': 'cancel button',
97+
'aria-labelledby': 'cancel-label',
98+
},
99+
},
100+
],
101+
});
102+
}
66103
</script>
67104
</body>
68105
</html>

0 commit comments

Comments
 (0)