diff --git a/core/src/components/datetime/test/basic/e2e.ts b/core/src/components/datetime/test/basic/e2e.ts index b9a902dd94..7a3ea3347d 100644 --- a/core/src/components/datetime/test/basic/e2e.ts +++ b/core/src/components/datetime/test/basic/e2e.ts @@ -7,14 +7,12 @@ const getActiveElementText = async (page) => { test('datetime/picker: focus trap', async () => { const page = await newE2EPage({ url: '/src/components/datetime/test/basic?ionic:_testing=true' }); - await page.click('#datetime-part'); await page.waitForSelector('#datetime-part'); let datetime = await page.find('ion-datetime'); expect(datetime).not.toBe(null); - await datetime.waitForVisible(); // TODO fix await page.waitFor(100); diff --git a/core/src/components/modal/test/basic/e2e.ts b/core/src/components/modal/test/basic/e2e.ts index ff10d74118..06ce04f06b 100644 --- a/core/src/components/modal/test/basic/e2e.ts +++ b/core/src/components/modal/test/basic/e2e.ts @@ -9,17 +9,15 @@ const getActiveElementText = async (page) => { test('modal: focus trap', async () => { const page = await newE2EPage({ url: '/src/components/modal/test/basic?ionic:_testing=true' }); + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); await page.click('#basic-modal'); await page.waitForSelector('#basic-modal'); let modal = await page.find('ion-modal'); - expect(modal).not.toBe(null); - await modal.waitForVisible(); - // TODO fix - await page.waitFor(50); + await ionModalDidPresent.next(); await page.keyboard.press('Tab'); @@ -39,6 +37,33 @@ test('modal: focus trap', async () => { expect(activeElementTextThree).toEqual('Dismiss Modal'); }); +test('modal: return focus', async () => { + const page = await newE2EPage({ url: '/src/components/modal/test/basic?ionic:_testing=true' }); + const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss'); + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + + await page.click('#basic-modal'); + await page.waitForSelector('#basic-modal'); + + let modal = await page.find('ion-modal'); + expect(modal).not.toBe(null); + + await ionModalDidPresent.next() + + await Promise.all([ + await modal.callMethod('dismiss'), + await ionModalDidDismiss.next(), + await modal.waitForNotVisible(), + ]); + + modal = await page.find('ion-modal'); + expect(modal).toBeNull(); + + const activeElement = await page.evaluateHandle(() => document.activeElement); + const id = await activeElement.evaluate((node) => node.id); + expect(id).toEqual('basic-modal'); +}); + test('modal: basic', async () => { await testModal(DIRECTORY, '#basic-modal'); }); diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index 2346a4693a..d66301085e 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -260,11 +260,47 @@ export const present = async ( overlay.didPresent.emit(); } + /** + * When an overlay that steals focus + * is dismissed, focus should be returned + * to the element that was focused + * prior to the overlay opening. Toast + * does not steal focus and is excluded + * from returning focus as a result. + */ + if (overlay.el.tagName !== 'ION-TOAST') { + focusPreviousElementOnDismiss(overlay.el); + } + if (overlay.keyboardClose) { overlay.el.focus(); } }; +/** + * When an overlay component is dismissed, + * focus should be returned to the element + * that presented the overlay. Otherwise + * focus will be set on the body which + * means that people using screen readers + * or tabbing will need to re-navigate + * to where they were before they + * opened the overlay. + */ +const focusPreviousElementOnDismiss = async (overlayEl: any) => { + let previousElement = document.activeElement as HTMLElement | null; + if (!previousElement) { return; } + + const shadowRoot = previousElement && previousElement.shadowRoot; + if (shadowRoot) { + // If there are no inner focusable elements, just focus the host element. + previousElement = shadowRoot.querySelector(innerFocusableQueryString) || previousElement; + } + + await overlayEl.onDidDismiss(); + previousElement.focus(); +} + export const dismiss = async ( overlay: OverlayInterface, data: any | undefined,