diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index 7e2395d341..bf8192e59e 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -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 { diff --git a/core/src/utils/test/overlays.spec.ts b/core/src/utils/test/overlays.spec.ts new file mode 100644 index 0000000000..ec32ee9e94 --- /dev/null +++ b/core/src/utils/test/overlays.spec.ts @@ -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: ` + + ` + }); + + 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: ` + + ` + }); + + 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: ` +
+
+ ` + }); + + 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: ` +
+ ` + }); + + setRootAriaHidden(true); + }); +});