mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 19:57:22 +08:00
fix(overlays): screen readers no longer read content behind overlays (#23284)
resolves #22714
This commit is contained in:
@ -236,6 +236,41 @@ export const getOverlay = (doc: Document, overlayTag?: string, id?: string): HTM
|
||||
: overlays.find(o => o.id === id);
|
||||
};
|
||||
|
||||
/**
|
||||
* When an overlay is presented, the main
|
||||
* focus is the overlay not the page content.
|
||||
* We need to remove the page content from the
|
||||
* accessibility tree otherwise when
|
||||
* users use "read screen from top" gestures with
|
||||
* TalkBack and VoiceOver, the screen reader will begin
|
||||
* to read the content underneath the overlay.
|
||||
*
|
||||
* We need a container where all page components
|
||||
* exist that is separate from where the overlays
|
||||
* are added in the DOM. For most apps, this element
|
||||
* is the top most ion-router-outlet. In the event
|
||||
* that devs are not using a router,
|
||||
* they will need to add the "ion-view-container-root"
|
||||
* id to the element that contains all of their views.
|
||||
*
|
||||
* TODO: If Framework supports having multiple top
|
||||
* level router outlets we would need to update this.
|
||||
* Example: One outlet for side menu and one outlet
|
||||
* for main content.
|
||||
*/
|
||||
export const setRootAriaHidden = (hidden = false) => {
|
||||
const root = getAppRoot(document);
|
||||
const viewContainer = root.querySelector('ion-router-outlet, ion-nav, #ion-view-container-root');
|
||||
|
||||
if (!viewContainer) { return; }
|
||||
|
||||
if (hidden) {
|
||||
viewContainer.setAttribute('aria-hidden', 'true');
|
||||
} else {
|
||||
viewContainer.removeAttribute('aria-hidden');
|
||||
}
|
||||
}
|
||||
|
||||
export const present = async (
|
||||
overlay: OverlayInterface,
|
||||
name: keyof IonicConfig,
|
||||
@ -246,6 +281,9 @@ export const present = async (
|
||||
if (overlay.presented) {
|
||||
return;
|
||||
}
|
||||
|
||||
setRootAriaHidden(true);
|
||||
|
||||
overlay.presented = true;
|
||||
overlay.willPresent.emit();
|
||||
|
||||
@ -313,6 +351,9 @@ export const dismiss = async (
|
||||
if (!overlay.presented) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setRootAriaHidden(false);
|
||||
|
||||
overlay.presented = false;
|
||||
|
||||
try {
|
||||
|
79
core/src/utils/test/overlays.spec.ts
Normal file
79
core/src/utils/test/overlays.spec.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
import { setRootAriaHidden } from '../overlays';
|
||||
import { RouterOutlet } from '../../components/router-outlet/route-outlet';
|
||||
import { Nav } from '../../components/nav/nav';
|
||||
|
||||
describe('setRootAriaHidden()', () => {
|
||||
it('should correctly remove and re-add router outlet from accessibility tree', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [RouterOutlet],
|
||||
html: `
|
||||
<ion-router-outlet></ion-router-outlet>
|
||||
`
|
||||
});
|
||||
|
||||
const routerOutlet = page.body.querySelector('ion-router-outlet');
|
||||
|
||||
expect(routerOutlet.hasAttribute('aria-hidden')).toEqual(false);
|
||||
|
||||
setRootAriaHidden(true);
|
||||
expect(routerOutlet.hasAttribute('aria-hidden')).toEqual(true);
|
||||
|
||||
setRootAriaHidden(false);
|
||||
expect(routerOutlet.hasAttribute('aria-hidden')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should correctly remove and re-add nav from accessibility tree', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Nav],
|
||||
html: `
|
||||
<ion-nav></ion-nav>
|
||||
`
|
||||
});
|
||||
|
||||
const nav = page.body.querySelector('ion-nav');
|
||||
|
||||
expect(nav.hasAttribute('aria-hidden')).toEqual(false);
|
||||
|
||||
setRootAriaHidden(true);
|
||||
expect(nav.hasAttribute('aria-hidden')).toEqual(true);
|
||||
|
||||
setRootAriaHidden(false);
|
||||
expect(nav.hasAttribute('aria-hidden')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should correctly remove and re-add custom container from accessibility tree', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [],
|
||||
html: `
|
||||
<div id="ion-view-container-root"></div>
|
||||
<div id="not-container-root"></div>
|
||||
`
|
||||
});
|
||||
|
||||
const containerRoot = page.body.querySelector('#ion-view-container-root');
|
||||
const notContainerRoot = page.body.querySelector('#not-container-root');
|
||||
|
||||
expect(containerRoot.hasAttribute('aria-hidden')).toEqual(false);
|
||||
expect(notContainerRoot.hasAttribute('aria-hidden')).toEqual(false);
|
||||
|
||||
setRootAriaHidden(true);
|
||||
expect(containerRoot.hasAttribute('aria-hidden')).toEqual(true);
|
||||
expect(notContainerRoot.hasAttribute('aria-hidden')).toEqual(false);
|
||||
|
||||
setRootAriaHidden(false);
|
||||
expect(containerRoot.hasAttribute('aria-hidden')).toEqual(false);
|
||||
expect(notContainerRoot.hasAttribute('aria-hidden')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should not error if router outlet was not found', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [],
|
||||
html: `
|
||||
<div></div>
|
||||
`
|
||||
});
|
||||
|
||||
setRootAriaHidden(true);
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user