fix(action-sheet): add aria-labelledby (#25837)

This commit is contained in:
Amanda Johnston
2022-08-29 15:47:11 -05:00
committed by GitHub
parent 9cedfcd3ef
commit 527015184e
3 changed files with 132 additions and 5 deletions

View File

@ -239,16 +239,18 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
} }
render() { render() {
const { htmlAttributes } = this; const { header, htmlAttributes, overlayIndex } = this;
const mode = getIonMode(this); const mode = getIonMode(this);
const allButtons = this.getButtons(); const allButtons = this.getButtons();
const cancelButton = allButtons.find((b) => b.role === 'cancel'); const cancelButton = allButtons.find((b) => b.role === 'cancel');
const buttons = allButtons.filter((b) => b.role !== 'cancel'); const buttons = allButtons.filter((b) => b.role !== 'cancel');
const headerID = `action-sheet-${overlayIndex}-header`;
return ( return (
<Host <Host
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby={header !== undefined ? headerID : null}
tabindex="-1" tabindex="-1"
{...(htmlAttributes as any)} {...(htmlAttributes as any)}
style={{ style={{
@ -256,7 +258,6 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
}} }}
class={{ class={{
[mode]: true, [mode]: true,
...getClassMap(this.cssClass), ...getClassMap(this.cssClass),
'overlay-hidden': true, 'overlay-hidden': true,
'action-sheet-translucent': this.translucent, 'action-sheet-translucent': this.translucent,
@ -268,17 +269,18 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
<div tabindex="0"></div> <div tabindex="0"></div>
<div class="action-sheet-wrapper ion-overlay-wrapper" role="dialog" ref={(el) => (this.wrapperEl = el)}> <div class="action-sheet-wrapper ion-overlay-wrapper" ref={(el) => (this.wrapperEl = el)}>
<div class="action-sheet-container"> <div class="action-sheet-container">
<div class="action-sheet-group" ref={(el) => (this.groupEl = el)}> <div class="action-sheet-group" ref={(el) => (this.groupEl = el)}>
{this.header !== undefined && ( {header !== undefined && (
<div <div
id={headerID}
class={{ class={{
'action-sheet-title': true, 'action-sheet-title': true,
'action-sheet-has-sub-title': this.subHeader !== undefined, 'action-sheet-has-sub-title': this.subHeader !== undefined,
}} }}
> >
{this.header} {header}
{this.subHeader && <div class="action-sheet-sub-title">{this.subHeader}</div>} {this.subHeader && <div class="action-sheet-sub-title">{this.subHeader}</div>}
</div> </div>
)} )}

View File

@ -0,0 +1,57 @@
import AxeBuilder from '@axe-core/playwright';
import { expect } from '@playwright/test';
import type { E2EPage } from '@utils/test/playwright';
import { test } from '@utils/test/playwright';
const testAria = async (page: E2EPage, buttonID: string, expectedAriaLabelledBy: string | null) => {
const didPresent = await page.spyOnEvent('ionActionSheetDidPresent');
const button = page.locator(`#${buttonID}`);
await button.click();
await didPresent.next();
const alert = 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');
expect(ariaLabelledBy).toBe(expectedAriaLabelledBy);
};
test.describe('action-sheet: a11y', () => {
test.beforeEach(async ({ page, skip }) => {
skip.rtl();
await page.goto(`/src/components/action-sheet/test/a11y`);
});
test('should not have accessibility violations when header is defined', async ({ page }) => {
const button = page.locator('#bothHeaders');
const didPresent = await page.spyOnEvent('ionActionSheetDidPresent');
await button.click();
await didPresent.next();
/**
* action-sheet overlays the entire screen, so
* Axe will be unable to verify color contrast
* on elements under the backdrop.
*/
const results = await new AxeBuilder({ page }).disableRules('color-contrast').analyze();
expect(results.violations).toEqual([]);
});
test('should have aria-labelledby when header is set', async ({ page }) => {
await testAria(page, 'bothHeaders', 'action-sheet-1-header');
});
test('should not have aria-labelledby when header is not set', async ({ page }) => {
await testAria(page, 'noHeaders', null);
});
test('should allow for manually specifying aria attributes', async ({ page }) => {
await testAria(page, 'customAria', 'Custom title');
});
});

View File

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Action Sheet - A11y</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<script type="module">
import { actionSheetController } from '../../../../dist/ionic/index.esm.js';
window.actionSheetController = actionSheetController;
</script>
<body>
<main class="ion-padding">
<h1>Action Sheet - A11y</h1>
<ion-button id="bothHeaders" expand="block" onclick="presentBothHeaders()">Both Headers</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="customAria" expand="block" onclick="presentCustomAria()">Custom Aria</ion-button>
</main>
<script>
async function openActionSheet(opts) {
const actionSheet = await actionSheetController.create(opts);
await actionSheet.present();
}
function presentBothHeaders() {
openActionSheet({
header: 'Header',
subHeader: 'Subtitle',
buttons: ['Confirm'],
});
}
function presentSubHeaderOnly() {
openActionSheet({
subHeader: 'Subtitle',
buttons: ['Confirm'],
});
}
function presentNoHeaders() {
openActionSheet({
buttons: ['Confirm'],
});
}
function presentCustomAria() {
openActionSheet({
header: 'Header',
subHeader: 'Subtitle',
buttons: ['Confirm'],
htmlAttributes: {
'aria-labelledby': 'Custom title',
'aria-describedby': 'Custom description',
},
});
}
</script>
</body>
</html>