mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
fix(modal, popover): opening modal and popover now works even if overlay was added to ion-app directly (#24174)
resolves #23728
This commit is contained in:
@@ -101,12 +101,16 @@ Converted `ion-modal` to use [Shadow DOM](https://developer.mozilla.org/en-US/do
|
||||
|
||||
If you were targeting the internals of `ion-modal` in your CSS, you will need to target the `backdrop` or `content` [Shadow Parts](https://ionicframework.com/docs/theming/css-shadow-parts) instead, or use the provided CSS Variables.
|
||||
|
||||
Developers dynamically creating modals using `document.createElement('ion-modal')` will now need to call `modal.remove()` after the modal has been dismissed if they want the modal to be removed from the DOM.
|
||||
|
||||
#### Popover
|
||||
|
||||
Converted `ion-popover` to use [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM).
|
||||
|
||||
If you were targeting the internals of `ion-popover` in your CSS, you will need to target the `backdrop`, `arrow`, or `content` [Shadow Parts](https://ionicframework.com/docs/theming/css-shadow-parts) instead, or use the provided CSS Variables.
|
||||
|
||||
Developers dynamically creating popovers using `document.createElement('ion-popover')` will now need to call `popover.remove()` after the popover has been dismissed if they want the popover to be removed from the DOM.
|
||||
|
||||
#### Radio
|
||||
|
||||
The `RadioChangeEventDetail` interface has been removed. Instead, listen for the `ionChange` event on `ion-radio-group` and use the `RadioGroupChangeEventDetail` interface.
|
||||
|
||||
4
core/src/components.d.ts
vendored
4
core/src/components.d.ts
vendored
@@ -1532,6 +1532,7 @@ export namespace Components {
|
||||
* The horizontal line that displays at the top of a sheet modal. It is `true` by default when setting the `breakpoints` and `initialBreakpoint` properties.
|
||||
*/
|
||||
"handle"?: boolean;
|
||||
"hasController": boolean;
|
||||
/**
|
||||
* Additional attributes to pass to the modal.
|
||||
*/
|
||||
@@ -1883,6 +1884,7 @@ export namespace Components {
|
||||
*/
|
||||
"event": any;
|
||||
"getParentPopover": () => Promise<HTMLIonPopoverElement | null>;
|
||||
"hasController": boolean;
|
||||
/**
|
||||
* Additional attributes to pass to the popover.
|
||||
*/
|
||||
@@ -5220,6 +5222,7 @@ declare namespace LocalJSX {
|
||||
* The horizontal line that displays at the top of a sheet modal. It is `true` by default when setting the `breakpoints` and `initialBreakpoint` properties.
|
||||
*/
|
||||
"handle"?: boolean;
|
||||
"hasController"?: boolean;
|
||||
/**
|
||||
* Additional attributes to pass to the modal.
|
||||
*/
|
||||
@@ -5509,6 +5512,7 @@ declare namespace LocalJSX {
|
||||
* The event to pass to the popover animation.
|
||||
*/
|
||||
"event"?: any;
|
||||
"hasController"?: boolean;
|
||||
/**
|
||||
* Additional attributes to pass to the popover.
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// iOS Modals
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(:first-of-type) {
|
||||
:host {
|
||||
--backdrop-opacity: var(--ion-backdrop-opacity, 0.4);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,16 +5,13 @@
|
||||
// Material Design Modals
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(:first-of-type) {
|
||||
:host {
|
||||
--backdrop-opacity: var(--ion-backdrop-opacity, 0.32);
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $modal-inset-min-width) and (min-height: $modal-inset-min-height-small) {
|
||||
:host {
|
||||
--border-radius: 2px;
|
||||
}
|
||||
|
||||
:host(:first-of-type) {
|
||||
--box-shadow: #{$modal-inset-box-shadow};
|
||||
}
|
||||
}
|
||||
@@ -24,3 +21,4 @@
|
||||
|
||||
opacity: .01;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,9 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
|
||||
@Element() el!: HTMLIonModalElement;
|
||||
|
||||
/** @internal */
|
||||
@Prop() hasController = false;
|
||||
|
||||
/** @internal */
|
||||
@Prop() overlayIndex!: number;
|
||||
|
||||
@@ -322,13 +325,13 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
* If using overlay inline
|
||||
* we potentially need to use the coreDelegate
|
||||
* so that this works in vanilla JS apps.
|
||||
* If a user has already placed the overlay
|
||||
* as a direct descendant of ion-app or
|
||||
* the body, then we can assume that
|
||||
* the overlay is already in the correct place.
|
||||
* If a developer has presented this component
|
||||
* via a controller, then we can assume
|
||||
* the component is already in the
|
||||
* correct place.
|
||||
*/
|
||||
const parentEl = this.el.parentNode as HTMLElement | null;
|
||||
const inline = this.inline = parentEl !== null && parentEl.tagName !== 'ION-APP' && parentEl.tagName !== 'BODY';
|
||||
const inline = this.inline = parentEl !== null && !this.hasController;
|
||||
const delegate = this.workingDelegate = (inline) ? this.delegate || this.coreDelegate : this.delegate
|
||||
|
||||
return { inline, delegate }
|
||||
|
||||
@@ -168,6 +168,9 @@
|
||||
async function presentModal(options) {
|
||||
const modal = createModal(options);
|
||||
await modal.present();
|
||||
|
||||
await modal.onDidDismiss();
|
||||
modal.remove();
|
||||
}
|
||||
|
||||
async function presentCardModal() {
|
||||
|
||||
@@ -56,6 +56,9 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
||||
|
||||
@Element() el!: HTMLIonPopoverElement;
|
||||
|
||||
/** @internal */
|
||||
@Prop() hasController = false;
|
||||
|
||||
/** @internal */
|
||||
@Prop() delegate?: FrameworkDelegate;
|
||||
|
||||
@@ -337,13 +340,13 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
||||
* If using overlay inline
|
||||
* we potentially need to use the coreDelegate
|
||||
* so that this works in vanilla JS apps.
|
||||
* If a user has already placed the overlay
|
||||
* as a direct descendant of ion-app or
|
||||
* the body, then we can assume that
|
||||
* the overlay is already in the correct place.
|
||||
* If a developer has presented this component
|
||||
* via a controller, then we can assume
|
||||
* the component is already in the
|
||||
* correct place.
|
||||
*/
|
||||
const parentEl = this.el.parentNode as HTMLElement | null;
|
||||
const inline = this.inline = parentEl !== null && parentEl.tagName !== 'ION-APP' && parentEl.tagName !== 'BODY';
|
||||
const inline = this.inline = parentEl !== null && !this.hasController;
|
||||
const delegate = this.workingDelegate = (inline) ? this.delegate || this.coreDelegate : this.delegate
|
||||
|
||||
return { inline, delegate }
|
||||
|
||||
@@ -86,6 +86,30 @@ html.ios ion-modal .ion-page {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subsequent modals should not have a backdrop/box shadow
|
||||
* as it will cause the screen to appear to get progressively
|
||||
* darker. With Ionic 6, declarative modals made it
|
||||
* possible to have multiple non-presented modals in the DOM,
|
||||
* so we could no longer rely on ion-modal:first-of-type.
|
||||
* Here we disable the opacity/box-shadow for every modal
|
||||
* that comes after the first presented modal.
|
||||
*
|
||||
* Note: ion-modal:not(.overlay-hidden):first-of-type
|
||||
* does not match the first modal to not have
|
||||
* the .overlay-hidden class, it will match the
|
||||
* first modal in general only if it does not
|
||||
* have the .overlay-hidden class.
|
||||
* The :nth-child() pseudo-class has support
|
||||
* for selectors which would help us here. At the
|
||||
* time of writing it does not have great cross browser
|
||||
* support.
|
||||
*/
|
||||
ion-modal:not(.overlay-hidden) ~ ion-modal {
|
||||
--backdrop-opacity: 0;
|
||||
--box-shadow: none;
|
||||
}
|
||||
|
||||
// Ionic Colors
|
||||
// --------------------------------------------------
|
||||
// Generates the color classes and variables based on the
|
||||
|
||||
@@ -54,7 +54,7 @@ export const createOverlay = <T extends HTMLIonOverlayElement>(tagName: string,
|
||||
* Convert the passed in overlay options into props
|
||||
* that get passed down into the new overlay.
|
||||
*/
|
||||
Object.assign(element, { ...opts });
|
||||
Object.assign(element, { ...opts, hasController: true });
|
||||
|
||||
// append the overlay element to the document body
|
||||
getAppRoot(document).appendChild(element);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('framework-delegate: should present modal already at ion-app root', async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/test/framework-delegate?ionic:_testing=true' });
|
||||
|
||||
const button = await page.find('#button-inline-root');
|
||||
await button.click();
|
||||
|
||||
const modal = await page.find('#inline-root');
|
||||
expect(modal).not.toBe(null);
|
||||
await modal.waitForVisible();
|
||||
|
||||
});
|
||||
|
||||
test('framework-delegate: should present modal in content', async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/test/framework-delegate?ionic:_testing=true' });
|
||||
|
||||
const button = await page.find('#button-inline-content');
|
||||
await button.click();
|
||||
|
||||
const modal = await page.find('#inline-content');
|
||||
expect(modal).not.toBe(null);
|
||||
await modal.waitForVisible();
|
||||
});
|
||||
|
||||
test('framework-delegate: should present modal via controller', async () => {
|
||||
const page = await newE2EPage({ url: '/src/utils/test/framework-delegate?ionic:_testing=true' });
|
||||
|
||||
const button = await page.find('#button-controller');
|
||||
await button.click();
|
||||
|
||||
const modal = await page.find('#controller');
|
||||
expect(modal).not.toBe(null);
|
||||
await modal.waitForVisible();
|
||||
});
|
||||
67
core/src/utils/test/framework-delegate/index.html
Normal file
67
core/src/utils/test/framework-delegate/index.html
Normal file
@@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Framework Delegate</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-modal id="inline-root" trigger="button-inline-root">
|
||||
<ion-content class="ion-padding">
|
||||
Hello World
|
||||
</ion-content>
|
||||
</ion-modal>
|
||||
<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="button-inline-root">Open inline modal at ion-app root</ion-button>
|
||||
<ion-button id="button-inline-content">Open inline modal inside of content</ion-button>
|
||||
<ion-button id="button-controller" onclick="createAndPresentModal()">Open controller modal</ion-button>
|
||||
|
||||
<ion-modal id="inline-content" trigger="button-inline-content">
|
||||
<ion-content class="ion-padding">
|
||||
Hello World
|
||||
</ion-content>
|
||||
</ion-modal>
|
||||
</ion-content>
|
||||
</div>
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
const createModal = async () => {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = `
|
||||
<ion-content class="ion-padding">
|
||||
Hello World
|
||||
</ion-content>
|
||||
`;
|
||||
|
||||
const modal = await modalController.create({
|
||||
component: div,
|
||||
id: 'controller'
|
||||
});
|
||||
|
||||
return modal;
|
||||
}
|
||||
|
||||
const createAndPresentModal = async () => {
|
||||
const modal = await createModal();
|
||||
await modal.present();
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user