fix(toast): screen readers announce content (#27198)

Issue URL: resolves #25866

---------

Docs PR: https://github.com/ionic-team/ionic-docs/pull/2914

<!-- Please refer to our contributing documentation for any questions on
submitting a pull request, or let us know here if you need any help:
https://ionicframework.com/docs/building/contributing -->

<!-- Some docs updates need to be made in the `ionic-docs` repo, in a
separate PR. See
https://github.com/ionic-team/ionic-framework/blob/main/.github/CONTRIBUTING.md#modifying-documentation
for details. -->

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->


<!-- Issues are required for both bug fixes and features. -->

NVDA is not announcing toasts on present.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Toast has a "status" role and "polite" announcement.
- We also revisited the intended behavior of toasts to better align with
the Material Design v2 spec:
https://m2.material.io/components/snackbars/web#accessibility

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Dev build: 7.0.3-dev.11681482468.19d7784f
This commit is contained in:
octicon-git-branch(16/)
octicon-tag(16/)
Liam DeBeasi
2023-04-18 10:57:30 -04:00
committed by GitHub
gitea-unlock(16/)
parent 742d4295dd
commit 76c8b94e2a
octicon-diff(16/tw-mr-1) 4 changed files with 165 additions and 31 deletions

38
core/src/components/toast/test/a11y/index.html
View File

@@ -18,21 +18,47 @@
<main>
<h1 style="background-color: white">Toast - a11y</h1>
<button id="polite" onclick="presentToast({ message: 'This is a toast message' })">Present Toast</button>
<button
id="assertive"
onclick="presentToast({ message: 'This is an assertive toast message', htmlAttributes: { 'aria-live': 'assertive' } })"
<ion-button id="inline-toast-trigger">Present Inline Toast</ion-button>
<ion-toast
id="inline-toast"
trigger="inline-toast-trigger"
icon="person"
header="Inline Toast Header"
message="Inline Toast Message"
></ion-toast>
<ion-button
id="controller-toast-trigger"
onclick="presentToast({ icon: 'person', header: 'Controller Toast Header', message: 'Controller Toast Message', buttons: ['Ok'] })"
>
Present Assertive Toast
</button>
Present Controller Toast
</ion-button>
<ion-button onclick="updateContent()">Update Inner Content</ion-button>
</main>
</ion-app>
<script>
const inlineToast = document.querySelector('#inline-toast');
inlineToast.buttons = ['Ok'];
const presentToast = async (opts) => {
const toast = await toastController.create(opts);
await toast.present();
};
const updateContent = () => {
const toasts = document.querySelectorAll('ion-toast');
/**
* Note: Multiple updates to the props
* may cause screen readers like NVDA to announce
* the entire content multiple times.
*/
toasts.forEach((toast) => {
toast.header = 'Updated Header';
toast.message = 'Updated Message';
});
};
</script>
</body>
</html>

12
core/src/components/toast/test/a11y/toast.e2e.ts
View File

@@ -7,12 +7,10 @@ test.describe('toast: a11y', () => {
test.skip(testInfo.project.metadata.rtl === true, 'This test does not check LTR vs RTL layouts');
await page.goto(`/src/components/toast/test/a11y`);
});
test('should not have any axe violations with polite toasts', async ({ page }) => {
test('should not have any axe violations with inline toasts', async ({ page }) => {
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent');
const politeButton = page.locator('#polite');
await politeButton.click();
await page.click('#inline-toast-trigger');
await ionToastDidPresent.next();
/**
@@ -23,12 +21,10 @@ test.describe('toast: a11y', () => {
const results = await new AxeBuilder({ page }).disableRules('color-contrast').analyze();
expect(results.violations).toEqual([]);
});
test('should not have any axe violations with assertive toasts', async ({ page }) => {
test('should not have any axe violations with controller toasts', async ({ page }) => {
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent');
const politeButton = page.locator('#assertive');
await politeButton.click();
await page.click('#controller-toast-trigger');
await ionToastDidPresent.next();
/**

49
core/src/components/toast/test/toast.spec.ts
View File

@@ -2,7 +2,7 @@ import { newSpecPage } from '@stencil/core/testing';
import { Toast } from '../toast';
import { config } from '../../../global/config';
describe('alert: custom html', () => {
describe('toast: custom html', () => {
it('should not allow for custom html by default', async () => {
const page = await newSpecPage({
components: [Toast],
@@ -41,3 +41,50 @@ describe('alert: custom html', () => {
expect(content.querySelector('button.custom-html')).toBe(null);
});
});
/**
* These tests check if the aria-hidden attributes are being
* removed on present. Without this functionality, screen readers
* would not announce toast content correctly.
*/
describe('toast: a11y smoke test', () => {
it('should have aria-hidden content when dismissed', async () => {
const page = await newSpecPage({
components: [Toast],
html: `<ion-toast message="Message" header="Header"></ion-toast>`,
});
const toast = page.body.querySelector('ion-toast');
const header = toast.shadowRoot.querySelector('.toast-header');
const message = toast.shadowRoot.querySelector('.toast-message');
expect(header.getAttribute('aria-hidden')).toBe('true');
expect(message.getAttribute('aria-hidden')).toBe('true');
});
it('should not have aria-hidden content when presented', async () => {
const page = await newSpecPage({
components: [Toast],
html: `
<ion-app>
<ion-toast animated="false" message="Message" header="Header"></ion-toast>
</ion-app>
`,
});
const toast = page.body.querySelector('ion-toast');
/**
* Wait for present method to resolve
* and for state change to take effect.
*/
await toast.present();
await page.waitForChanges();
const header = toast.shadowRoot.querySelector('.toast-header');
const message = toast.shadowRoot.querySelector('.toast-message');
expect(header.getAttribute('aria-hidden')).toBe(null);
expect(message.getAttribute('aria-hidden')).toBe(null);
});
});