feat(modal): add card-style presentation with swipe to close gesture (#19428)

resolves #18660
This commit is contained in:
Manu MA
2019-12-10 22:02:41 +01:00
committed by Liam DeBeasi
parent 56f67bd9a5
commit b3b3312711
29 changed files with 994 additions and 159 deletions

View File

@ -16,6 +16,8 @@ import { RouteView, getUrl } from './stack-utils';
inputs: ['animated', 'swipeGesture']
})
export class IonRouterOutlet implements OnDestroy, OnInit {
nativeEl: HTMLIonRouterOutletElement;
private activated: ComponentRef<any> | null = null;
private activatedView: RouteView | null = null;
@ -23,7 +25,6 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
private _swipeGesture?: boolean;
private name: string;
private stackCtrl: StackController;
private nativeEl: HTMLIonRouterOutletElement;
// Maintain map of activated route proxies for each component instance
private proxyMap = new WeakMap<any, ActivatedRoute>();

View File

@ -683,7 +683,9 @@ ion-modal,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefin
ion-modal,prop,keyboardClose,boolean,true,false,false
ion-modal,prop,leaveAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
ion-modal,prop,mode,"ios" | "md",undefined,false,false
ion-modal,prop,presentingElement,HTMLElement | undefined,undefined,false,false
ion-modal,prop,showBackdrop,boolean,true,false,false
ion-modal,prop,swipeToClose,boolean,false,false,false
ion-modal,method,dismiss,dismiss(data?: any, role?: string | undefined) => Promise<boolean>
ion-modal,method,onDidDismiss,onDidDismiss() => Promise<OverlayEventDetail<any>>
ion-modal,method,onWillDismiss,onWillDismiss() => Promise<OverlayEventDetail<any>>

View File

@ -1500,9 +1500,17 @@ export namespace Components {
*/
'present': () => Promise<void>;
/**
* The element that presented the modal. This is used for card presentation effects and for stacking multiple modals on top of each other. Only applies in iOS mode.
*/
'presentingElement'?: HTMLElement;
/**
* If `true`, a backdrop will be displayed behind the modal.
*/
'showBackdrop': boolean;
/**
* If `true`, the modal can be swiped to dismiss. Only applies in iOS mode.
*/
'swipeToClose': boolean;
}
interface IonModalController {
/**
@ -4853,9 +4861,17 @@ declare namespace LocalJSX {
*/
'onIonModalWillPresent'?: (event: CustomEvent<void>) => void;
/**
* The element that presented the modal. This is used for card presentation effects and for stacking multiple modals on top of each other. Only applies in iOS mode.
*/
'presentingElement'?: HTMLElement;
/**
* If `true`, a backdrop will be displayed behind the modal.
*/
'showBackdrop'?: boolean;
/**
* If `true`, the modal can be swiped to dismiss. Only applies in iOS mode.
*/
'swipeToClose'?: boolean;
}
interface IonModalController {}
interface IonNav {

View File

@ -1,7 +1,7 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, Watch, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { AlertButton, AlertInput, Animation, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface } from '../../interface';
import { AlertButton, AlertInput, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface } from '../../interface';
import { BACKDROP, dismiss, eventMethod, isCancel, prepareOverlay, present, safeCall } from '../../utils/overlays';
import { sanitizeDOMString } from '../../utils/sanitization';
import { getClassMap } from '../../utils/theme';
@ -30,7 +30,6 @@ export class Alert implements ComponentInterface, OverlayInterface {
private processedButtons: AlertButton[] = [];
presented = false;
animation?: Animation;
mode = getIonMode(this);
@Element() el!: HTMLIonAlertElement;

View File

@ -120,6 +120,8 @@
}
.transition-effect {
display: none;
position: absolute;
/* stylelint-disable property-blacklist */

View File

@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Meth
import { config } from '../../global/config';
import { getIonMode } from '../../global/ionic-global';
import { Animation, AnimationBuilder, OverlayEventDetail, OverlayInterface, SpinnerTypes } from '../../interface';
import { AnimationBuilder, OverlayEventDetail, OverlayInterface, SpinnerTypes } from '../../interface';
import { BACKDROP, dismiss, eventMethod, prepareOverlay, present } from '../../utils/overlays';
import { sanitizeDOMString } from '../../utils/sanitization';
import { getClassMap } from '../../utils/theme';
@ -27,7 +27,6 @@ export class Loading implements ComponentInterface, OverlayInterface {
private durationTimeout: any;
presented = false;
animation?: Animation;
mode = getIonMode(this);
@Element() el!: HTMLIonLoadingElement;

View File

@ -1,90 +1,53 @@
import { Animation } from '../../../interface';
import { createAnimation } from '../../../utils/animation/animation';
import { SwipeToCloseDefaults } from '../gestures/swipe-to-close';
/**
* iOS Modal Enter Animation
* iOS Modal Enter Animation for the Card presentation style
*/
export const iosEnterAnimation = (baseEl: HTMLElement): Animation => {
const baseAnimation = createAnimation();
const backdropAnimation = createAnimation();
const wrapperAnimation = createAnimation();
backdropAnimation
export const iosEnterAnimation = (
baseEl: HTMLElement,
presentingEl?: HTMLElement,
): Animation => {
// The top translate Y for the presenting element
const backdropAnimation = createAnimation()
.addElement(baseEl.querySelector('ion-backdrop')!)
.fromTo('opacity', 0.01, 'var(--backdrop-opacity)');
wrapperAnimation
const wrapperAnimation = createAnimation()
.addElement(baseEl.querySelector('.modal-wrapper')!)
.beforeStyles({ 'opacity': 1 })
.fromTo('transform', 'translateY(100%)', 'translateY(0%)');
return baseAnimation
const baseAnimation = createAnimation()
.addElement(baseEl)
.easing('cubic-bezier(0.36,0.66,0.04,1)')
.duration(400)
.easing('cubic-bezier(0.32,0.72,0,1)')
.duration(500)
.beforeAddClass('show-modal')
.addAnimation([backdropAnimation, wrapperAnimation]);
if (presentingEl) {
const modalTransform = (presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined) ? 40 : 0;
const bodyEl = document.body;
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
const finalTransform = `translateY(${-modalTransform}px) scale(${toPresentingScale})`;
const presentingAnimation = createAnimation()
.beforeStyles({
'transform': 'translateY(0)'
})
.afterStyles({
'transform': finalTransform
})
.beforeAddWrite(() => bodyEl.style.setProperty('background-color', 'black'))
.addElement(presentingEl)
.keyframes([
{ offset: 0, transform: 'translateY(0px) scale(1)', 'border-radius': '0px' },
{ offset: 1, transform: finalTransform, 'border-radius': '10px 10px 0 0' }
]);
baseAnimation.addAnimation(presentingAnimation);
}
return baseAnimation;
};
/**
* Animations for modals
*/
// export function modalSlideIn(rootEl: HTMLElement) {
// }
// export class ModalSlideOut {
// constructor(el: HTMLElement) {
// let backdrop = new Animation(this.plt, el.querySelector('ion-backdrop'));
// let wrapperEle = <HTMLElement>el.querySelector('.modal-wrapper');
// let wrapperEleRect = wrapperEle.getBoundingClientRect();
// let wrapper = new Animation(this.plt, wrapperEle);
// // height of the screen - top of the container tells us how much to scoot it down
// // so it's off-screen
// wrapper.fromTo('translateY', '0px', `${this.plt.height() - wrapperEleRect.top}px`);
// backdrop.fromTo('opacity', 0.4, 0.0);
// this
// .element(this.leavingView.pageRef())
// .easing('ease-out')
// .duration(250)
// .add(backdrop)
// .add(wrapper);
// }
// }
// export class ModalMDSlideIn {
// constructor(el: HTMLElement) {
// const backdrop = new Animation(this.plt, el.querySelector('ion-backdrop'));
// const wrapper = new Animation(this.plt, el.querySelector('.modal-wrapper'));
// backdrop.fromTo('opacity', 0.01, 0.4);
// wrapper.fromTo('translateY', '40px', '0px');
// wrapper.fromTo('opacity', 0.01, 1);
// const DURATION = 280;
// const EASING = 'cubic-bezier(0.36,0.66,0.04,1)';
// this.element(this.enteringView.pageRef()).easing(EASING).duration(DURATION)
// .add(backdrop)
// .add(wrapper);
// }
// }
// export class ModalMDSlideOut {
// constructor(el: HTMLElement) {
// const backdrop = new Animation(this.plt, el.querySelector('ion-backdrop'));
// const wrapper = new Animation(this.plt, el.querySelector('.modal-wrapper'));
// backdrop.fromTo('opacity', 0.4, 0.0);
// wrapper.fromTo('translateY', '0px', '40px');
// wrapper.fromTo('opacity', 0.99, 0);
// this
// .element(this.leavingView.pageRef())
// .duration(200)
// .easing('cubic-bezier(0.47,0,0.745,0.715)')
// .add(wrapper)
// .add(backdrop);
// }
// }

View File

@ -1,28 +1,55 @@
import { Animation } from '../../../interface';
import { createAnimation } from '../../../utils/animation/animation';
import { SwipeToCloseDefaults } from '../gestures/swipe-to-close';
/**
* iOS Modal Leave Animation
*/
export const iosLeaveAnimation = (baseEl: HTMLElement): Animation => {
const baseAnimation = createAnimation();
const backdropAnimation = createAnimation();
const wrapperAnimation = createAnimation();
const wrapperEl = baseEl.querySelector('.modal-wrapper');
const wrapperElRect = wrapperEl!.getBoundingClientRect();
export const iosLeaveAnimation = (
baseEl: HTMLElement,
presentingEl?: HTMLElement,
duration = 500
): Animation => {
backdropAnimation
const backdropAnimation = createAnimation()
.addElement(baseEl.querySelector('ion-backdrop')!)
.fromTo('opacity', 'var(--backdrop-opacity)', 0.0);
wrapperAnimation
.addElement(wrapperEl!)
const wrapperAnimation = createAnimation()
.addElement(baseEl.querySelector('.modal-wrapper')!)
.beforeStyles({ 'opacity': 1 })
.fromTo('transform', 'translateY(0%)', `translateY(${(baseEl.ownerDocument as any).defaultView.innerHeight - wrapperElRect.top}px)`);
.fromTo('transform', `translateY(0%)`, 'translateY(100%)');
return baseAnimation
const baseAnimation = createAnimation()
.addElement(baseEl)
.easing('ease-out')
.duration(250)
.easing('cubic-bezier(0.32,0.72,0,1)')
.duration(duration)
.addAnimation([backdropAnimation, wrapperAnimation]);
if (presentingEl) {
const modalTransform = (presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined) ? 40 : 0;
const bodyEl = document.body;
const currentPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
const presentingAnimation = createAnimation()
.addElement(presentingEl)
.beforeClearStyles(['transform'])
.afterClearStyles(['transform'])
.onFinish(currentStep => {
// only reset background color if this is the last card-style modal
if (currentStep !== 1) { return; }
const numModals = Array.from(bodyEl.querySelectorAll('ion-modal')).filter(m => m.presentingElement !== undefined).length;
if (numModals <= 1) {
bodyEl.style.setProperty('background-color', '');
}
})
.keyframes([
{ offset: 0, transform: `translateY(${-modalTransform}px) scale(${currentPresentingScale})`, 'border-radius': '10px 10px 0 0' },
{ offset: 1, transform: 'translateY(0px) scale(1)', 'border-radius': '0px' }
]);
baseAnimation.addAnimation(presentingAnimation);
}
return baseAnimation;
};

View File

@ -0,0 +1,97 @@
import { Animation } from '../../../interface';
import { getTimeGivenProgression } from '../../../utils/animation/cubic-bezier';
import { GestureDetail, createGesture } from '../../../utils/gesture';
import { clamp } from '../../../utils/helpers';
// Defaults for the card swipe animation
export const SwipeToCloseDefaults = {
MIN_BACKDROP_OPACITY: 0.4,
MIN_PRESENTING_SCALE: 0.95,
MIN_Y_CARD: 44,
MIN_Y_FULLSCREEN: 0,
MIN_PRESENTING_Y: 0
};
export const createSwipeToCloseGesture = (
el: HTMLIonModalElement,
animation: Animation,
onDismiss: () => void
) => {
const height = el.offsetHeight;
let isOpen = false;
const canStart = (detail: GestureDetail) => {
const target = detail.event.target as HTMLElement | null;
if (target === null ||
!(target as any).closest) {
return true;
}
const content = target.closest('ion-content');
if (content === null) {
return true;
}
// Target is in the content so we don't start the gesture.
// We could be more nuanced here and allow it for content that
// does not need to scroll.
return false;
};
const onStart = () => {
animation.progressStart(true, (isOpen) ? 1 : 0);
};
const onMove = (detail: GestureDetail) => {
const step = detail.deltaY / height;
if (step < 0) { return; }
animation.progressStep(step);
};
const onEnd = (detail: GestureDetail) => {
const velocity = detail.velocityY;
const step = detail.deltaY / height;
if (step < 0) { return; }
const threshold = (detail.deltaY + velocity * 1000) / height;
const shouldComplete = threshold >= 0.5;
let newStepValue = (shouldComplete) ? -0.001 : 0.001;
if (!shouldComplete) {
animation.easing('cubic-bezier(1, 0, 0.68, 0.28)');
newStepValue += getTimeGivenProgression([0, 0], [1, 0], [0.68, 0.28], [1, 1], step)[0];
} else {
animation.easing('cubic-bezier(0.32, 0.72, 0, 1)');
newStepValue += getTimeGivenProgression([0, 0], [0.32, 0.72], [0, 1], [1, 1], step)[0];
}
const duration = (shouldComplete) ? computeDuration(step * height, velocity) : computeDuration((1 - step) * height, velocity);
isOpen = shouldComplete;
animation
.onFinish(() => {
if (shouldComplete) {
onDismiss();
}
})
.progressEnd((shouldComplete) ? 1 : 0, newStepValue, duration);
};
return createGesture({
el,
gestureName: 'modalSwipeToClose',
gesturePriority: 40,
direction: 'y',
threshold: 10,
canStart,
onStart,
onMove,
onEnd
});
};
const computeDuration = (remaining: number, velocity: number) => {
return clamp(100, remaining / Math.abs(velocity * 1.1), 400);
};

View File

@ -3,11 +3,13 @@ import { AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, Mode
export interface ModalOptions<T extends ComponentRef = ComponentRef> {
component: T;
componentProps?: ComponentProps<T>;
presentingElement?: HTMLElement;
showBackdrop?: boolean;
backdropDismiss?: boolean;
cssClass?: string | string[];
delegate?: FrameworkDelegate;
animated?: boolean;
swipeToClose?: boolean;
mode?: Mode;
keyboardClose?: boolean;

View File

@ -18,3 +18,12 @@
// hidden by default to prevent flickers, the animation will show it
@include transform(translate3d(0, 100%, 0));
}
:host(.modal-card) {
align-items: flex-end;
}
:host(.modal-card) .modal-wrapper {
@include border-radius($modal-ios-border-radius, $modal-ios-border-radius, 0, 0);
height: calc(100% - 40px);
}

View File

@ -8,3 +8,5 @@ $modal-ios-background-color: $background-color !default;
/// @prop - Border radius for the modal
$modal-ios-border-radius: 10px !default;
$modal-ios-card-border-radius: 10px !default;

View File

@ -20,4 +20,4 @@
@include transform(translate3d(0, 40px, 0));
opacity: .01;
}
}

View File

@ -1,9 +1,9 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { Animation, AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, OverlayEventDetail, OverlayInterface } from '../../interface';
import { Animation, AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, Gesture, OverlayEventDetail, OverlayInterface } from '../../interface';
import { attachComponent, detachComponent } from '../../utils/framework-delegate';
import { BACKDROP, dismiss, eventMethod, prepareOverlay, present } from '../../utils/overlays';
import { BACKDROP, activeAnimations, dismiss, eventMethod, prepareOverlay, present } from '../../utils/overlays';
import { getClassMap } from '../../utils/theme';
import { deepReady } from '../../utils/transition';
@ -11,6 +11,7 @@ import { iosEnterAnimation } from './animations/ios.enter';
import { iosLeaveAnimation } from './animations/ios.leave';
import { mdEnterAnimation } from './animations/md.enter';
import { mdLeaveAnimation } from './animations/md.leave';
import { createSwipeToCloseGesture } from './gestures/swipe-to-close';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
@ -24,11 +25,13 @@ import { mdLeaveAnimation } from './animations/md.leave';
scoped: true
})
export class Modal implements ComponentInterface, OverlayInterface {
private gesture?: Gesture;
// Reference to the user's provided modal content
private usersElement?: HTMLElement;
presented = false;
animation: Animation | undefined;
animation?: Animation;
mode = getIonMode(this);
@Element() el!: HTMLIonModalElement;
@ -85,6 +88,17 @@ export class Modal implements ComponentInterface, OverlayInterface {
*/
@Prop() animated = true;
/**
* If `true`, the modal can be swiped to dismiss. Only applies in iOS mode.
*/
@Prop() swipeToClose = false;
/**
* The element that presented the modal. This is used for card presentation effects
* and for stacking multiple modals on top of each other. Only applies in iOS mode.
*/
@Prop() presentingElement?: HTMLElement;
/**
* Emitted after the modal has presented.
*/
@ -127,7 +141,21 @@ export class Modal implements ComponentInterface, OverlayInterface {
};
this.usersElement = await attachComponent(this.delegate, container, this.component, ['ion-page'], componentProps);
await deepReady(this.usersElement);
return present(this, 'modalEnter', iosEnterAnimation, mdEnterAnimation);
await present(this, 'modalEnter', iosEnterAnimation, mdEnterAnimation, this.presentingElement);
const mode = getIonMode(this);
if (this.swipeToClose && mode === 'ios') {
// All of the elements needed for the swipe gesture
// should be in the DOM and referenced by now, except
// for the presenting el
const ani = this.animation = iosLeaveAnimation(this.el, this.presentingElement);
this.gesture = createSwipeToCloseGesture(
this.el,
ani,
() => this.dismiss(undefined, 'gesture')
);
this.gesture.enable(true);
}
}
/**
@ -138,10 +166,21 @@ export class Modal implements ComponentInterface, OverlayInterface {
*/
@Method()
async dismiss(data?: any, role?: string): Promise<boolean> {
const dismissed = await dismiss(this, data, role, 'modalLeave', iosLeaveAnimation, mdLeaveAnimation);
const iosAni = (this.animation === undefined || (role === BACKDROP || role === undefined)) ? iosLeaveAnimation : undefined;
const enteringAnimation = activeAnimations.get(this) || [];
const dismissed = await dismiss(this, data, role, 'modalLeave', iosAni, mdLeaveAnimation, this.presentingElement);
if (dismissed) {
await detachComponent(this.delegate, this.usersElement);
if (this.animation) {
this.animation.destroy();
}
enteringAnimation.forEach(ani => ani.destroy());
}
this.animation = undefined;
return dismissed;
}
@ -194,6 +233,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
aria-modal="true"
class={{
[mode]: true,
[`modal-card`]: this.presentingElement !== undefined,
...getClassMap(this.cssClass)
}}
style={{
@ -209,10 +249,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
<ion-backdrop visible={this.showBackdrop} tappable={this.backdropDismiss}/>
<div
role="dialog"
class={{
[`modal-wrapper`]: true,
[mode]: true,
}}
class="modal-wrapper"
>
</div>
</Host>

View File

@ -153,6 +153,42 @@ import { EventModalModule } from '../modals/event/event.module';
export class CalendarComponentModule {}
```
### Swipeable Modals
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
```javascript
import { IonRouterOutlet } from '@ionic/angular';
constructor(private routerOutlet: IonRouterOutlet) {}
async presentModal() {
const modal = await this.modalController.create({
component: ModalPage,
swipeToClose: true,
presentingElement: this.routerOutlet.nativeEl
});
return await modal.present();
}
```
In most scenarios, using the `ion-router-outlet` element as the `presentingElement` is fine. In cases where you are presenting a card-style modal from within another modal, you should pass in the top-most `ion-modal` element as the `presentingElement`.
```javascript
import { ModalController } from '@ionic/angular';
constructor(private modalCtrl: ModalController) {}
async presentModal() {
const modal = await this.modalController.create({
component: ModalPage,
swipeToClose: true,
presentingElement: await this.modalCtrl.getTop() // Get the top-most ion-modal
});
return await modal.present();
}
```
### Javascript
@ -235,6 +271,27 @@ console.log(data);
```
### Swipeable Modals
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
```javascript
const modalElement = document.createElement('ion-modal');
modalElement.component = 'modal-page';
modalElement.swipeToClose = true;
modalElement.presentingElement = document.querySelector('ion-nav');
```
In most scenarios, using the `ion-nav` element as the `presentingElement` is fine. In cases where you are presenting a card-style modal from within a modal, you should pass in the top-most `ion-modal` element as the `presentingElement`.
```javascript
const modalElement = document.createElement('ion-modal');
modalElement.component = 'modal-page';
modalElement.swipeToClose = true;
modalElement.presentingElement = await modalController.getTop(); // Get the top-most ion-modal
```
### React
```tsx
@ -256,6 +313,43 @@ export const ModalExample: React.FC = () => {
};
```
### Swipeable Modals
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
```tsx
<IonModal
isOpen={showModal}
swipeToClose={true}
presentingElement={pageRef.current}
onDidDismiss={() => setShowModal(false)}>
<p>This is modal content</p>
<IonButton onClick={() => setShowModal(false)}>Close Modal</IonButton>
</IonModal>
```
In most scenarios, setting a ref on `IonPage` and passing that ref's `current` value to `presentingElement` is fine. In cases where you are presenting a card-style modal from within another modal, you should pass in the top-most `ion-modal` ref as the `presentingElement`.
```tsx
<IonModal
ref={firstModalRef}
isOpen={showModal}
swipeToClose={true}
presentingElement={pageRef.current}
onDidDismiss={() => setShowModal(false)}>
<p>This is modal content</p>
<IonButton onClick={() => setShow2ndModal(true)}>Show 2nd Modal</IonButton>
<IonButton onClick={() => setShowModal(false)}>Close Modal</IonButton>
</IonModal>
<IonModal
isOpen={show2ndModal}
presentingElement={firstModalRef.current}
onDidDismiss={() => setShow2ndModal(false)}>
<p>This is more modal content</p>
<IonButton onClick={() => setShow2ndModal(false)}>Close Modal</IonButton>
</IonModal>
```
### Vue
@ -326,18 +420,20 @@ export default {
## Properties
| Property | Attribute | Description | Type | Default |
| ------------------------ | ------------------ | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------- |
| `animated` | `animated` | If `true`, the modal will animate. | `boolean` | `true` |
| `backdropDismiss` | `backdrop-dismiss` | If `true`, the modal will be dismissed when the backdrop is clicked. | `boolean` | `true` |
| `component` _(required)_ | `component` | The component to display inside of the modal. | `Function \| HTMLElement \| null \| string` | `undefined` |
| `componentProps` | -- | The data to pass to the modal component. | `undefined \| { [key: string]: any; }` | `undefined` |
| `cssClass` | `css-class` | Additional classes to apply for custom CSS. If multiple classes are provided they should be separated by spaces. | `string \| string[] \| undefined` | `undefined` |
| `enterAnimation` | -- | Animation to use when the modal is presented. | `((baseEl: any, opts?: any) => Animation) \| undefined` | `undefined` |
| `keyboardClose` | `keyboard-close` | If `true`, the keyboard will be automatically dismissed when the overlay is presented. | `boolean` | `true` |
| `leaveAnimation` | -- | Animation to use when the modal is dismissed. | `((baseEl: any, opts?: any) => Animation) \| undefined` | `undefined` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `showBackdrop` | `show-backdrop` | If `true`, a backdrop will be displayed behind the modal. | `boolean` | `true` |
| Property | Attribute | Description | Type | Default |
| ------------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------- |
| `animated` | `animated` | If `true`, the modal will animate. | `boolean` | `true` |
| `backdropDismiss` | `backdrop-dismiss` | If `true`, the modal will be dismissed when the backdrop is clicked. | `boolean` | `true` |
| `component` _(required)_ | `component` | The component to display inside of the modal. | `Function \| HTMLElement \| null \| string` | `undefined` |
| `componentProps` | -- | The data to pass to the modal component. | `undefined \| { [key: string]: any; }` | `undefined` |
| `cssClass` | `css-class` | Additional classes to apply for custom CSS. If multiple classes are provided they should be separated by spaces. | `string \| string[] \| undefined` | `undefined` |
| `enterAnimation` | -- | Animation to use when the modal is presented. | `((baseEl: any, opts?: any) => Animation) \| undefined` | `undefined` |
| `keyboardClose` | `keyboard-close` | If `true`, the keyboard will be automatically dismissed when the overlay is presented. | `boolean` | `true` |
| `leaveAnimation` | -- | Animation to use when the modal is dismissed. | `((baseEl: any, opts?: any) => Animation) \| undefined` | `undefined` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `presentingElement` | -- | The element that presented the modal. This is used for card presentation effects and for stacking multiple modals on top of each other. Only applies in iOS mode. | `HTMLElement \| undefined` | `undefined` |
| `showBackdrop` | `show-backdrop` | If `true`, a backdrop will be displayed behind the modal. | `boolean` | `true` |
| `swipeToClose` | `swipe-to-close` | If `true`, the modal can be swiped to dismiss. Only applies in iOS mode. | `boolean` | `false` |
## Events

View File

@ -23,23 +23,26 @@
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Modal - Basic</ion-title>
</ion-toolbar>
</ion-header>
<div class="ion-page">
<ion-header>
<ion-toolbar>
<ion-title>Modal - Basic</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<p>
<ion-button id="basic-modal" onclick="presentModal()">Present modal</ion-button>
</p>
<p>
<ion-button id="presentModal" class="e2ePresentModal" onclick="presentCloseModal()">Present and close modal</ion-button>
</p>
<p>
<ion-button id="presentModal" class="e2ePresentModal" onclick="presentCloseModal2()">Present and close modal (crash)</ion-button>
</p>
</ion-content>
<ion-content class="ion-padding">
<p>
<ion-button id="basic-modal" onclick="presentModal()">Present modal</ion-button>
</p>
<p>
<ion-button id="presentModal" class="e2ePresentModal" onclick="presentCloseModal()">Present and close modal</ion-button>
</p>
<p>
<ion-button id="presentModal" class="e2ePresentModal" onclick="presentCloseModal2()">Present and close modal (crash)</ion-button>
</p>
</ion-content>
</div>
<ion-modal-controller></ion-modal-controller>
</ion-app>
@ -77,8 +80,9 @@
}
async function presentModal() {
const presentingEl = document.querySelectorAll('.ion-page')[1];
const modal = createModal();
await modal.present();
await modal.present(presentingEl);
}
async function presentCloseModal() {
const modal = createModal();

View File

@ -0,0 +1,11 @@
import { testModal } from '../test.utils';
const DIRECTORY = 'spec';
test('modal: card', async () => {
await testModal(DIRECTORY, '#card-modal');
});
test('modal:rtl: card', async () => {
await testModal(DIRECTORY, '#card-modal', true);
});

View File

@ -0,0 +1,478 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Modal - Spec</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
:root {
--ion-safe-area-top: 20px;
--ion-safe-area-bottom: 20px;
}
#modal-header {
padding-top: 5px !important;
height: 55px;
}
#modal-header ion-title {
padding-top: 5px;
}
#modal-header ion-note {
display: block;
width: 100%;
height: 15px;
text-align: center;
font-size: 11px;
color: #111;
}
ion-list ion-icon {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<ion-app>
<div class="ion-page">
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button id="card-modal" onclick="presentModal(document.querySelectorAll('.ion-page')[1])">
<ion-icon name="add" slot="icon-only"></ion-icon>
</ion-button>
</ion-buttons>
<ion-title>Favorites</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list id="list"></ion-list>
</ion-content>
</div>
<ion-modal-controller></ion-modal-controller>
</ion-app>
<script>
window.addEventListener("ionModalDidDismiss", function (e) { console.log('DidDismiss', e) })
window.addEventListener("ionModalWillDismiss", function (e) { console.log('WillDismiss', e) })
const people = [
{
"name": "Miyah Myles",
"email": "miyah.myles@gmail.com",
"position": "Business Analyst",
},
{
"name": "June Cha",
"email": "june.cha@gmail.com",
"position": "Data Entry Clerk",
},
{
"name": "Iida Niskanen",
"email": "iida.niskanen@gmail.com",
"position": "Business Analyst",
},
{
"name": "Renee Sims",
"email": "renee.sims@gmail.com",
"position": "Lead Developer",
},
{
"name": "Jonathan Nu\u00f1ez",
"email": "jonathan.nu\u00f1ez@gmail.com",
"position": "Receptionist",
},
{
"name": "Sasha Ho",
"email": "sasha.ho@gmail.com",
"position": "Sales",
},
{
"name": "Abdullah Hadley",
"email": "abdullah.hadley@gmail.com",
"position": "Sales Manager",
},
{
"name": "Veeti Seppanen",
"email": "veeti.seppanen@gmail.com",
"position": "Marketing",
},
{
"name": "Thomas Stock",
"email": "thomas.stock@gmail.com",
"position": "Clerical",
},
{
"name": "Bonnie Riley",
"email": "bonnie.riley@gmail.com",
"position": "Medical Assistant",
}
];
const allContacts = [
{
"name": "Miyah Myles",
"email": "miyah.myles@gmail.com",
"position": "Office Assistant",
},
{
"name": "June Cha",
"email": "june.cha@gmail.com",
"position": "Administrative Assistant",
},
{
"name": "Iida Niskanen",
"email": "iida.niskanen@gmail.com",
"position": "Customer Service Representative",
},
{
"name": "Renee Sims",
"email": "renee.sims@gmail.com",
"position": "Customer Service Representative",
},
{
"name": "Jonathan Nu\u00f1ez",
"email": "jonathan.nu\u00f1ez@gmail.com",
"position": "Sales",
},
{
"name": "Sasha Ho",
"email": "sasha.ho@gmail.com",
"position": "Marketing",
},
{
"name": "Abdullah Hadley",
"email": "abdullah.hadley@gmail.com",
"position": "Marketing",
},
{
"name": "Veeti Seppanen",
"email": "veeti.seppanen@gmail.com",
"position": "Project Manager",
},
{
"name": "Thomas Stock",
"email": "thomas.stock@gmail.com",
"position": "Customer Service",
},
{
"name": "Bonnie Riley",
"email": "bonnie.riley@gmail.com",
"position": "Executive Assistant",
},
{
"name": "Steve T. Scaife",
"email": "steve.t..scaife@gmail.com",
"position": "Receptionist",
},
{
"name": "Andreas Brixen",
"email": "andreas.brixen@gmail.com",
"position": "Director",
},
{
"name": "Lilja Peltola",
"email": "lilja.peltola@gmail.com",
"position": "Sales Manager",
},
{
"name": "Sean PJPGR Doran",
"email": "sean.pjpgr.doran@gmail.com",
"position": "Lead Developer",
},
{
"name": "Elliana Palacios",
"email": "elliana.palacios@gmail.com",
"position": "Marketing",
},
{
"name": "Eduard Franz",
"email": "eduard.franz@gmail.com",
"position": "Manager",
},
{
"name": "Leah Stevens",
"email": "leah.stevens@gmail.com",
"position": "Attorney",
},
{
"name": "Britney Cooper",
"email": "britney.cooper@gmail.com",
"position": "Data Entry Clerk",
},
{
"name": "Chrishell Stause",
"email": "chrishell.stause@gmail.com",
"position": "Receptionist",
},
{
"name": "Ana De Armas",
"email": "ana.de.armas@gmail.com",
"position": "Administrative Assistant",
},
{
"name": "Jennifer Fritz",
"email": "jennifer.fritz@gmail.com",
"position": "Graphic Designer",
},
{
"name": "Wyatt Morris",
"email": "wyatt.morris@gmail.com",
"position": "Executive Assistant",
},
{
"name": "Lourdes Browning",
"email": "lourdes.browning@gmail.com",
"position": "Sales",
},
{
"name": "Tim Schoch",
"email": "tim.schoch@gmail.com",
"position": "Product Designer",
},
{
"name": "Nykyta Korotkevych",
"email": "nykyta.korotkevych@gmail.com",
"position": "Lead Developer",
},
{
"name": "Carys Metz",
"email": "carys.metz@gmail.com",
"position": "Administrative Assistant",
},
{
"name": "Loki Bright",
"email": "loki.bright@gmail.com",
"position": "Data Entry",
},
{
"name": "Ferdinand Karl",
"email": "ferdinand.karl@gmail.com",
"position": "Medical Assistant",
},
{
"name": "Andrew Kumar",
"email": "andrew.kumar@gmail.com",
"position": "Accounting",
},
{
"name": "Mario Palmer",
"email": "mario.palmer@gmail.com",
"position": "Attorney",
},
{
"name": "Zechariah Burrell",
"email": "zechariah.burrell@gmail.com",
"position": "Part Time",
},
{
"name": "Lucr\u00e9cia Caldeira",
"email": "lucr\u00e9cia.caldeira@gmail.com",
"position": "Human Resources",
},
{
"name": "Love Grayson",
"email": "love.grayson@gmail.com",
"position": "Office Assistant",
},
{
"name": "Elizabeth Olsen",
"email": "elizabeth.olsen@gmail.com",
"position": "Accounting",
},
{
"name": "Layton Diament",
"email": "layton.diament@gmail.com",
"position": "Receptionist",
},
{
"name": "Sophie French",
"email": "sophie.french@gmail.com",
"position": "Medical Assistant",
},
{
"name": "Mia Denys",
"email": "mia.denys@gmail.com",
"position": "Data Entry Clerk",
},
{
"name": "Christine M. Maldonado",
"email": "christine.m..maldonado@gmail.com",
"position": "Director",
},
{
"name": "Line Rolland",
"email": "line.rolland@gmail.com",
"position": "Project Manager",
},
{
"name": "Micheal Murphy",
"email": "micheal.murphy@gmail.com",
"position": "Software Engineer",
},
{
"name": "Jacob Ginnish",
"email": "jacob.ginnish@gmail.com",
"position": "Sales",
},
{
"name": "Erwan Gauthier",
"email": "erwan.gauthier@gmail.com",
"position": "Marketing",
},
{
"name": "Derrick Wells",
"email": "derrick.wells@gmail.com",
"position": "Office Assistant",
},
{
"name": "Emre Topalo\u011flu",
"email": "emre.topalo\u011flu@gmail.com",
"position": "Project Manager",
},
{
"name": "Lucy Walker",
"email": "lucy.walker@gmail.com",
"position": "Business Analyst",
},
{
"name": "Ece Akman",
"email": "ece.akman@gmail.com",
"position": "Accounting",
},
{
"name": "Sophie Louise Hart",
"email": "sophie.louise.hart@gmail.com",
"position": "Attorney",
},
{
"name": "Carmen Velasco",
"email": "carmen.velasco@gmail.com",
"position": "Executive Assistant",
}
]
const list = document.querySelector('#list');
const addFavorite = (p) => {
const item = document.createElement('ion-item');
item.innerHTML = `
<ion-avatar slot="start"><ion-icon name="person"></ion-icon></ion-avatar>
<ion-label>
<h2>${p.name}</h2>
<h3>${p.position}</h3>
</ion-label>
`;
list.appendChild(item);
}
people.forEach(p => addFavorite(p));
function handleAddFavorite (email) {
const modalController = document.querySelector('ion-modal-controller');
modalController.dismiss(email);
}
async function createModal(presentingEl) {
// initialize controller
const modalController = document.querySelector('ion-modal-controller');
await modalController.componentOnReady();
const contactGroups = allContacts
.sort((a, b) => {
const aSplit = a.name.split(' ');
const bSplit = b.name.split(' ');
return aSplit[1].localeCompare(bSplit[1]);
}).reduce((groups, contact) => {
const firstLast = contact.name.split(' ')[1].charAt(0);
if (!groups.hasOwnProperty(firstLast)) {
groups[firstLast] = [];
}
groups[firstLast].push(contact);
return groups;
}, {});
const sortedGroups = Object.keys(contactGroups)
.map(k => { return { letter: k, contacts: contactGroups[k] }})
.sort((a, b) => a.letter.localeCompare(b.letter))
const items = new Array(20).fill(0).map((item, i) => {
return {
title: `Person ${i}`
}
});
// create component to open
const element = document.createElement('div');
element.innerHTML = `
<ion-header id="modal-header">
<ion-note>Choose a contact to add to Favorites</ion-note>
<ion-toolbar>
<ion-title>Contacts</ion-title>
<ion-buttons slot="end">
<ion-button class="add">
<ion-icon name="add" slot="icon-only"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
${sortedGroups.map(group => {
return `<ion-item-divider sticky="true">
<ion-label>
${group.letter}
</ion-label>
</ion-item-divider>
${group.contacts.map(item => `<ion-item onclick="handleAddFavorite('${item.email}')"><ion-label>${item.name}</ion-label></ion-item>`).join('')}
`;
}).join('')}
</ion-list>
<ion-button class="dismiss">Dismiss Modal</ion-button>
</ion-content>
`;
// listen for close event
const button = element.querySelector('ion-button.dismiss');
button.addEventListener('click', () => {
modalController.dismiss();
});
const create = element.querySelector('ion-button.add');
create.addEventListener('click', async () => {
const topModal = await modalController.getTop();
presentModal(topModal);
});
// present the modal
const modalElement = await modalController.create({
presentingElement: presentingEl,
component: element,
swipeToClose: true
});
return modalElement;
}
async function presentModal(presentingEl) {
const modal = await createModal(presentingEl);
await modal.present();
const data = await modal.onWillDismiss();
const person = allContacts.find(c => c.email === data.data);
person && addFavorite(person);
}
</script>
</body>
</html>

View File

@ -129,4 +129,40 @@ import { EventModalModule } from '../modals/event/event.module';
})
export class CalendarComponentModule {}
```
```
### Swipeable Modals
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
```javascript
import { IonRouterOutlet } from '@ionic/angular';
constructor(private routerOutlet: IonRouterOutlet) {}
async presentModal() {
const modal = await this.modalController.create({
component: ModalPage,
swipeToClose: true,
presentingElement: this.routerOutlet.nativeEl
});
return await modal.present();
}
```
In most scenarios, using the `ion-router-outlet` element as the `presentingElement` is fine. In cases where you are presenting a card-style modal from within another modal, you should pass in the top-most `ion-modal` element as the `presentingElement`.
```javascript
import { ModalController } from '@ionic/angular';
constructor(private modalCtrl: ModalController) {}
async presentModal() {
const modal = await this.modalController.create({
component: ModalPage,
swipeToClose: true,
presentingElement: await this.modalCtrl.getTop() // Get the top-most ion-modal
});
return await modal.present();
}
```

View File

@ -75,4 +75,25 @@ After being dismissed, the data can be read in through the `onWillDismiss` or `o
```javascript
const { data } = await modalElement.onWillDismiss();
console.log(data);
```
```
### Swipeable Modals
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
```javascript
const modalElement = document.createElement('ion-modal');
modalElement.component = 'modal-page';
modalElement.swipeToClose = true;
modalElement.presentingElement = document.querySelector('ion-nav');
```
In most scenarios, using the `ion-nav` element as the `presentingElement` is fine. In cases where you are presenting a card-style modal from within a modal, you should pass in the top-most `ion-modal` element as the `presentingElement`.
```javascript
const modalElement = document.createElement('ion-modal');
modalElement.component = 'modal-page';
modalElement.swipeToClose = true;
modalElement.presentingElement = await modalController.getTop(); // Get the top-most ion-modal
```

View File

@ -16,3 +16,40 @@ export const ModalExample: React.FC = () => {
);
};
```
### Swipeable Modals
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
```tsx
<IonModal
isOpen={showModal}
swipeToClose={true}
presentingElement={pageRef.current}
onDidDismiss={() => setShowModal(false)}>
<p>This is modal content</p>
<IonButton onClick={() => setShowModal(false)}>Close Modal</IonButton>
</IonModal>
```
In most scenarios, setting a ref on `IonPage` and passing that ref's `current` value to `presentingElement` is fine. In cases where you are presenting a card-style modal from within another modal, you should pass in the top-most `ion-modal` ref as the `presentingElement`.
```tsx
<IonModal
ref={firstModalRef}
isOpen={showModal}
swipeToClose={true}
presentingElement={pageRef.current}
onDidDismiss={() => setShowModal(false)}>
<p>This is modal content</p>
<IonButton onClick={() => setShow2ndModal(true)}>Show 2nd Modal</IonButton>
<IonButton onClick={() => setShowModal(false)}>Close Modal</IonButton>
</IonModal>
<IonModal
isOpen={show2ndModal}
presentingElement={firstModalRef.current}
onDidDismiss={() => setShow2ndModal(false)}>
<p>This is more modal content</p>
<IonButton onClick={() => setShow2ndModal(false)}>Close Modal</IonButton>
</IonModal>
```

View File

@ -986,7 +986,7 @@ export class Nav implements NavOutlet {
newStepValue += getTimeGivenProgression([0, 0], [0.32, 0.72], [0, 1], [1, 1], stepValue)[0];
}
(this.sbAni as Animation).progressEnd(shouldComplete ? 1 : 0, newStepValue, dur);
this.sbAni.progressEnd(shouldComplete ? 1 : 0, newStepValue, dur);
}
}

View File

@ -1,7 +1,7 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, State, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { Animation, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface, PickerButton, PickerColumn } from '../../interface';
import { AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface, PickerButton, PickerColumn } from '../../interface';
import { BACKDROP, dismiss, eventMethod, isCancel, prepareOverlay, present, safeCall } from '../../utils/overlays';
import { getClassMap } from '../../utils/theme';
@ -24,8 +24,6 @@ export class Picker implements ComponentInterface, OverlayInterface {
mode = getIonMode(this);
animation?: Animation;
@Element() el!: HTMLIonPickerElement;
@State() presented = false;

View File

@ -1,7 +1,7 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { Animation, AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, OverlayEventDetail, OverlayInterface } from '../../interface';
import { AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, OverlayEventDetail, OverlayInterface } from '../../interface';
import { attachComponent, detachComponent } from '../../utils/framework-delegate';
import { BACKDROP, dismiss, eventMethod, prepareOverlay, present } from '../../utils/overlays';
import { getClassMap } from '../../utils/theme';
@ -28,7 +28,6 @@ export class Popover implements ComponentInterface, OverlayInterface {
private usersElement?: HTMLElement;
presented = false;
animation?: Animation;
mode = getIonMode(this);
@Element() el!: HTMLIonPopoverElement;

View File

@ -96,7 +96,7 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
newStepValue += getTimeGivenProgression([0, 0], [0.32, 0.72], [0, 1], [1, 1], step)[0];
}
(this.ani as Animation).progressEnd(shouldComplete ? 1 : 0, newStepValue, dur);
this.ani.progressEnd(shouldComplete ? 1 : 0, newStepValue, dur);
}
}

View File

@ -1,7 +1,7 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { Animation, AnimationBuilder, Color, CssClassMap, OverlayEventDetail, OverlayInterface, ToastButton } from '../../interface';
import { AnimationBuilder, Color, CssClassMap, OverlayEventDetail, OverlayInterface, ToastButton } from '../../interface';
import { dismiss, eventMethod, isCancel, prepareOverlay, present, safeCall } from '../../utils/overlays';
import { sanitizeDOMString } from '../../utils/sanitization';
import { createColorClasses, getClassMap } from '../../utils/theme';
@ -27,7 +27,6 @@ export class Toast implements ComponentInterface, OverlayInterface {
private durationTimeout: any;
presented = false;
animation?: Animation;
mode = getIonMode(this);
@Element() el!: HTMLIonToastElement;

View File

@ -1,7 +1,7 @@
import { EventEmitter } from '@stencil/core';
import { HTMLStencilElement } from '@stencil/core/internal';
import { Animation, AnimationBuilder, Mode } from '../interface';
import { AnimationBuilder, Mode } from '../interface';
export interface OverlayEventDetail<T = any> {
data?: T;
@ -15,7 +15,6 @@ export interface OverlayInterface {
keyboardClose: boolean;
overlayIndex: number;
presented: boolean;
animation?: Animation;
enterAnimation?: AnimationBuilder;
leaveAnimation?: AnimationBuilder;

View File

@ -1,8 +1,10 @@
import { config } from '../global/config';
import { ActionSheetOptions, AlertOptions, AnimationBuilder, BackButtonEvent, HTMLIonOverlayElement, IonicConfig, LoadingOptions, ModalOptions, OverlayInterface, PickerOptions, PopoverOptions, ToastOptions } from '../interface';
import { ActionSheetOptions, AlertOptions, Animation, AnimationBuilder, BackButtonEvent, HTMLIonOverlayElement, IonicConfig, LoadingOptions, ModalOptions, OverlayInterface, PickerOptions, PopoverOptions, ToastOptions } from '../interface';
let lastId = 0;
export const activeAnimations = new WeakMap<OverlayInterface, Animation[]>();
const createController = <Opts extends object, HTMLElm extends any>(tagName: string) => {
return {
create(options: Opts): Promise<HTMLElm> {
@ -140,7 +142,7 @@ export const dismiss = async (
data: any | undefined,
role: string | undefined,
name: keyof IonicConfig,
iosLeaveAnimation: AnimationBuilder,
iosLeaveAnimation: AnimationBuilder | undefined,
mdLeaveAnimation: AnimationBuilder,
opts?: any
): Promise<boolean> => {
@ -156,9 +158,13 @@ export const dismiss = async (
? overlay.leaveAnimation
: config.get(name, overlay.mode === 'ios' ? iosLeaveAnimation : mdLeaveAnimation);
await overlayAnimation(overlay, animationBuilder, overlay.el, opts);
if (animationBuilder !== undefined) {
await overlayAnimation(overlay, animationBuilder, overlay.el, opts);
}
overlay.didDismiss.emit({ data, role });
activeAnimations.delete(overlay);
} catch (err) {
console.error(err);
}
@ -177,19 +183,12 @@ const overlayAnimation = async (
baseEl: any,
opts: any
): Promise<boolean> => {
if (overlay.animation) {
overlay.animation.destroy();
overlay.animation = undefined;
return false;
}
// Make overlay visible in case it's hidden
baseEl.classList.remove('overlay-hidden');
const aniRoot = baseEl.shadowRoot || overlay.el;
const animation = animationBuilder(aniRoot, opts);
overlay.animation = animation;
if (!overlay.animated || !config.getBoolean('animated', true)) {
animation.duration(0);
}
@ -203,9 +202,11 @@ const overlayAnimation = async (
});
}
const activeAni = activeAnimations.get(overlay) || [];
activeAnimations.set(overlay, [...activeAni, animation]);
await animation.play();
overlay.animation = undefined;
return true;
};

View File

@ -256,8 +256,8 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio
enteringTransitionEffect
.addElement(enteringTransitionEffectEl)
.beforeStyles({ opacity: '1' })
.afterStyles({ opacity: '' });
.beforeStyles({ opacity: '1', display: 'block' })
.afterStyles({ opacity: '', display: '' });
enteringTransitionCover
.addElement(enteringTransitionCoverEl!) // REVIEW
@ -406,8 +406,8 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio
leavingTransitionEffect
.addElement(leavingTransitionEffectEl)
.beforeStyles({ opacity: '1' })
.afterStyles({ opacity: '' });
.beforeStyles({ opacity: '1', display: 'block' })
.afterStyles({ opacity: '', display: '' });
leavingTransitionCover
.addElement(leavingTransitionCoverEl!) // REVIEW