diff --git a/core/src/components/action-sheet/action-sheet.tsx b/core/src/components/action-sheet/action-sheet.tsx index 778369864f..e16754c9e3 100644 --- a/core/src/components/action-sheet/action-sheet.tsx +++ b/core/src/components/action-sheet/action-sheet.tsx @@ -252,6 +252,7 @@ export class ActionSheet implements ComponentInterface, OverlayInterface { [mode]: true, ...getClassMap(this.cssClass), + 'overlay-hidden': true, 'action-sheet-translucent': this.translucent }} onIonActionSheetWillDismiss={this.dispatchCancelHandler} diff --git a/core/src/components/alert/alert.tsx b/core/src/components/alert/alert.tsx index fdaa264bd8..3ce650238f 100644 --- a/core/src/components/alert/alert.tsx +++ b/core/src/components/alert/alert.tsx @@ -575,6 +575,7 @@ export class Alert implements ComponentInterface, OverlayInterface { class={{ ...getClassMap(this.cssClass), [mode]: true, + 'overlay-hidden': true, 'alert-translucent': this.translucent }} onIonAlertWillDismiss={this.dispatchCancelHandler} diff --git a/core/src/components/loading/loading.tsx b/core/src/components/loading/loading.tsx index 1a497bc9c7..6b9d1ec9ef 100644 --- a/core/src/components/loading/loading.tsx +++ b/core/src/components/loading/loading.tsx @@ -197,6 +197,7 @@ export class Loading implements ComponentInterface, OverlayInterface { class={{ ...getClassMap(this.cssClass), [mode]: true, + 'overlay-hidden': true, 'loading-translucent': this.translucent }} > diff --git a/core/src/components/picker/picker.tsx b/core/src/components/picker/picker.tsx index 5d64e75ab4..e7bf39a906 100644 --- a/core/src/components/picker/picker.tsx +++ b/core/src/components/picker/picker.tsx @@ -233,7 +233,7 @@ export class Picker implements ComponentInterface, OverlayInterface { // Used internally for styling [`picker-${mode}`]: true, - + 'overlay-hidden': true, ...getClassMap(this.cssClass) }} onIonBackdropTap={this.onBackdropTap} diff --git a/core/src/components/toast/toast.tsx b/core/src/components/toast/toast.tsx index 065fa03e72..6d519a35f4 100644 --- a/core/src/components/toast/toast.tsx +++ b/core/src/components/toast/toast.tsx @@ -289,6 +289,7 @@ export class Toast implements ComponentInterface, OverlayInterface { class={createColorClasses(this.color, { [mode]: true, ...getClassMap(this.cssClass), + 'overlay-hidden': true, 'toast-translucent': this.translucent })} onIonToastWillDismiss={this.dispatchCancelHandler} diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index 544d533f4c..cc21b3bb62 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -256,7 +256,7 @@ export const connectListeners = (doc: Document) => { // handle back-button click doc.addEventListener('ionBackButton', ev => { - const lastOverlay = getOverlay(doc); + const lastOverlay = getTopOpenOverlay(doc); if (lastOverlay && lastOverlay.backdropDismiss) { (ev as BackButtonEvent).detail.register(OVERLAY_BACK_BUTTON_PRIORITY, () => { return lastOverlay.dismiss(undefined, BACKDROP); @@ -267,7 +267,7 @@ export const connectListeners = (doc: Document) => { // handle ESC to close overlay doc.addEventListener('keyup', ev => { if (ev.key === 'Escape') { - const lastOverlay = getOverlay(doc); + const lastOverlay = getTopOpenOverlay(doc); if (lastOverlay && lastOverlay.backdropDismiss) { lastOverlay.dismiss(undefined, BACKDROP); } @@ -292,6 +292,29 @@ export const getOverlays = (doc: Document, selector?: string): HTMLIonOverlayEle .filter(c => c.overlayIndex > 0); }; +/** + * Gets the top-most/last opened + * overlay that is currently presented. + */ +const getTopOpenOverlay = (doc: Document): HTMLIonOverlayElement | undefined => { + const overlays = getOverlays(doc); + for (let i = overlays.length - 1; i >= 0; i--) { + const overlay = overlays[i]; + + /** + * Only consider overlays that + * are presented. Presented overlays + * will not have the .overlay-hidden + * class on the host. + */ + if (!overlay.classList.contains('overlay-hidden')) { + return overlay; + } + } + + return; +} + export const getOverlay = (doc: Document, overlayTag?: string, id?: string): HTMLIonOverlayElement | undefined => { const overlays = getOverlays(doc, overlayTag); return (id === undefined) diff --git a/core/src/utils/test/overlays/index.html b/core/src/utils/test/overlays/index.html new file mode 100644 index 0000000000..abba3376a9 --- /dev/null +++ b/core/src/utils/test/overlays/index.html @@ -0,0 +1,98 @@ + + + + + Overlays + + + + + + + + + + + Open Modal + + +
+ + + Modal - Inline + + + + + Create a Modal + Present a Hidden Modal + Create and Present a Modal + Simulate Hardware Back Button + +
+
+ + + + + diff --git a/core/src/utils/test/overlays/overlays.e2e.ts b/core/src/utils/test/overlays/overlays.e2e.ts new file mode 100644 index 0000000000..058968722c --- /dev/null +++ b/core/src/utils/test/overlays/overlays.e2e.ts @@ -0,0 +1,104 @@ +import { newE2EPage } from '@stencil/core/testing'; + +test('overlays: hardware back button: should dismss a presented overlay', async () => { + const page = await newE2EPage({ url: '/src/utils/test/overlays?ionic:_testing=true' }); + + const createAndPresentButton = await page.find('#create-and-present'); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss'); + + await createAndPresentButton.click() + const modal = await page.find('ion-modal'); + expect(modal).not.toBe(null); + + await ionModalDidPresent.next(); + + const simulateButton = await modal.find('#modal-simulate'); + expect(simulateButton).not.toBe(null); + + await simulateButton.click(); + + await ionModalDidDismiss.next(); + + await page.waitForSelector('ion-modal', { hidden: true }) +}); + +test('overlays: hardware back button: should dismss the presented overlay, even though another hidden modal was added last', async () => { + const page = await newE2EPage({ url: '/src/utils/test/overlays?ionic:_testing=true' }); + + const createAndPresentButton = await page.find('#create-and-present'); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss'); + + await createAndPresentButton.click(); + const modal = await page.find('ion-modal'); + expect(modal).not.toBe(null); + + await ionModalDidPresent.next(); + + const createButton = await page.find('#modal-create'); + await createButton.click(); + + const modals = await page.$$('ion-modal'); + expect(modals.length).toEqual(2); + + expect(await modals[0].evaluate(node => node.classList.contains('overlay-hidden'))).toEqual(false); + expect(await modals[1].evaluate(node => node.classList.contains('overlay-hidden'))).toEqual(true); + + const simulateButton = await modal.find('#modal-simulate'); + expect(simulateButton).not.toBe(null); + + await simulateButton.click(); + + expect(await modals[0].evaluate(node => node.classList.contains('overlay-hidden'))).toEqual(true); + expect(await modals[1].evaluate(node => node.classList.contains('overlay-hidden'))).toEqual(true); +}); + +test('overlays: Esc: should dismss a presented overlay', async () => { + const page = await newE2EPage({ url: '/src/utils/test/overlays?ionic:_testing=true' }); + + const createAndPresentButton = await page.find('#create-and-present'); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss'); + + await createAndPresentButton.click() + const modal = await page.find('ion-modal'); + expect(modal).not.toBe(null); + + await ionModalDidPresent.next(); + + await page.keyboard.press('Escape'); + + await ionModalDidDismiss.next(); + + await page.waitForSelector('ion-modal', { hidden: true }) +}); + + +test('overlays: Esc: should dismss the presented overlay, even though another hidden modal was added last', async () => { + const page = await newE2EPage({ url: '/src/utils/test/overlays?ionic:_testing=true' }); + + const createAndPresentButton = await page.find('#create-and-present'); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss'); + + await createAndPresentButton.click(); + const modal = await page.find('ion-modal'); + expect(modal).not.toBe(null); + + await ionModalDidPresent.next(); + + const createButton = await page.find('#modal-create'); + await createButton.click(); + + const modals = await page.$$('ion-modal'); + expect(modals.length).toEqual(2); + + await page.keyboard.press('Escape'); + + await page.waitForSelector('ion-modal#ion-overlay-1', { hidden: true }); +}); diff --git a/core/src/utils/test/overlays.spec.ts b/core/src/utils/test/overlays/overlays.spec.ts similarity index 92% rename from core/src/utils/test/overlays.spec.ts rename to core/src/utils/test/overlays/overlays.spec.ts index ec32ee9e94..cc83700439 100644 --- a/core/src/utils/test/overlays.spec.ts +++ b/core/src/utils/test/overlays/overlays.spec.ts @@ -1,7 +1,7 @@ import { newSpecPage } from '@stencil/core/testing'; -import { setRootAriaHidden } from '../overlays'; -import { RouterOutlet } from '../../components/router-outlet/route-outlet'; -import { Nav } from '../../components/nav/nav'; +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 () => {