mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 19:57:22 +08:00
fix(overlays): declarative modals now work properly with the hardware back button (#24165)
This commit is contained in:
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -197,6 +197,7 @@ export class Loading implements ComponentInterface, OverlayInterface {
|
||||
class={{
|
||||
...getClassMap(this.cssClass),
|
||||
[mode]: true,
|
||||
'overlay-hidden': true,
|
||||
'loading-translucent': this.translucent
|
||||
}}
|
||||
>
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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)
|
||||
|
98
core/src/utils/test/overlays/index.html
Normal file
98
core/src/utils/test/overlays/index.html
Normal file
@ -0,0 +1,98 @@
|
||||
<!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 = `
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Modal</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">
|
||||
Modal Content
|
||||
|
||||
<ion-button id="modal-create">Create a Modal</ion-button>
|
||||
<ion-button id="modal-simulate">Simulate Hardware Back Button</ion-button>
|
||||
|
||||
</ion-content>
|
||||
`;
|
||||
|
||||
const createButton = div.querySelector('ion-button#modal-create');
|
||||
createButton.onclick = () => {
|
||||
createModal();
|
||||
}
|
||||
|
||||
const simulateButton = div.querySelector('ion-button#modal-simulate');
|
||||
simulateButton.onclick = () => {
|
||||
backButton();
|
||||
}
|
||||
|
||||
const modal = await modalController.create({
|
||||
component: div
|
||||
});
|
||||
|
||||
return modal;
|
||||
}
|
||||
|
||||
const createAndPresentModal = async () => {
|
||||
const modal = await createModal();
|
||||
await modal.present();
|
||||
}
|
||||
|
||||
const backButton = () => {
|
||||
const ev = new CustomEvent('backbutton');
|
||||
document.dispatchEvent(ev);
|
||||
}
|
||||
|
||||
const presentHiddenModal = () => {
|
||||
const modal = document.querySelector('ion-modal.overlay-hidden');
|
||||
if (modal) {
|
||||
modal.present();
|
||||
}
|
||||
}
|
||||
|
||||
window.Ionic = {
|
||||
config: {
|
||||
hardwareBackButton: true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
104
core/src/utils/test/overlays/overlays.e2e.ts
Normal file
104
core/src/utils/test/overlays/overlays.e2e.ts
Normal file
@ -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 });
|
||||
});
|
@ -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 () => {
|
Reference in New Issue
Block a user