Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions angular/src/directives/overlays/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,14 @@ 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();
});
this.el.addEventListener('didDismiss', () => {
this.isCmpOpen = false;
c.detectChanges();
});

proxyOutputs(this, this.el, [
'ionModalDidPresent',
'ionModalWillPresent',
Expand Down
4 changes: 4 additions & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5830,6 +5830,10 @@ declare namespace LocalJSX {
* Emitted before the modal has presented.
*/
"onIonModalWillPresent"?: (event: IonModalCustomEvent<void>) => 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>) => void;
/**
* Emitted before the modal has dismissed. Shorthand for ionModalWillDismiss.
*/
Expand Down
37 changes: 35 additions & 2 deletions core/src/components/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -316,6 +316,16 @@ export class Modal implements ComponentInterface, OverlayInterface {
*/
@Event({ eventName: 'didDismiss' }) didDismissShorthand!: EventEmitter<OverlayEventDetail>;

/**
* 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<void>;

breakpointsChanged(breakpoints: number[] | undefined) {
if (breakpoints !== undefined) {
this.sortedBreakpoints = breakpoints.sort((a, b) => a - b);
Expand Down Expand Up @@ -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'));

Expand Down
87 changes: 42 additions & 45 deletions core/src/components/popover/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -455,7 +455,6 @@ export class Popover implements ComponentInterface, PopoverInterface {
this.componentProps,
inline
);
hasLazyBuild(el) && (await deepReady(this.usersElement));

if (!this.keyboardEvents) {
this.configureKeyboardInteraction();
Expand All @@ -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<PopoverPresentOptions>(
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);
}

resolve();
});
});
} else if (!this.keepContentsMounted) {
await waitForMount();
}

this.currentTransition = present<PopoverPresentOptions>(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);
}
}

/**
Expand Down
17 changes: 17 additions & 0 deletions core/src/utils/transition/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> => {
return new Promise((resolve) => raf(() => raf(() => resolve())));
};

export const deepReady = async (el: any | undefined): Promise<void> => {
const element = el as any;
if (element) {
Expand Down