Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(overlays): getTop now returns the top-most presented overlay #24547

Merged
merged 9 commits into from
Jan 11, 2022
36 changes: 11 additions & 25 deletions core/src/utils/overlays.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ export const focusFirstDescendant = (ref: Element, overlay: HTMLIonOverlayElemen
}
};

const isOverlayHidden = (overlay: Element) => overlay.classList.contains('overlay-hidden');

const focusLastDescendant = (ref: Element, overlay: HTMLIonOverlayElement) => {
const inputs = Array.from(ref.querySelectorAll(focusableQueryString)) as HTMLElement[];
let lastInput = inputs.length > 0 ? inputs[inputs.length - 1] : null;
Expand Down Expand Up @@ -291,7 +293,7 @@ export const connectListeners = (doc: Document) => {

// handle back-button click
doc.addEventListener('ionBackButton', ev => {
const lastOverlay = getTopOpenOverlay(doc);
const lastOverlay = getOverlay(doc);
if (lastOverlay && lastOverlay.backdropDismiss) {
(ev as BackButtonEvent).detail.register(OVERLAY_BACK_BUTTON_PRIORITY, () => {
return lastOverlay.dismiss(undefined, BACKDROP);
Expand All @@ -302,7 +304,7 @@ export const connectListeners = (doc: Document) => {
// handle ESC to close overlay
doc.addEventListener('keyup', ev => {
if (ev.key === 'Escape') {
const lastOverlay = getTopOpenOverlay(doc);
const lastOverlay = getOverlay(doc);
if (lastOverlay && lastOverlay.backdropDismiss) {
lastOverlay.dismiss(undefined, BACKDROP);
}
Expand All @@ -328,30 +330,14 @@ export const getOverlays = (doc: Document, selector?: string): HTMLIonOverlayEle
};

/**
* Gets the top-most/last opened
* overlay that is currently presented.
* Returns an overlay element
* @param doc The document to find the element within.
* @param overlayTag The selector for the overlay, defaults to Ionic overlay components.
* @param id The unique identifier for the overlay instance.
* @returns The overlay element or `undefined` if no overlay element is found.
*/
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);
const getOverlay = (doc: Document, overlayTag?: string, id?: string): HTMLIonOverlayElement | undefined => {
const overlays = getOverlays(doc, overlayTag).filter(o => !isOverlayHidden(o));
return (id === undefined)
? overlays[overlays.length - 1]
: overlays.find(o => o.id === id);
averyjohnston marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
186 changes: 116 additions & 70 deletions core/src/utils/test/overlays/index.html
Original file line number Diff line number Diff line change
@@ -1,44 +1,48 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Overlays</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<script type="module">
import { modalController, createAnimation } from '../../../../../dist/ionic/index.esm.js';
window.modalController = modalController;
</script>
</head>
<body>
<ion-app>
<ion-menu content-id="main-content">
<ion-content>
<ion-button onclick="openModal(event)">Open Modal</ion-button>
</ion-content>
</ion-menu>
<div class="ion-page" id="main-content">
<ion-header>
<ion-toolbar>
<ion-title>Modal - Inline</ion-title>
</ion-toolbar>
</ion-header>

<ion-content class="ion-padding">
<ion-button id="create" onclick="createModal()">Create a Modal</ion-button>
<ion-button id="present" onclick="presentHiddenModal()">Present a Hidden Modal</ion-button>
<ion-button id="create-and-present" onclick="createAndPresentModal()">Create and Present a Modal</ion-button>
<ion-button id="simulate" onclick="backButton()">Simulate Hardware Back Button</ion-button>
</ion-content>
</div>
</ion-app>

<script>
const createModal = async () => {
const div = document.createElement('div');
div.innerHTML = `

<head>
<meta charset="UTF-8">
<title>Overlays</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<script type="module">
import { modalController, createAnimation } from '../../../../../dist/ionic/index.esm.js';
window.modalController = modalController;
</script>
</head>

<body>
<ion-app>
<ion-menu content-id="main-content">
<ion-content>
<ion-button onclick="openModal(event)">Open Modal</ion-button>
</ion-content>
</ion-menu>
<div class="ion-page" id="main-content">
<ion-header>
<ion-toolbar>
<ion-title>Modal - Inline</ion-title>
</ion-toolbar>
</ion-header>

<ion-content class="ion-padding">
<ion-button id="create" onclick="createModal()">Create a Modal</ion-button>
<ion-button id="create-nested" onclick="createNestedOverlayModal()">Create Nested Overlay Modal</ion-button>
<ion-button id="present" onclick="presentHiddenModal()">Present a Hidden Modal</ion-button>
<ion-button id="create-and-present" onclick="createAndPresentModal()">Create and Present a Modal</ion-button>
<ion-button id="simulate" onclick="backButton()">Simulate Hardware Back Button</ion-button>
</ion-content>
</div>
</ion-app>

<script>
const createModal = async () => {
const div = document.createElement('div');
div.innerHTML = `
<ion-header>
<ion-toolbar>
<ion-title>Modal</ion-title>
Expand All @@ -53,46 +57,88 @@
</ion-content>
`;

const createButton = div.querySelector('ion-button#modal-create');
createButton.onclick = () => {
createModal();
}
const createButton = div.querySelector('ion-button#modal-create');
createButton.onclick = () => {
createModal();
}

const simulateButton = div.querySelector('ion-button#modal-simulate');
simulateButton.onclick = () => {
backButton();
}
const simulateButton = div.querySelector('ion-button#modal-simulate');
simulateButton.onclick = () => {
backButton();
}

const modal = await modalController.create({
component: div
});
const modal = await modalController.create({
component: div
});

return modal;
}
return modal;
}

const createAndPresentModal = async () => {
const modal = await createModal();
await modal.present();
}
const createAndPresentModal = async () => {
const modal = await createModal();
await modal.present();
}

const backButton = () => {
const ev = new CustomEvent('backbutton');
document.dispatchEvent(ev);
}

const backButton = () => {
const ev = new CustomEvent('backbutton');
document.dispatchEvent(ev);
const presentHiddenModal = () => {
const modal = document.querySelector('ion-modal.overlay-hidden');
if (modal) {
modal.present();
}
}

const createNestedOverlayModal = async () => {
const div = document.createElement('div');
div.innerHTML = `
<ion-header>
<ion-toolbar>
<ion-title>Modal</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
Modal Content

<ion-button id="modal-nested-overlay">Datetime inline modal</ion-button>
<ion-button id="dismiss-modal-nested-overlay">Dismiss nested overlay</ion-button>

<ion-modal trigger="modal-nested-overlay">
<ion-content>
<ion-datetime show-default-buttons></ion-datetime>
</ion-content>
</ion-modal>

</ion-content>
`;

const presentHiddenModal = () => {
const modal = document.querySelector('ion-modal.overlay-hidden');
if (modal) {
modal.present();
}
const dismissModalNestedOverlay = div.querySelector('ion-button#dismiss-modal-nested-overlay');
dismissModalNestedOverlay.onclick = () => {
window.modalController.getTop().then(top => {
top.dismiss();
});
}

window.Ionic = {
config: {
hardwareBackButton: true
}
const modal = await modalController.create({
component: div
});


await modal.present();

return modal;

};

window.Ionic = {
config: {
hardwareBackButton: true
}
</script>
}
</script>

</body>

</body>
</html>
26 changes: 22 additions & 4 deletions core/src/utils/test/overlays/overlays.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { newE2EPage } from '@stencil/core/testing';

test('overlays: hardware back button: should dismss a presented overlay', async () => {
test('overlays: hardware back button: should dismiss a presented overlay', async () => {
const page = await newE2EPage({ url: '/src/utils/test/overlays?ionic:_testing=true' });

const createAndPresentButton = await page.find('#create-and-present');
Expand All @@ -24,7 +24,7 @@ test('overlays: hardware back button: should dismss a presented overlay', async
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 () => {
test('overlays: hardware back button: should dismiss 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');
Expand Down Expand Up @@ -56,7 +56,7 @@ test('overlays: hardware back button: should dismss the presented overlay, even
expect(await modals[1].evaluate(node => node.classList.contains('overlay-hidden'))).toEqual(true);
});

test('overlays: Esc: should dismss a presented overlay', async () => {
test('overlays: Esc: should dismiss a presented overlay', async () => {
const page = await newE2EPage({ url: '/src/utils/test/overlays?ionic:_testing=true' });

const createAndPresentButton = await page.find('#create-and-present');
Expand All @@ -78,7 +78,7 @@ test('overlays: Esc: should dismss a presented overlay', async () => {
});


test('overlays: Esc: should dismss the presented overlay, even though another hidden modal was added last', async () => {
test('overlays: Esc: should dismiss 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');
Expand All @@ -102,3 +102,21 @@ test('overlays: Esc: should dismss the presented overlay, even though another hi

await page.waitForSelector('ion-modal#ion-overlay-1', { hidden: true });
});

test('overlays: Nested: should dismiss the top overlay', async () => {
const page = await newE2EPage({ url: '/src/utils/test/overlays?ionic:_testing=true' });

const createNestedButton = await page.find('#create-nested');

await createNestedButton.click();

const modal = await page.find('ion-modal');
expect(modal).not.toBe(null);

const dismissNestedOverlayButton = await page.find('#dismiss-modal-nested-overlay');
await dismissNestedOverlayButton.click();

const modals = await page.$$('ion-modal');
expect(modals.length).toEqual(0);

});