diff --git a/angular/src/directives/overlays/modal.ts b/angular/src/directives/overlays/modal.ts index 6f92c7f941..645a542c57 100644 --- a/angular/src/directives/overlays/modal.ts +++ b/angular/src/directives/overlays/modal.ts @@ -116,7 +116,7 @@ export class IonModal { constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { this.el = r.nativeElement; - this.el.addEventListener('willPresent', () => { + this.el.addEventListener('ionMount', () => { this.isCmpOpen = true; c.detectChanges(); }); @@ -124,7 +124,6 @@ export class IonModal { this.isCmpOpen = false; c.detectChanges(); }); - proxyOutputs(this, this.el, [ 'ionModalDidPresent', 'ionModalWillPresent', diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 1344e0d863..b61bac1a57 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -5830,6 +5830,10 @@ declare namespace LocalJSX { * Emitted before the modal has presented. */ "onIonModalWillPresent"?: (event: IonModalCustomEvent) => void; + /** + * Emitted before the modal has presented, but after the component has been mounted in the DOM. This event exists so iOS can run the entering transition properly + */ + "onIonMount"?: (event: IonModalCustomEvent) => void; /** * Emitted before the modal has dismissed. Shorthand for ionModalWillDismiss. */ diff --git a/core/src/components/modal/modal.tsx b/core/src/components/modal/modal.tsx index d1e7e6345b..816484ab7f 100644 --- a/core/src/components/modal/modal.tsx +++ b/core/src/components/modal/modal.tsx @@ -31,7 +31,7 @@ import { } from '../../utils/overlays'; import type { OverlayEventDetail } from '../../utils/overlays-interface'; import { getClassMap } from '../../utils/theme'; -import { deepReady } from '../../utils/transition'; +import { deepReady, waitForMount } from '../../utils/transition'; import { iosEnterAnimation } from './animations/ios.enter'; import { iosLeaveAnimation } from './animations/ios.leave'; @@ -316,6 +316,16 @@ export class Modal implements ComponentInterface, OverlayInterface { */ @Event({ eventName: 'didDismiss' }) didDismissShorthand!: EventEmitter; + /** + * Emitted before the modal has presented, but after the component + * has been mounted in the DOM. + * This event exists so iOS can run the entering + * transition properly + * + * @internal + */ + @Event() ionMount!: EventEmitter; + breakpointsChanged(breakpoints: number[] | undefined) { if (breakpoints !== undefined) { this.sortedBreakpoints = breakpoints.sort((a, b) => a - b); @@ -443,7 +453,30 @@ export class Modal implements ComponentInterface, OverlayInterface { const { inline, delegate } = this.getDelegate(true); this.usersElement = await attachComponent(delegate, el, this.component, ['ion-page'], this.componentProps, inline); - hasLazyBuild(el) && (await deepReady(this.usersElement)); + + this.ionMount.emit(); + + /** + * When using the lazy loaded build of Stencil, we need to wait + * for every Stencil component instance to be ready before presenting + * otherwise there can be a flash of unstyled content. With the + * custom elements bundle we need to wait for the JS framework + * mount the inner contents of the overlay otherwise WebKit may + * get the transition incorrect. + */ + if (hasLazyBuild(el)) { + await deepReady(this.usersElement); + /** + * If keepContentsMounted="true" then the + * JS Framework has already mounted the inner + * contents so there is no need to wait. + * Otherwise, we need to wait for the JS + * Framework to mount the inner contents + * of this component. + */ + } else if (!this.keepContentsMounted) { + await waitForMount(); + } writeTask(() => this.el.classList.add('show-modal')); diff --git a/core/src/components/popover/popover.tsx b/core/src/components/popover/popover.tsx index f93c6507b5..ff5779d5a2 100644 --- a/core/src/components/popover/popover.tsx +++ b/core/src/components/popover/popover.tsx @@ -10,7 +10,7 @@ import { BACKDROP, dismiss, eventMethod, focusFirstDescendant, prepareOverlay, p import type { OverlayEventDetail } from '../../utils/overlays-interface'; import { isPlatform } from '../../utils/platform'; import { getClassMap } from '../../utils/theme'; -import { deepReady } from '../../utils/transition'; +import { deepReady, waitForMount } from '../../utils/transition'; import { iosEnterAnimation } from './animations/ios.enter'; import { iosLeaveAnimation } from './animations/ios.leave'; @@ -455,7 +455,6 @@ export class Popover implements ComponentInterface, PopoverInterface { this.componentProps, inline ); - hasLazyBuild(el) && (await deepReady(this.usersElement)); if (!this.keyboardEvents) { this.configureKeyboardInteraction(); @@ -464,52 +463,50 @@ export class Popover implements ComponentInterface, PopoverInterface { this.ionMount.emit(); - return new Promise((resolve) => { + /** + * When using the lazy loaded build of Stencil, we need to wait + * for every Stencil component instance to be ready before presenting + * otherwise there can be a flash of unstyled content. With the + * custom elements bundle we need to wait for the JS framework + * mount the inner contents of the overlay otherwise WebKit may + * get the transition incorrect. + */ + if (hasLazyBuild(el)) { + await deepReady(this.usersElement); /** - * Wait two request animation frame loops before presenting the popover. - * This allows the framework implementations enough time to mount - * the popover contents, so the bounding box is set when the popover - * transition starts. - * - * On Angular and React, a single raf is enough time, but for Vue - * we need to wait two rafs. As a result we are using two rafs for - * all frameworks to ensure the popover is presented correctly. + * If keepContentsMounted="true" then the + * JS Framework has already mounted the inner + * contents so there is no need to wait. + * Otherwise, we need to wait for the JS + * Framework to mount the inner contents + * of this component. */ - raf(() => { - raf(async () => { - this.currentTransition = present( - this, - 'popoverEnter', - iosEnterAnimation, - mdEnterAnimation, - { - event: event || this.event, - size: this.size, - trigger: this.triggerEl, - reference: this.reference, - side: this.side, - align: this.alignment, - } - ); + } else if (!this.keepContentsMounted) { + await waitForMount(); + } - await this.currentTransition; - - this.currentTransition = undefined; - - /** - * If popover is nested and was - * presented using the "Right" arrow key, - * we need to move focus to the first - * descendant inside of the popover. - */ - if (this.focusDescendantOnPresent) { - focusFirstDescendant(this.el, this.el); - } - - resolve(); - }); - }); + this.currentTransition = present(this, 'popoverEnter', iosEnterAnimation, mdEnterAnimation, { + event: event || this.event, + size: this.size, + trigger: this.triggerEl, + reference: this.reference, + side: this.side, + align: this.alignment, }); + + await this.currentTransition; + + this.currentTransition = undefined; + + /** + * If popover is nested and was + * presented using the "Right" arrow key, + * we need to move focus to the first + * descendant inside of the popover. + */ + if (this.focusDescendantOnPresent) { + focusFirstDescendant(this.el, this.el); + } } /** diff --git a/core/src/utils/transition/index.ts b/core/src/utils/transition/index.ts index 28b45c65ac..5600c5cf3f 100644 --- a/core/src/utils/transition/index.ts +++ b/core/src/utils/transition/index.ts @@ -198,6 +198,23 @@ export const lifecycle = (el: HTMLElement | undefined, eventName: string) => { } }; +/** + * Wait two request animation frame loops. + * This allows the framework implementations enough time to mount + * the user-defined contents. This is often needed when using inline + * modals and popovers that accept user components. For popover, + * the contents must be mounted for the popover to be sized correctly. + * For modals, the contents must be mounted for iOS to run the + * transition correctly. + * + * On Angular and React, a single raf is enough time, but for Vue + * we need to wait two rafs. As a result we are using two rafs for + * all frameworks to ensure contents are mounted. + */ +export const waitForMount = (): Promise => { + return new Promise((resolve) => raf(() => raf(() => resolve()))); +}; + export const deepReady = async (el: any | undefined): Promise => { const element = el as any; if (element) {