octicon-rss(16/)
You've already forked ionic-framework
mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-10 22:44:13 +08:00
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/)
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
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user