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 () => {