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:
Liam DeBeasi
2021-11-08 10:19:23 -05:00
committed by GitHub
parent b3759aed5b
commit da339a8a74
11 changed files with 157 additions and 16 deletions

View File

@@ -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.

View File

@@ -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.
*/

View File

@@ -4,7 +4,7 @@
// iOS Modals
// --------------------------------------------------
:host(:first-of-type) {
:host {
--backdrop-opacity: var(--ion-backdrop-opacity, 0.4);
}

View File

@@ -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;
}

View File

@@ -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 }

View File

@@ -168,6 +168,9 @@
async function presentModal(options) {
const modal = createModal(options);
await modal.present();
await modal.onDidDismiss();
modal.remove();
}
async function presentCardModal() {

View File

@@ -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 }

View File

@@ -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

View File

@@ -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);

View File

@@ -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();
});

View 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>