mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-15 01:03:03 +08:00
feat(modal): modals can now be used inline (#23341)
resolves #20117, resolves #20263
This commit is contained in:
@ -14,6 +14,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver
|
||||
|
||||
- [Components](#components)
|
||||
* [Header](#header)
|
||||
* [Modal](#modal)
|
||||
* [Popover](#popover)
|
||||
* [Tab Bar](#tab-bar)
|
||||
* [Toast](#toast)
|
||||
@ -47,7 +48,13 @@ ion-header.header-collapse-condense ion-toolbar:last-of-type {
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
#### Modal
|
||||
|
||||
Converted `ion-modal` 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-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.
|
||||
|
||||
#### Tab Bar
|
||||
|
||||
|
39
angular/src/directives/overlays/ion-modal.ts
Normal file
39
angular/src/directives/overlays/ion-modal.ts
Normal file
@ -0,0 +1,39 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, NgZone, TemplateRef } from "@angular/core";
|
||||
import { ProxyCmp, proxyOutputs } from "../proxies-utils";
|
||||
import { Components } from "@ionic/core";
|
||||
export declare interface IonModal extends Components.IonModal {
|
||||
}
|
||||
@ProxyCmp({ inputs: ["animated", "backdropDismiss", "cssClass", "enterAnimation", "event", "isOpen", "keyboardClose", "leaveAnimation", "mode", "showBackdrop", "translucent"], "methods": ["present", "dismiss", "onDidDismiss", "onWillDismiss"] })
|
||||
@Component({ selector: "ion-modal", changeDetection: ChangeDetectionStrategy.OnPush, template: `<ng-container [ngTemplateOutlet]="template" *ngIf="isCmpOpen"></ng-container>`, inputs: ["animated", "backdropDismiss", "component", "componentProps", "cssClass", "enterAnimation", "event", "isOpen", "keyboardClose", "leaveAnimation", "mode", "showBackdrop", "translucent"] })
|
||||
export class IonModal {
|
||||
@ContentChild(TemplateRef, { static: false }) template: TemplateRef<any>;
|
||||
|
||||
ionModalDidPresent!: EventEmitter<CustomEvent>;
|
||||
ionModalWillPresent!: EventEmitter<CustomEvent>;
|
||||
ionModalWillDismiss!: EventEmitter<CustomEvent>;
|
||||
ionModalDidDismiss!: EventEmitter<CustomEvent>;
|
||||
didPresent!: EventEmitter<CustomEvent>;
|
||||
willPresent!: EventEmitter<CustomEvent>;
|
||||
willDismiss!: EventEmitter<CustomEvent>;
|
||||
didDismiss!: EventEmitter<CustomEvent>;
|
||||
isCmpOpen: boolean = false;
|
||||
|
||||
protected el: HTMLElement;
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
||||
c.detach();
|
||||
this.el = r.nativeElement;
|
||||
|
||||
this.el.addEventListener('willPresent', () => {
|
||||
this.isCmpOpen = true;
|
||||
c.detectChanges();
|
||||
});
|
||||
this.el.addEventListener('didDismiss', () => {
|
||||
this.isCmpOpen = false;
|
||||
c.detectChanges();
|
||||
});
|
||||
|
||||
proxyOutputs(this, this.el, ["ionModalDidPresent", "ionModalWillPresent", "ionModalWillDismiss", "ionModalDidDismiss", "didPresent", "willPresent", "willDismiss", "didDismiss"]);
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ import { IonRouterOutlet } from './directives/navigation/ion-router-outlet';
|
||||
import { IonTabs } from './directives/navigation/ion-tabs';
|
||||
import { NavDelegate } from './directives/navigation/nav-delegate';
|
||||
import { RouterLinkDelegate } from './directives/navigation/router-link-delegate';
|
||||
import { IonModal } from './directives/overlays/ion-modal';
|
||||
import { IonPopover } from './directives/overlays/ion-popover';
|
||||
import { IonAccordion, IonAccordionGroup, IonApp, IonAvatar, IonBackButton, IonBackdrop, IonBadge, IonButton, IonButtons, IonCard, IonCardContent, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCheckbox, IonChip, IonCol, IonContent, IonDatetime, IonFab, IonFabButton, IonFabList, IonFooter, IonGrid, IonHeader, IonIcon, IonImg, IonInfiniteScroll, IonInfiniteScrollContent, IonInput, IonItem, IonItemDivider, IonItemGroup, IonItemOption, IonItemOptions, IonItemSliding, IonLabel, IonList, IonListHeader, IonMenu, IonMenuButton, IonMenuToggle, IonNav, IonNavLink, IonNote, IonProgressBar, IonRadio, IonRadioGroup, IonRange, IonRefresher, IonRefresherContent, IonReorder, IonReorderGroup, IonRippleEffect, IonRow, IonSearchbar, IonSegment, IonSegmentButton, IonSelect, IonSelectOption, IonSkeletonText, IonSlide, IonSlides, IonSpinner, IonSplitPane, IonTabBar, IonTabButton, IonText, IonTextarea, IonThumbnail, IonTitle, IonToggle, IonToolbar } from './directives/proxies';
|
||||
import { VirtualFooter } from './directives/virtual-scroll/virtual-footer';
|
||||
@ -68,6 +69,7 @@ const DECLARATIONS = [
|
||||
IonMenu,
|
||||
IonMenuButton,
|
||||
IonMenuToggle,
|
||||
IonModal,
|
||||
IonNav,
|
||||
IonNavLink,
|
||||
IonNote,
|
||||
|
13
core/api.txt
13
core/api.txt
@ -721,27 +721,30 @@ ion-menu-toggle,shadow
|
||||
ion-menu-toggle,prop,autoHide,boolean,true,false,false
|
||||
ion-menu-toggle,prop,menu,string | undefined,undefined,false,false
|
||||
|
||||
ion-modal,scoped
|
||||
ion-modal,shadow
|
||||
ion-modal,prop,animated,boolean,true,false,false
|
||||
ion-modal,prop,backdropDismiss,boolean,true,false,false
|
||||
ion-modal,prop,component,Function | HTMLElement | null | string,undefined,true,false
|
||||
ion-modal,prop,componentProps,undefined | { [key: string]: any; },undefined,false,false
|
||||
ion-modal,prop,cssClass,string | string[] | undefined,undefined,false,false
|
||||
ion-modal,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
|
||||
ion-modal,prop,isOpen,boolean,false,false,false
|
||||
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,prop,trigger,string | undefined,undefined,false,false
|
||||
ion-modal,method,dismiss,dismiss(data?: any, role?: string | undefined) => Promise<boolean>
|
||||
ion-modal,method,onDidDismiss,onDidDismiss<T = any>() => Promise<OverlayEventDetail<T>>
|
||||
ion-modal,method,onWillDismiss,onWillDismiss<T = any>() => Promise<OverlayEventDetail<T>>
|
||||
ion-modal,method,present,present() => Promise<void>
|
||||
ion-modal,event,didDismiss,OverlayEventDetail<any>,true
|
||||
ion-modal,event,didPresent,void,true
|
||||
ion-modal,event,ionModalDidDismiss,OverlayEventDetail<any>,true
|
||||
ion-modal,event,ionModalDidPresent,void,true
|
||||
ion-modal,event,ionModalWillDismiss,OverlayEventDetail<any>,true
|
||||
ion-modal,event,ionModalWillPresent,void,true
|
||||
ion-modal,event,willDismiss,OverlayEventDetail<any>,true
|
||||
ion-modal,event,willPresent,void,true
|
||||
ion-modal,css-prop,--backdrop-opacity
|
||||
ion-modal,css-prop,--background
|
||||
ion-modal,css-prop,--border-color
|
||||
@ -754,6 +757,8 @@ ion-modal,css-prop,--max-width
|
||||
ion-modal,css-prop,--min-height
|
||||
ion-modal,css-prop,--min-width
|
||||
ion-modal,css-prop,--width
|
||||
ion-modal,part,backdrop
|
||||
ion-modal,part,content
|
||||
|
||||
ion-nav,shadow
|
||||
ion-nav,prop,animated,boolean,true,false,false
|
||||
|
38
core/src/components.d.ts
vendored
38
core/src/components.d.ts
vendored
@ -1375,7 +1375,7 @@ export namespace Components {
|
||||
/**
|
||||
* The component to display inside of the modal.
|
||||
*/
|
||||
"component": ComponentRef;
|
||||
"component"?: ComponentRef;
|
||||
/**
|
||||
* The data to pass to the modal component.
|
||||
*/
|
||||
@ -1395,6 +1395,11 @@ export namespace Components {
|
||||
* Animation to use when the modal is presented.
|
||||
*/
|
||||
"enterAnimation"?: AnimationBuilder;
|
||||
"inline": boolean;
|
||||
/**
|
||||
* If `true`, the modal will open. If `false`, the modal will close. Use this if you need finer grained control over presentation, otherwise just use the modalController or the `trigger` property. Note: `isOpen` will not automatically be set back to `false` when the modal dismisses. You will need to do that in your code.
|
||||
*/
|
||||
"isOpen": boolean;
|
||||
/**
|
||||
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
|
||||
*/
|
||||
@ -1432,6 +1437,10 @@ export namespace Components {
|
||||
* If `true`, the modal can be swiped to dismiss. Only applies in iOS mode.
|
||||
*/
|
||||
"swipeToClose": boolean;
|
||||
/**
|
||||
* An ID corresponding to the trigger element that causes the modal to open when clicked.
|
||||
*/
|
||||
"trigger": string | undefined;
|
||||
}
|
||||
interface IonNav {
|
||||
/**
|
||||
@ -4831,7 +4840,7 @@ declare namespace LocalJSX {
|
||||
/**
|
||||
* The component to display inside of the modal.
|
||||
*/
|
||||
"component": ComponentRef;
|
||||
"component"?: ComponentRef;
|
||||
/**
|
||||
* The data to pass to the modal component.
|
||||
*/
|
||||
@ -4845,6 +4854,11 @@ declare namespace LocalJSX {
|
||||
* Animation to use when the modal is presented.
|
||||
*/
|
||||
"enterAnimation"?: AnimationBuilder;
|
||||
"inline"?: boolean;
|
||||
/**
|
||||
* If `true`, the modal will open. If `false`, the modal will close. Use this if you need finer grained control over presentation, otherwise just use the modalController or the `trigger` property. Note: `isOpen` will not automatically be set back to `false` when the modal dismisses. You will need to do that in your code.
|
||||
*/
|
||||
"isOpen"?: boolean;
|
||||
/**
|
||||
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
|
||||
*/
|
||||
@ -4857,6 +4871,14 @@ declare namespace LocalJSX {
|
||||
* The mode determines which platform styles to use.
|
||||
*/
|
||||
"mode"?: "ios" | "md";
|
||||
/**
|
||||
* Emitted after the modal has dismissed. Shorthand for ionModalDidDismiss.
|
||||
*/
|
||||
"onDidDismiss"?: (event: CustomEvent<OverlayEventDetail>) => void;
|
||||
/**
|
||||
* Emitted after the modal has presented. Shorthand for ionModalWillDismiss.
|
||||
*/
|
||||
"onDidPresent"?: (event: CustomEvent<void>) => void;
|
||||
/**
|
||||
* Emitted after the modal has dismissed.
|
||||
*/
|
||||
@ -4873,6 +4895,14 @@ declare namespace LocalJSX {
|
||||
* Emitted before the modal has presented.
|
||||
*/
|
||||
"onIonModalWillPresent"?: (event: CustomEvent<void>) => void;
|
||||
/**
|
||||
* Emitted before the modal has dismissed. Shorthand for ionModalWillDismiss.
|
||||
*/
|
||||
"onWillDismiss"?: (event: CustomEvent<OverlayEventDetail>) => void;
|
||||
/**
|
||||
* Emitted before the modal has presented. Shorthand for ionModalWillPresent.
|
||||
*/
|
||||
"onWillPresent"?: (event: CustomEvent<void>) => void;
|
||||
"overlayIndex": number;
|
||||
/**
|
||||
* 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.
|
||||
@ -4886,6 +4916,10 @@ declare namespace LocalJSX {
|
||||
* If `true`, the modal can be swiped to dismiss. Only applies in iOS mode.
|
||||
*/
|
||||
"swipeToClose"?: boolean;
|
||||
/**
|
||||
* An ID corresponding to the trigger element that causes the modal to open when clicked.
|
||||
*/
|
||||
"trigger"?: string | undefined;
|
||||
}
|
||||
interface IonNav {
|
||||
/**
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Animation } from '../../../interface';
|
||||
import { createAnimation } from '../../../utils/animation/animation';
|
||||
import { getElementRoot } from '../../../utils/helpers';
|
||||
import { SwipeToCloseDefaults } from '../gestures/swipe-to-close';
|
||||
|
||||
/**
|
||||
@ -9,8 +10,9 @@ export const iosEnterAnimation = (
|
||||
baseEl: HTMLElement,
|
||||
presentingEl?: HTMLElement,
|
||||
): Animation => {
|
||||
const root = getElementRoot(baseEl);
|
||||
const backdropAnimation = createAnimation()
|
||||
.addElement(baseEl.querySelector('ion-backdrop')!)
|
||||
.addElement(root.querySelector('ion-backdrop')!)
|
||||
.fromTo('opacity', 0.01, 'var(--backdrop-opacity)')
|
||||
.beforeStyles({
|
||||
'pointer-events': 'none'
|
||||
@ -18,7 +20,7 @@ export const iosEnterAnimation = (
|
||||
.afterClearStyles(['pointer-events']);
|
||||
|
||||
const wrapperAnimation = createAnimation()
|
||||
.addElement(baseEl.querySelectorAll('.modal-wrapper, .modal-shadow')!)
|
||||
.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow')!)
|
||||
.beforeStyles({ 'opacity': 1 })
|
||||
.fromTo('transform', 'translateY(100vh)', 'translateY(0vh)');
|
||||
|
||||
@ -31,6 +33,7 @@ export const iosEnterAnimation = (
|
||||
if (presentingEl) {
|
||||
const isMobile = window.innerWidth < 768;
|
||||
const hasCardModal = (presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined);
|
||||
const presentingElRoot = getElementRoot(presentingEl);
|
||||
|
||||
const presentingAnimation = createAnimation()
|
||||
.beforeStyles({
|
||||
@ -77,7 +80,7 @@ export const iosEnterAnimation = (
|
||||
.afterStyles({
|
||||
'transform': finalTransform
|
||||
})
|
||||
.addElement(presentingEl.querySelector('.modal-wrapper')!)
|
||||
.addElement(presentingElRoot.querySelector('.modal-wrapper')!)
|
||||
.keyframes([
|
||||
{ offset: 0, filter: 'contrast(1)', transform: 'translateY(0) scale(1)' },
|
||||
{ offset: 1, filter: 'contrast(0.85)', transform: finalTransform }
|
||||
@ -87,7 +90,7 @@ export const iosEnterAnimation = (
|
||||
.afterStyles({
|
||||
'transform': finalTransform
|
||||
})
|
||||
.addElement(presentingEl.querySelector('.modal-shadow')!)
|
||||
.addElement(presentingElRoot.querySelector('.modal-shadow')!)
|
||||
.keyframes([
|
||||
{ offset: 0, opacity: '1', transform: 'translateY(0) scale(1)' },
|
||||
{ offset: 1, opacity: '0', transform: finalTransform }
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Animation } from '../../../interface';
|
||||
import { createAnimation } from '../../../utils/animation/animation';
|
||||
import { getElementRoot } from '../../../utils/helpers';
|
||||
import { SwipeToCloseDefaults } from '../gestures/swipe-to-close';
|
||||
|
||||
/**
|
||||
@ -10,12 +11,13 @@ export const iosLeaveAnimation = (
|
||||
presentingEl?: HTMLElement,
|
||||
duration = 500
|
||||
): Animation => {
|
||||
const root = getElementRoot(baseEl);
|
||||
const backdropAnimation = createAnimation()
|
||||
.addElement(baseEl.querySelector('ion-backdrop')!)
|
||||
.addElement(root.querySelector('ion-backdrop')!)
|
||||
.fromTo('opacity', 'var(--backdrop-opacity)', 0.0);
|
||||
|
||||
const wrapperAnimation = createAnimation()
|
||||
.addElement(baseEl.querySelectorAll('.modal-wrapper, .modal-shadow')!)
|
||||
.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow')!)
|
||||
.beforeStyles({ 'opacity': 1 })
|
||||
.fromTo('transform', 'translateY(0vh)', 'translateY(100vh)');
|
||||
|
||||
@ -28,6 +30,7 @@ export const iosLeaveAnimation = (
|
||||
if (presentingEl) {
|
||||
const isMobile = window.innerWidth < 768;
|
||||
const hasCardModal = (presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined);
|
||||
const presentingElRoot = getElementRoot(presentingEl);
|
||||
|
||||
const presentingAnimation = createAnimation()
|
||||
.beforeClearStyles(['transform'])
|
||||
@ -70,7 +73,7 @@ export const iosLeaveAnimation = (
|
||||
const finalTransform = `translateY(-10px) scale(${toPresentingScale})`;
|
||||
|
||||
presentingAnimation
|
||||
.addElement(presentingEl.querySelector('.modal-wrapper')!)
|
||||
.addElement(presentingElRoot.querySelector('.modal-wrapper')!)
|
||||
.afterStyles({
|
||||
'transform': 'translate3d(0, 0, 0)'
|
||||
})
|
||||
@ -80,7 +83,7 @@ export const iosLeaveAnimation = (
|
||||
]);
|
||||
|
||||
const shadowAnimation = createAnimation()
|
||||
.addElement(presentingEl.querySelector('.modal-shadow')!)
|
||||
.addElement(presentingElRoot.querySelector('.modal-shadow')!)
|
||||
.afterStyles({
|
||||
'transform': 'translateY(0) scale(1)'
|
||||
})
|
||||
|
@ -1,16 +1,18 @@
|
||||
import { Animation } from '../../../interface';
|
||||
import { createAnimation } from '../../../utils/animation/animation';
|
||||
import { getElementRoot } from '../../../utils/helpers';
|
||||
|
||||
/**
|
||||
* Md Modal Enter Animation
|
||||
*/
|
||||
export const mdEnterAnimation = (baseEl: HTMLElement): Animation => {
|
||||
const root = getElementRoot(baseEl);
|
||||
const baseAnimation = createAnimation();
|
||||
const backdropAnimation = createAnimation();
|
||||
const wrapperAnimation = createAnimation();
|
||||
|
||||
backdropAnimation
|
||||
.addElement(baseEl.querySelector('ion-backdrop')!)
|
||||
.addElement(root.querySelector('ion-backdrop')!)
|
||||
.fromTo('opacity', 0.01, 'var(--backdrop-opacity)')
|
||||
.beforeStyles({
|
||||
'pointer-events': 'none'
|
||||
@ -18,7 +20,7 @@ export const mdEnterAnimation = (baseEl: HTMLElement): Animation => {
|
||||
.afterClearStyles(['pointer-events']);
|
||||
|
||||
wrapperAnimation
|
||||
.addElement(baseEl.querySelector('.modal-wrapper')!)
|
||||
.addElement(root.querySelector('.modal-wrapper')!)
|
||||
.keyframes([
|
||||
{ offset: 0, opacity: 0.01, transform: 'translateY(40px)' },
|
||||
{ offset: 1, opacity: 1, transform: 'translateY(0px)' }
|
||||
|
@ -1,17 +1,19 @@
|
||||
import { Animation } from '../../../interface';
|
||||
import { createAnimation } from '../../../utils/animation/animation';
|
||||
import { getElementRoot } from '../../../utils/helpers';
|
||||
|
||||
/**
|
||||
* Md Modal Leave Animation
|
||||
*/
|
||||
export const mdLeaveAnimation = (baseEl: HTMLElement): Animation => {
|
||||
const root = getElementRoot(baseEl);
|
||||
const baseAnimation = createAnimation();
|
||||
const backdropAnimation = createAnimation();
|
||||
const wrapperAnimation = createAnimation();
|
||||
const wrapperEl = baseEl.querySelector('.modal-wrapper')!;
|
||||
const wrapperEl = root.querySelector('.modal-wrapper')!;
|
||||
|
||||
backdropAnimation
|
||||
.addElement(baseEl.querySelector('ion-backdrop')!)
|
||||
.addElement(root.querySelector('ion-backdrop')!)
|
||||
.fromTo('opacity', 'var(--backdrop-opacity)', 0.0);
|
||||
|
||||
wrapperAnimation
|
||||
|
@ -4,7 +4,7 @@
|
||||
// iOS Modals
|
||||
// --------------------------------------------------
|
||||
|
||||
:host:first-of-type {
|
||||
:host(:first-of-type) {
|
||||
--backdrop-opacity: var(--ion-backdrop-opacity, 0.4);
|
||||
}
|
||||
|
||||
@ -58,19 +58,13 @@
|
||||
--height: calc(100% - (120px + var(--ion-safe-area-top) + var(--ion-safe-area-bottom)));
|
||||
--max-width: 720px;
|
||||
--max-height: 1000px;
|
||||
}
|
||||
|
||||
:host(.modal-card) {
|
||||
--backdrop-opacity: 0;
|
||||
--box-shadow: 0px 0px 30px 10px rgba(0, 0, 0, 0.1);
|
||||
|
||||
transition: all 0.5s ease-in-out;
|
||||
|
||||
&:first-of-type {
|
||||
--backdrop-opacity: 0.18;
|
||||
}
|
||||
}
|
||||
|
||||
:host(.modal-card) .modal-shadow {
|
||||
box-shadow: 0px 0px 30px 10px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
// Material Design Modals
|
||||
// --------------------------------------------------
|
||||
|
||||
:host:first-of-type {
|
||||
:host(:first-of-type) {
|
||||
--backdrop-opacity: var(--ion-backdrop-opacity, 0.32);
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
--border-radius: 2px;
|
||||
}
|
||||
|
||||
:host:first-of-type {
|
||||
:host(:first-of-type) {
|
||||
--box-shadow: #{$modal-inset-box-shadow};
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +48,13 @@
|
||||
outline: none;
|
||||
|
||||
contain: strict;
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:host(.modal-interactive) .modal-wrapper,
|
||||
:host(.modal-interactive) ion-backdrop {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
:host(.overlay-hidden) {
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, Watch, h, writeTask } from '@stencil/core';
|
||||
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, State, Watch, h, writeTask } from '@stencil/core';
|
||||
|
||||
import { config } from '../../global/config';
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import { Animation, AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, Gesture, OverlayEventDetail, OverlayInterface } from '../../interface';
|
||||
import { attachComponent, detachComponent } from '../../utils/framework-delegate';
|
||||
import { CoreDelegate, attachComponent, detachComponent } from '../../utils/framework-delegate';
|
||||
import { raf } from '../../utils/helpers';
|
||||
import { BACKDROP, activeAnimations, dismiss, eventMethod, prepareOverlay, present } from '../../utils/overlays';
|
||||
import { getClassMap } from '../../utils/theme';
|
||||
import { deepReady } from '../../utils/transition';
|
||||
@ -16,6 +17,11 @@ import { createSwipeToCloseGesture } from './gestures/swipe-to-close';
|
||||
|
||||
/**
|
||||
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
|
||||
*
|
||||
* @slot = Content is placed inside of the `.modal-content` element.
|
||||
*
|
||||
* @part backdrop - The `ion-backdrop` element.
|
||||
* @part content - The wrapper element for the default slot.
|
||||
*/
|
||||
@Component({
|
||||
tag: 'ion-modal',
|
||||
@ -23,22 +29,31 @@ import { createSwipeToCloseGesture } from './gestures/swipe-to-close';
|
||||
ios: 'modal.ios.scss',
|
||||
md: 'modal.md.scss'
|
||||
},
|
||||
scoped: true
|
||||
shadow: true
|
||||
})
|
||||
export class Modal implements ComponentInterface, OverlayInterface {
|
||||
private gesture?: Gesture;
|
||||
private modalIndex = modalIds++;
|
||||
private modalId?: string;
|
||||
private coreDelegate: FrameworkDelegate = CoreDelegate();
|
||||
private currentTransition?: Promise<any>;
|
||||
private destroyTriggerInteraction?: () => void;
|
||||
|
||||
// Reference to the user's provided modal content
|
||||
private usersElement?: HTMLElement;
|
||||
|
||||
// Whether or not modal is being dismissed via gesture
|
||||
private gestureAnimationDismissing = false;
|
||||
presented = false;
|
||||
lastFocus?: HTMLElement;
|
||||
animation?: Animation;
|
||||
|
||||
@State() presented = false;
|
||||
|
||||
@Element() el!: HTMLIonModalElement;
|
||||
|
||||
/** @internal */
|
||||
@Prop() inline = true;
|
||||
|
||||
/** @internal */
|
||||
@Prop() overlayIndex!: number;
|
||||
|
||||
@ -62,17 +77,20 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
|
||||
/**
|
||||
* The component to display inside of the modal.
|
||||
* @internal
|
||||
*/
|
||||
@Prop() component!: ComponentRef;
|
||||
@Prop() component?: ComponentRef;
|
||||
|
||||
/**
|
||||
* The data to pass to the modal component.
|
||||
* @internal
|
||||
*/
|
||||
@Prop() componentProps?: ComponentProps;
|
||||
|
||||
/**
|
||||
* Additional classes to apply for custom CSS. If multiple classes are
|
||||
* provided they should be separated by spaces.
|
||||
* @internal
|
||||
*/
|
||||
@Prop() cssClass?: string | string[];
|
||||
|
||||
@ -102,6 +120,34 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@Prop() presentingElement?: HTMLElement;
|
||||
|
||||
/**
|
||||
* If `true`, the modal will open. If `false`, the modal will close.
|
||||
* Use this if you need finer grained control over presentation, otherwise
|
||||
* just use the modalController or the `trigger` property.
|
||||
* Note: `isOpen` will not automatically be set back to `false` when
|
||||
* the modal dismisses. You will need to do that in your code.
|
||||
*/
|
||||
@Prop() isOpen = false;
|
||||
|
||||
@Watch('isOpen')
|
||||
onIsOpenChange(newValue: boolean, oldValue: boolean) {
|
||||
if (newValue === true && oldValue === false) {
|
||||
this.present();
|
||||
} else if (newValue === false && oldValue === true) {
|
||||
this.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An ID corresponding to the trigger element that
|
||||
* causes the modal to open when clicked.
|
||||
*/
|
||||
@Prop() trigger: string | undefined;
|
||||
@Watch('trigger')
|
||||
onTriggerChange() {
|
||||
this.configureTriggerInteraction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted after the modal has presented.
|
||||
*/
|
||||
@ -122,6 +168,30 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@Event({ eventName: 'ionModalDidDismiss' }) didDismiss!: EventEmitter<OverlayEventDetail>;
|
||||
|
||||
/**
|
||||
* Emitted after the modal has presented.
|
||||
* Shorthand for ionModalWillDismiss.
|
||||
*/
|
||||
@Event({ eventName: 'didPresent' }) didPresentShorthand!: EventEmitter<void>;
|
||||
|
||||
/**
|
||||
* Emitted before the modal has presented.
|
||||
* Shorthand for ionModalWillPresent.
|
||||
*/
|
||||
@Event({ eventName: 'willPresent' }) willPresentShorthand!: EventEmitter<void>;
|
||||
|
||||
/**
|
||||
* Emitted before the modal has dismissed.
|
||||
* Shorthand for ionModalWillDismiss.
|
||||
*/
|
||||
@Event({ eventName: 'willDismiss' }) willDismissShorthand!: EventEmitter<OverlayEventDetail>;
|
||||
|
||||
/**
|
||||
* Emitted after the modal has dismissed.
|
||||
* Shorthand for ionModalDidDismiss.
|
||||
*/
|
||||
@Event({ eventName: 'didDismiss' }) didDismissShorthand!: EventEmitter<OverlayEventDetail>;
|
||||
|
||||
@Watch('swipeToClose')
|
||||
swipeToCloseChanged(enable: boolean) {
|
||||
if (this.gesture) {
|
||||
@ -135,6 +205,50 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
prepareOverlay(this.el);
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
/**
|
||||
* If user has custom ID set then we should
|
||||
* not assign the default incrementing ID.
|
||||
*/
|
||||
this.modalId = (this.el.hasAttribute('id')) ? this.el.getAttribute('id')! : `ion-modal-${this.modalIndex}`;
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
/**
|
||||
* If modal was rendered with isOpen="true"
|
||||
* then we should open modal immediately.
|
||||
*/
|
||||
if (this.isOpen === true) {
|
||||
raf(() => this.present());
|
||||
}
|
||||
|
||||
this.configureTriggerInteraction();
|
||||
}
|
||||
|
||||
private configureTriggerInteraction = () => {
|
||||
const { trigger, el, destroyTriggerInteraction } = this;
|
||||
|
||||
if (destroyTriggerInteraction) {
|
||||
destroyTriggerInteraction();
|
||||
}
|
||||
|
||||
const triggerEl = (trigger !== undefined) ? document.getElementById(trigger) : null;
|
||||
if (!triggerEl) { return; }
|
||||
|
||||
const configureTriggerInteraction = (triggerEl: HTMLElement, modalEl: HTMLIonModalElement) => {
|
||||
const openModal = () => {
|
||||
modalEl.present();
|
||||
}
|
||||
triggerEl.addEventListener('click', openModal);
|
||||
|
||||
return () => {
|
||||
triggerEl.removeEventListener('click', openModal);
|
||||
}
|
||||
}
|
||||
|
||||
this.destroyTriggerInteraction = configureTriggerInteraction(triggerEl, el);
|
||||
}
|
||||
|
||||
/**
|
||||
* Present the modal overlay after it has been created.
|
||||
*/
|
||||
@ -143,20 +257,42 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
if (this.presented) {
|
||||
return;
|
||||
}
|
||||
const container = this.el.querySelector(`.modal-wrapper`);
|
||||
if (!container) {
|
||||
throw new Error('container is undefined');
|
||||
|
||||
/**
|
||||
* When using an inline modal
|
||||
* and dismissing a modal it is possible to
|
||||
* quickly present the modal while it is
|
||||
* dismissing. We need to await any current
|
||||
* transition to allow the dismiss to finish
|
||||
* before presenting again.
|
||||
*/
|
||||
if (this.currentTransition !== undefined) {
|
||||
await this.currentTransition;
|
||||
}
|
||||
const componentProps = {
|
||||
|
||||
const data = {
|
||||
...this.componentProps,
|
||||
modal: this.el
|
||||
};
|
||||
this.usersElement = await attachComponent(this.delegate, container, this.component, ['ion-page'], componentProps);
|
||||
|
||||
/**
|
||||
* If using modal inline
|
||||
* we potentially need to use the coreDelegate
|
||||
* so that this works in vanilla JS apps
|
||||
*/
|
||||
const delegate = (this.inline) ? this.delegate || this.coreDelegate : this.delegate;
|
||||
|
||||
this.usersElement = await attachComponent(delegate, this.el, this.component, ['ion-page'], data, this.inline);
|
||||
|
||||
await deepReady(this.usersElement);
|
||||
|
||||
writeTask(() => this.el.classList.add('show-modal'));
|
||||
|
||||
await present(this, 'modalEnter', iosEnterAnimation, mdEnterAnimation, this.presentingElement);
|
||||
this.currentTransition = present(this, 'modalEnter', iosEnterAnimation, mdEnterAnimation, this.presentingElement);
|
||||
|
||||
await this.currentTransition;
|
||||
|
||||
this.currentTransition = undefined;
|
||||
|
||||
if (this.swipeToClose) {
|
||||
this.initSwipeToClose();
|
||||
@ -207,11 +343,27 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* When using an inline modal
|
||||
* and presenting a modal it is possible to
|
||||
* quickly dismiss the modal while it is
|
||||
* presenting. We need to await any current
|
||||
* transition to allow the present to finish
|
||||
* before dismissing again.
|
||||
*/
|
||||
if (this.currentTransition !== undefined) {
|
||||
await this.currentTransition;
|
||||
}
|
||||
|
||||
const enteringAnimation = activeAnimations.get(this) || [];
|
||||
const dismissed = await dismiss(this, data, role, 'modalLeave', iosLeaveAnimation, mdLeaveAnimation, this.presentingElement);
|
||||
|
||||
this.currentTransition = dismiss(this, data, role, 'modalLeave', iosLeaveAnimation, mdLeaveAnimation, this.presentingElement);
|
||||
|
||||
const dismissed = await this.currentTransition;
|
||||
|
||||
if (dismissed) {
|
||||
await detachComponent(this.delegate, this.usersElement);
|
||||
const delegate = (this.inline) ? this.delegate || this.coreDelegate : this.delegate;
|
||||
await detachComponent(delegate, this.usersElement);
|
||||
if (this.animation) {
|
||||
this.animation.destroy();
|
||||
}
|
||||
@ -220,6 +372,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
}
|
||||
|
||||
this.animation = undefined;
|
||||
this.currentTransition = undefined;
|
||||
|
||||
return dismissed;
|
||||
}
|
||||
@ -266,6 +419,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
|
||||
render() {
|
||||
const mode = getIonMode(this);
|
||||
const { presented, modalId } = this;
|
||||
|
||||
return (
|
||||
<Host
|
||||
@ -275,8 +429,11 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
class={{
|
||||
[mode]: true,
|
||||
[`modal-card`]: this.presentingElement !== undefined && mode === 'ios',
|
||||
'overlay-hidden': true,
|
||||
'modal-interactive': presented,
|
||||
...getClassMap(this.cssClass)
|
||||
}}
|
||||
id={modalId}
|
||||
style={{
|
||||
zIndex: `${20000 + this.overlayIndex}`,
|
||||
}}
|
||||
@ -287,19 +444,18 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
onIonModalWillDismiss={this.onLifecycle}
|
||||
onIonModalDidDismiss={this.onLifecycle}
|
||||
>
|
||||
<ion-backdrop visible={this.showBackdrop} tappable={this.backdropDismiss}/>
|
||||
<ion-backdrop visible={this.showBackdrop} tappable={this.backdropDismiss} part="backdrop" />
|
||||
|
||||
{mode === 'ios' && <div class="modal-shadow"></div>}
|
||||
|
||||
<div tabindex="0"></div>
|
||||
|
||||
<div
|
||||
role="dialog"
|
||||
class="modal-wrapper ion-overlay-wrapper"
|
||||
part="content"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
@ -311,3 +467,5 @@ const LIFECYCLE_MAP: any = {
|
||||
'ionModalWillDismiss': 'ionViewWillLeave',
|
||||
'ionModalDidDismiss': 'ionViewDidLeave',
|
||||
};
|
||||
|
||||
let modalIds = 0;
|
||||
|
@ -2,6 +2,66 @@
|
||||
|
||||
A Modal is a dialog that appears on top of the app's content, and must be dismissed by the app before interaction can resume. It is useful as a select component when there are a lot of options to choose from, or when filtering items in a list, as well as many other use cases.
|
||||
|
||||
## Presenting
|
||||
|
||||
There are two ways to use `ion-modal`: inline or via the `modalController`. Each method comes with different considerations, so be sure to use the approach that best fits your use case.
|
||||
|
||||
## Inline Modals
|
||||
|
||||
`ion-modal` can be used by writing the component directly in your template. This reduces the number of handlers you need to wire up in order to present the modal. See [Usage](#usage) for an example of how to write a modal inline.
|
||||
|
||||
When using `ion-modal` with Angular, React, or Vue, the component you pass in will be destroyed when the modal is dismissed. As this functionality is provided by the JavaScript framework, using `ion-modal` without a JavaScript framework will not destroy the component you passed in. If this is a needed functionality, we recommend using the `modalController` instead.
|
||||
|
||||
### Angular
|
||||
|
||||
Since the component you passed in needs to be created when the modal is presented and destroyed when the modal is dismissed, we are unable to project the content using `<ng-content>` internally. Instead, we use `<ng-container>` which expects an `<ng-template>` to be passed in. As a result, when passing in your component you will need to wrap it in an `<ng-template>`:
|
||||
|
||||
```html
|
||||
<ion-modal [isOpen]="isModalOpen">
|
||||
<ng-template>
|
||||
<app-modal-content></app-modal-content>
|
||||
</ng-template>
|
||||
</ion-modal>
|
||||
```
|
||||
|
||||
### When to use
|
||||
|
||||
Using a modal inline is useful when you do not want to explicitly wire up click events to open the modal. For example, you can use the `is-open` property to easily present or dismiss a modal based on some state in your application.
|
||||
|
||||
If you need fine grained control over when the modal is presented and dismissed, we recommend you use the `modalController`.
|
||||
|
||||
## Controller Modals
|
||||
|
||||
`ion-modal` can also be presented programmatically by using the `modalController` imported from Ionic Framework. This allows you to have complete control over when a modal is presented above and beyond the customization that inline modals give you. See [Usage](#usage) for an example of how to use the `modalController`.
|
||||
|
||||
### When to use
|
||||
|
||||
We typically recommend that you write your modals inline as it streamlines the amount of code in your application. You should only use the `modalController` for complex use cases where writing a modal inline is impractical.
|
||||
|
||||
## Interfaces
|
||||
|
||||
Below you will find all of the options available to you when using the `modalController`. These options should be supplied when calling `modalController.create()`.
|
||||
|
||||
```typescript
|
||||
interface ModalOptions {
|
||||
component: any;
|
||||
componentProps?: { [key: string]: any };
|
||||
presentingElement?: HTMLElement;
|
||||
showBackdrop?: boolean;
|
||||
backdropDismiss?: boolean;
|
||||
cssClass?: string | string[];
|
||||
animated?: boolean;
|
||||
swipeToClose?: boolean;
|
||||
|
||||
mode?: 'ios' | 'md';
|
||||
keyboardClose?: boolean;
|
||||
id?: string;
|
||||
|
||||
enterAnimation?: AnimationBuilder;
|
||||
leaveAnimation?: AnimationBuilder;
|
||||
}
|
||||
```
|
||||
|
||||
## Dismissing
|
||||
|
||||
The modal can be dismissed after creation by calling the `dismiss()` method on the modal controller. The `onDidDismiss` function can be called to perform an action after the modal is dismissed.
|
||||
@ -756,29 +816,32 @@ export default defineComponent({
|
||||
## 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` |
|
||||
| `isOpen` | `is-open` | If `true`, the modal will open. If `false`, the modal will close. Use this if you need finer grained control over presentation, otherwise just use the modalController or the `trigger` property. Note: `isOpen` will not automatically be set back to `false` when the modal dismisses. You will need to do that in your code. | `boolean` | `false` |
|
||||
| `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` |
|
||||
| `trigger` | `trigger` | An ID corresponding to the trigger element that causes the modal to open when clicked. | `string \| undefined` | `undefined` |
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Description | Type |
|
||||
| --------------------- | --------------------------------------- | -------------------------------------- |
|
||||
| --------------------- | -------------------------------------------------------------------------- | -------------------------------------- |
|
||||
| `didDismiss` | Emitted after the modal has dismissed. Shorthand for ionModalDidDismiss. | `CustomEvent<OverlayEventDetail<any>>` |
|
||||
| `didPresent` | Emitted after the modal has presented. Shorthand for ionModalWillDismiss. | `CustomEvent<void>` |
|
||||
| `ionModalDidDismiss` | Emitted after the modal has dismissed. | `CustomEvent<OverlayEventDetail<any>>` |
|
||||
| `ionModalDidPresent` | Emitted after the modal has presented. | `CustomEvent<void>` |
|
||||
| `ionModalWillDismiss` | Emitted before the modal has dismissed. | `CustomEvent<OverlayEventDetail<any>>` |
|
||||
| `ionModalWillPresent` | Emitted before the modal has presented. | `CustomEvent<void>` |
|
||||
| `willDismiss` | Emitted before the modal has dismissed. Shorthand for ionModalWillDismiss. | `CustomEvent<OverlayEventDetail<any>>` |
|
||||
| `willPresent` | Emitted before the modal has presented. Shorthand for ionModalWillPresent. | `CustomEvent<void>` |
|
||||
|
||||
|
||||
## Methods
|
||||
@ -824,6 +887,21 @@ Type: `Promise<void>`
|
||||
|
||||
|
||||
|
||||
## Slots
|
||||
|
||||
| Slot | Description |
|
||||
| --------------------------------------------------------------- | ----------- |
|
||||
| `"= Content is placed inside of the `.modal-content` element."` | |
|
||||
|
||||
|
||||
## Shadow Parts
|
||||
|
||||
| Part | Description |
|
||||
| ------------ | ----------------------------------------- |
|
||||
| `"backdrop"` | The `ion-backdrop` element. |
|
||||
| `"content"` | The wrapper element for the default slot. |
|
||||
|
||||
|
||||
## CSS Custom Properties
|
||||
|
||||
| Name | Description |
|
||||
|
@ -56,18 +56,15 @@ test('modal: return focus', async () => {
|
||||
await modal.waitForNotVisible(),
|
||||
]);
|
||||
|
||||
modal = await page.find('ion-modal');
|
||||
expect(modal).toBeNull();
|
||||
|
||||
const activeElement = await page.evaluateHandle(() => document.activeElement);
|
||||
const id = await activeElement.evaluate((node) => node.id);
|
||||
expect(id).toEqual('basic-modal');
|
||||
});
|
||||
|
||||
test('modal: basic', async () => {
|
||||
await testModal(DIRECTORY, '#basic-modal');
|
||||
await testModal(DIRECTORY, '#basic-modal', false);
|
||||
});
|
||||
|
||||
test('modal:rtl: basic', async () => {
|
||||
await testModal(DIRECTORY, '#basic-modal', true);
|
||||
await testModal(DIRECTORY, '#basic-modal', false, true);
|
||||
});
|
||||
|
@ -3,9 +3,9 @@ import { testModal } from '../test.utils';
|
||||
const DIRECTORY = 'custom';
|
||||
|
||||
test('modal: custom', async () => {
|
||||
await testModal(DIRECTORY, '#custom-modal');
|
||||
await testModal(DIRECTORY, '#custom-modal', false);
|
||||
});
|
||||
|
||||
test('modal:rtl: custom', async () => {
|
||||
await testModal(DIRECTORY, '#custom-modal', true);
|
||||
await testModal(DIRECTORY, '#custom-modal', false, true);
|
||||
});
|
||||
|
38
core/src/components/modal/test/inline/e2e.ts
Normal file
38
core/src/components/modal/test/inline/e2e.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('modal: inline', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/modal/test/inline?ionic:_testing=true' });
|
||||
const screenshotCompares = [];
|
||||
|
||||
await page.click('ion-button');
|
||||
await page.waitForSelector('ion-modal');
|
||||
|
||||
let modal = await page.find('ion-modal');
|
||||
|
||||
expect(modal).not.toBe(null);
|
||||
await modal.waitForVisible();
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
await modal.callMethod('dismiss');
|
||||
await modal.waitForNotVisible();
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot('dismiss'));
|
||||
|
||||
modal = await page.find('ion-modal');
|
||||
expect(modal).not.toBe(null);
|
||||
|
||||
await page.click('ion-button');
|
||||
await page.waitForSelector('ion-modal');
|
||||
|
||||
let modalAgain = await page.find('ion-modal');
|
||||
|
||||
expect(modalAgain).not.toBe(null);
|
||||
await modalAgain.waitForVisible();
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
for (const screenshotCompare of screenshotCompares) {
|
||||
expect(screenshotCompare).toMatchScreenshot();
|
||||
}
|
||||
});
|
54
core/src/components/modal/test/inline/index.html
Normal file
54
core/src/components/modal/test/inline/index.html
Normal file
@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Modal - Inline</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 src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<ion-app>
|
||||
<div class="ion-page">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Modal - Inline</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-button onclick="openModal(event)">Open Modal</ion-button>
|
||||
|
||||
<ion-modal swipe-to-close="true">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
Modal
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">
|
||||
This is my inline modal content!
|
||||
</ion-content>
|
||||
</ion-modal>
|
||||
</ion-content>
|
||||
</div>
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
const modal = document.querySelector('ion-modal');
|
||||
modal.presentingElement = document.querySelector('.ion-page');
|
||||
|
||||
const openModal = () => {
|
||||
modal.isOpen = true;
|
||||
}
|
||||
|
||||
modal.addEventListener('didDismiss', () => {
|
||||
modal.isOpen = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
47
core/src/components/modal/test/isOpen/e2e.ts
Normal file
47
core/src/components/modal/test/isOpen/e2e.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('should open the modal', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/modal/test/isOpen?ionic:_testing=true' });
|
||||
|
||||
const screenshotCompares = [];
|
||||
|
||||
const trigger = await page.find('#default');
|
||||
trigger.click();
|
||||
|
||||
await page.waitForSelector('ion-modal');
|
||||
const modal = await page.find('ion-modal');
|
||||
await modal.waitForVisible();
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
for (const screenshotCompare of screenshotCompares) {
|
||||
expect(screenshotCompare).toMatchScreenshot();
|
||||
}
|
||||
});
|
||||
|
||||
test('should open the modal then close after a timeout', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/modal/test/isOpen?ionic:_testing=true' });
|
||||
|
||||
const screenshotCompares = [];
|
||||
|
||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
|
||||
const trigger = await page.find('#timeout');
|
||||
trigger.click();
|
||||
|
||||
await page.waitForSelector('ion-modal');
|
||||
const modal = await page.find('ion-modal');
|
||||
await modal.waitForVisible();
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
await ionModalDidDismiss.next();
|
||||
|
||||
await modal.waitForNotVisible();
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
for (const screenshotCompare of screenshotCompares) {
|
||||
expect(screenshotCompare).toMatchScreenshot();
|
||||
}
|
||||
});
|
80
core/src/components/modal/test/isOpen/index.html
Normal file
80
core/src/components/modal/test/isOpen/index.html
Normal file
@ -0,0 +1,80 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Modal - isOpen</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 src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
<style>
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-row-gap: 20px;
|
||||
grid-column-gap: 20px;
|
||||
|
||||
padding: 200px;
|
||||
}
|
||||
.grid-item {
|
||||
margin: 0 auto;
|
||||
}
|
||||
h2 {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
|
||||
color: #6f7378;
|
||||
|
||||
margin-top: 10px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Modal - isOpen</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>Default</h2>
|
||||
<ion-button id="default" onclick="openModal()">Open Modal</ion-button>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<h2>Open, then close after 500ms</h2>
|
||||
<ion-button id="timeout" onclick="openModal(500)">Open Modal</ion-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ion-modal>
|
||||
<ion-content class="ion-padding">
|
||||
Hello World
|
||||
</ion-content>
|
||||
</ion-modal>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
const modal = document.querySelector('ion-modal');
|
||||
|
||||
const openModal = (timeout) => {
|
||||
modal.isOpen = true;
|
||||
|
||||
if (timeout) {
|
||||
setTimeout(() => {
|
||||
modal.isOpen = false;
|
||||
}, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
modal.addEventListener('ionModalDidDismiss', () => {
|
||||
modal.isOpen = false;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -3,9 +3,9 @@ import { testModal } from '../test.utils';
|
||||
const DIRECTORY = 'spec';
|
||||
|
||||
test('modal: card', async () => {
|
||||
await testModal(DIRECTORY, '#card-modal');
|
||||
await testModal(DIRECTORY, '#card-modal', true);
|
||||
});
|
||||
|
||||
test('modal:rtl: card', async () => {
|
||||
await testModal(DIRECTORY, '#card-modal', true);
|
||||
await testModal(DIRECTORY, '#card-modal', true, true);
|
||||
});
|
||||
|
@ -5,6 +5,7 @@ import { generateE2EUrl } from '../../../utils/test/utils';
|
||||
export const testModal = async (
|
||||
type: string,
|
||||
selector: string,
|
||||
expectUnmount = true,
|
||||
rtl = false
|
||||
) => {
|
||||
const pageUrl = generateE2EUrl('modal', type, rtl);
|
||||
@ -41,8 +42,10 @@ export const testModal = async (
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot('dismiss'));
|
||||
|
||||
if (expectUnmount) {
|
||||
modal = await page.find('ion-modal');
|
||||
expect(modal).toBeNull();
|
||||
}
|
||||
|
||||
for (const screenshotCompare of screenshotCompares) {
|
||||
expect(screenshotCompare).toMatchScreenshot();
|
||||
|
19
core/src/components/modal/test/trigger/e2e.ts
Normal file
19
core/src/components/modal/test/trigger/e2e.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('should open modal by left clicking on trigger', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/modal/test/trigger?ionic:_testing=true' });
|
||||
|
||||
const screenshotCompares = [];
|
||||
|
||||
await page.click('#left-click-trigger');
|
||||
await page.waitForSelector('.left-click-modal');
|
||||
|
||||
let modal = await page.find('.left-click-modal');
|
||||
await modal.waitForVisible();
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
for (const screenshotCompare of screenshotCompares) {
|
||||
expect(screenshotCompare).toMatchScreenshot();
|
||||
}
|
||||
});
|
60
core/src/components/modal/test/trigger/index.html
Normal file
60
core/src/components/modal/test/trigger/index.html
Normal file
@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Modal - Triggers</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 src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
<style>
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-row-gap: 20px;
|
||||
grid-column-gap: 20px;
|
||||
|
||||
padding: 200px;
|
||||
}
|
||||
.grid-item {
|
||||
margin: 0 auto;
|
||||
}
|
||||
h2 {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
|
||||
color: #6f7378;
|
||||
|
||||
margin-top: 10px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Modal - Triggers</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>Click</h2>
|
||||
<ion-button id="left-click-trigger">Trigger</ion-button>
|
||||
<ion-modal
|
||||
class="left-click-modal"
|
||||
trigger="left-click-trigger"
|
||||
>
|
||||
<ion-content class="ion-padding">
|
||||
Modal Content
|
||||
</ion-content>
|
||||
</ion-modal>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Meth
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import { AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, OverlayEventDetail, OverlayInterface, PopoverSize, PositionAlign, PositionReference, PositionSide, TriggerAction } from '../../interface';
|
||||
import { attachComponent, detachComponent } from '../../utils/framework-delegate';
|
||||
import { CoreDelegate, attachComponent, detachComponent } from '../../utils/framework-delegate';
|
||||
import { addEventListener, raf } from '../../utils/helpers';
|
||||
import { BACKDROP, dismiss, eventMethod, focusFirstDescendant, prepareOverlay, present } from '../../utils/overlays';
|
||||
import { isPlatform } from '../../utils/platform';
|
||||
@ -15,28 +15,6 @@ import { mdEnterAnimation } from './animations/md.enter';
|
||||
import { mdLeaveAnimation } from './animations/md.leave';
|
||||
import { configureDismissInteraction, configureKeyboardInteraction, configureTriggerInteraction } from './utils';
|
||||
|
||||
const CoreDelegate = () => {
|
||||
let Cmp: any;
|
||||
const attachViewToDom = (parentElement: HTMLElement) => {
|
||||
Cmp = parentElement;
|
||||
const app = document.querySelector('ion-app') || document.body;
|
||||
if (app && Cmp) {
|
||||
app.appendChild(Cmp);
|
||||
}
|
||||
|
||||
return Cmp;
|
||||
}
|
||||
|
||||
const removeViewFromDom = () => {
|
||||
if (Cmp) {
|
||||
Cmp.remove();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return { attachViewToDom, removeViewFromDom }
|
||||
}
|
||||
|
||||
/**
|
||||
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
|
||||
*
|
||||
|
@ -3,7 +3,6 @@
|
||||
A Popover is a dialog that appears on top of the current page. It can be used for anything, but generally it is used for overflow actions that don't fit in the navigation bar.
|
||||
|
||||
There are two ways to use `ion-popover`: inline or via the `popoverController`. Each method comes with different considerations, so be sure to use the approach that best fits your use case.
|
||||
<<<<<<< HEAD
|
||||
|
||||
## Inline Popovers
|
||||
|
||||
@ -79,68 +78,6 @@ type PositionSide = 'top' | 'right' | 'bottom' | 'left' | 'start' | 'end';
|
||||
type PositionAlign = 'start' | 'center' | 'end';
|
||||
```
|
||||
|
||||
=======
|
||||
|
||||
## Inline Popovers
|
||||
|
||||
`ion-popover` can be used by writing the component directly in your template. This reduces the number of handlers you need to wire up in order to present the popover. See [Usage](#usage) for an example of how to write a popover inline.
|
||||
|
||||
When using `ion-popover` with Angular, React, or Vue, the component you pass in will be destroyed when the popover is dismissed. If you are not using a JavaScript Framework, you should use the `component` property to pass in the name of a Web Component. This Web Component will be destroyed when the popover is dismissed, and a new instance will be created if the popover is presented again.
|
||||
|
||||
### Angular
|
||||
|
||||
Since the component you passed in needs to be created when the popover is presented and destroyed when the popover is dismissed, we are unable to project the content using `<ng-content>` internally. Instead, we use `<ng-container>` which expects an `<ng-template>` to be passed in. As a result, when passing in your component you will need to wrap it in an `<ng-template>`:
|
||||
|
||||
```html
|
||||
<ion-popover [isOpen]="isPopoverOpen">
|
||||
<ng-template>
|
||||
<app-popover-content></app-popover-content>
|
||||
</ng-template>
|
||||
</ion-popover>
|
||||
```
|
||||
|
||||
Liam: Usage will be filled out via desktop popover PR.
|
||||
|
||||
### When to use
|
||||
|
||||
Liam: Will be filled out via desktop popover PR.
|
||||
|
||||
## Controller Popovers
|
||||
|
||||
`ion-popover` can also be presented programmatically by using the `popoverController` imported from Ionic Framework. This allows you to have complete control over when a popover is presented above and beyond the customization that inline popovers give you. See [Usage](#usage) for an example of how to use the `popoverController`.
|
||||
|
||||
Liam: Usage will be filled out via desktop popover PR.
|
||||
|
||||
|
||||
### When to use
|
||||
|
||||
Liam: Will be filled out via desktop popover PR.
|
||||
|
||||
## Interfaces
|
||||
|
||||
Below you will find all of the options available to you when using the `popoverController`. These options should be supplied when calling `popoverController.create()`.
|
||||
|
||||
```typescript
|
||||
interface PopoverOptions {
|
||||
component: any;
|
||||
componentProps?: { [key: string]: any };
|
||||
showBackdrop?: boolean;
|
||||
backdropDismiss?: boolean;
|
||||
translucent?: boolean;
|
||||
cssClass?: string | string[];
|
||||
event?: Event;
|
||||
animated?: boolean;
|
||||
|
||||
mode?: 'ios' | 'md';
|
||||
keyboardClose?: boolean;
|
||||
id?: string;
|
||||
|
||||
enterAnimation?: AnimationBuilder;
|
||||
leaveAnimation?: AnimationBuilder;
|
||||
}
|
||||
```
|
||||
>>>>>>> origin/next
|
||||
|
||||
## Customization
|
||||
|
||||
Popover uses scoped encapsulation, which means it will automatically scope its CSS by appending each of the styles with an additional class at runtime. Overriding scoped selectors in CSS requires a [higher specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity) selector.
|
||||
|
@ -1,35 +1,35 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('popover - arrow side: top', async () => {
|
||||
await testPopover('top');
|
||||
await testPopover('top', false);
|
||||
});
|
||||
|
||||
test('popover - arrow side: right', async () => {
|
||||
await testPopover('right');
|
||||
await testPopover('right', false);
|
||||
});
|
||||
|
||||
test('popover - arrow side: bottom', async () => {
|
||||
await testPopover('bottom');
|
||||
await testPopover('bottom', false);
|
||||
});
|
||||
|
||||
test('popover - arrow side: left', async () => {
|
||||
await testPopover('left');
|
||||
await testPopover('left', false);
|
||||
});
|
||||
|
||||
test('popover - arrow side: start', async () => {
|
||||
await testPopover('start');
|
||||
await testPopover('start'), false;
|
||||
});
|
||||
|
||||
test('popover - arrow side: end', async () => {
|
||||
await testPopover('end');
|
||||
await testPopover('end', false);
|
||||
});
|
||||
|
||||
test('popover - arrow side: start, rtl', async () => {
|
||||
await testPopover('start', true);
|
||||
await testPopover('start', false, true);
|
||||
});
|
||||
|
||||
test('popover - arrow side: end, rtl', async () => {
|
||||
await testPopover('end', true);
|
||||
await testPopover('end', false, true);
|
||||
});
|
||||
|
||||
|
||||
|
@ -62,21 +62,21 @@ test('popover: custom class', async () => {
|
||||
*/
|
||||
|
||||
test('popover:rtl: basic', async () => {
|
||||
await testPopover(DIRECTORY, '#basic-popover', true);
|
||||
await testPopover(DIRECTORY, '#basic-popover', true, true);
|
||||
});
|
||||
|
||||
test('popover:rtl: translucent', async () => {
|
||||
await testPopover(DIRECTORY, '#translucent-popover', true);
|
||||
await testPopover(DIRECTORY, '#translucent-popover', true, true);
|
||||
});
|
||||
|
||||
test('popover:rtl: long list', async () => {
|
||||
await testPopover(DIRECTORY, '#long-list-popover', true);
|
||||
await testPopover(DIRECTORY, '#long-list-popover', true, true);
|
||||
});
|
||||
|
||||
test('popover:rtl: no event', async () => {
|
||||
await testPopover(DIRECTORY, '#no-event-popover', true);
|
||||
await testPopover(DIRECTORY, '#no-event-popover', true, true);
|
||||
});
|
||||
|
||||
test('popover:rtl: custom class', async () => {
|
||||
await testPopover(DIRECTORY, '#custom-class-popover', true);
|
||||
await testPopover(DIRECTORY, '#custom-class-popover', true, true);
|
||||
});
|
||||
|
@ -20,7 +20,6 @@ test('popover: inline', async () => {
|
||||
screenshotCompares.push(await page.compareScreenshot('dismiss'));
|
||||
|
||||
popover = await page.find('ion-popover');
|
||||
expect(popover).toBeNull();
|
||||
|
||||
await page.click('ion-button');
|
||||
await page.waitForSelector('ion-popover');
|
||||
|
@ -63,7 +63,6 @@
|
||||
const popover = document.querySelector('ion-popover');
|
||||
|
||||
const openPopover = (ev, timeout) => {
|
||||
console.log(ev, timeout)
|
||||
popover.event = ev;
|
||||
popover.isOpen = true;
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { generateE2EUrl } from '../../../utils/test/utils';
|
||||
export const testPopover = async (
|
||||
type: string,
|
||||
selector: string,
|
||||
expectUnmount = true,
|
||||
rtl = false
|
||||
) => {
|
||||
try {
|
||||
@ -29,8 +30,10 @@ export const testPopover = async (
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot('dismiss'));
|
||||
|
||||
if (expectUnmount) {
|
||||
popover = await page.find('ion-popover');
|
||||
expect(popover).toBeNull();
|
||||
}
|
||||
|
||||
for (const screenshotCompare of screenshotCompares) {
|
||||
expect(screenshotCompare).toMatchScreenshot();
|
||||
|
@ -70,6 +70,16 @@ html.ios ion-modal .ion-page {
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Card style modal on iPadOS
|
||||
* should only have backdrop on first instance.
|
||||
*/
|
||||
@media screen and (min-width: 768px) {
|
||||
html.ios ion-modal.modal-card:first-of-type {
|
||||
--backdrop-opacity: 0.18;
|
||||
}
|
||||
}
|
||||
|
||||
// Ionic Colors
|
||||
// --------------------------------------------------
|
||||
// Generates the color classes and variables based on the
|
||||
|
@ -45,3 +45,81 @@ export const detachComponent = (delegate: FrameworkDelegate | undefined, element
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
export const CoreDelegate = () => {
|
||||
let BaseComponent: any;
|
||||
let Reference: any;
|
||||
const attachViewToDom = async (
|
||||
parentElement: HTMLElement,
|
||||
userComponent: any,
|
||||
userComponentProps: any = {},
|
||||
cssClasses: string[] = []
|
||||
) => {
|
||||
BaseComponent = parentElement;
|
||||
/**
|
||||
* If passing in a component via the `component` props
|
||||
* we need to append it inside of our overlay component.
|
||||
*/
|
||||
if (userComponent) {
|
||||
/**
|
||||
* If passing in the tag name, create
|
||||
* the element otherwise just get a reference
|
||||
* to the component.
|
||||
*/
|
||||
const el: any = (typeof userComponent === 'string')
|
||||
? BaseComponent.ownerDocument && BaseComponent.ownerDocument.createElement(userComponent)
|
||||
: userComponent;
|
||||
|
||||
/**
|
||||
* Add any css classes passed in
|
||||
* via the cssClasses prop on the overlay.
|
||||
*/
|
||||
cssClasses.forEach(c => el.classList.add(c));
|
||||
|
||||
/**
|
||||
* Add any props passed in
|
||||
* via the componentProps prop on the overlay.
|
||||
*/
|
||||
Object.assign(el, userComponentProps);
|
||||
|
||||
/**
|
||||
* Finally, append the component
|
||||
* inside of the overlay component.
|
||||
*/
|
||||
BaseComponent.appendChild(el);
|
||||
|
||||
await new Promise(resolve => componentOnReady(el, resolve));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the root of the app and
|
||||
* add the overlay there.
|
||||
*/
|
||||
const app = document.querySelector('ion-app') || document.body;
|
||||
|
||||
/**
|
||||
* Create a placeholder comment so that
|
||||
* we can return this component to where
|
||||
* it was previously.
|
||||
*/
|
||||
Reference = document.createComment('ionic teleport');
|
||||
BaseComponent.parentNode.insertBefore(Reference, BaseComponent);
|
||||
|
||||
app.appendChild(BaseComponent);
|
||||
|
||||
return BaseComponent;
|
||||
}
|
||||
|
||||
const removeViewFromDom = () => {
|
||||
/**
|
||||
* Return component to where it was previously in the DOM.
|
||||
*/
|
||||
if (BaseComponent && Reference) {
|
||||
Reference.parentNode.insertBefore(BaseComponent, Reference);
|
||||
Reference.remove();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return { attachViewToDom, removeViewFromDom }
|
||||
}
|
||||
|
@ -445,6 +445,9 @@ export const dismiss = async (
|
||||
|
||||
activeAnimations.delete(overlay);
|
||||
|
||||
// Make overlay hidden again in case it is being reused
|
||||
overlay.el.classList.add('overlay-hidden');
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
@ -1,12 +1,8 @@
|
||||
import { ModalOptions, modalController } from '@ionic/core';
|
||||
import { JSX } from '@ionic/core';
|
||||
|
||||
import { createOverlayComponent } from './createOverlayComponent';
|
||||
import { createInlineOverlayComponent } from './createInlineOverlayComponent'
|
||||
|
||||
export type ReactModalOptions = Omit<ModalOptions, 'component' | 'componentProps'> & {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const IonModal = /*@__PURE__*/ createOverlayComponent<
|
||||
ReactModalOptions,
|
||||
export const IonModal = /*@__PURE__*/ createInlineOverlayComponent<
|
||||
JSX.IonModal,
|
||||
HTMLIonModalElement
|
||||
>('IonModal', modalController);
|
||||
>('ion-modal');
|
||||
|
@ -18,11 +18,6 @@ function generateOverlays() {
|
||||
controller: 'loadingController',
|
||||
name: 'IonLoading'
|
||||
},
|
||||
{
|
||||
tag: 'ion-modal',
|
||||
controller: 'modalController',
|
||||
name: 'IonModal'
|
||||
},
|
||||
{
|
||||
tag: 'ion-picker',
|
||||
controller: 'pickerController',
|
||||
|
22
packages/vue/src/components/IonModal.ts
Normal file
22
packages/vue/src/components/IonModal.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { defineComponent, h, ref, onMounted } from 'vue';
|
||||
|
||||
export const IonModal = defineComponent({
|
||||
name: 'IonModal',
|
||||
setup(_, { attrs, slots }) {
|
||||
const isOpen = ref(false);
|
||||
const elementRef = ref();
|
||||
|
||||
onMounted(() => {
|
||||
elementRef.value.addEventListener('will-present', () => isOpen.value = true);
|
||||
elementRef.value.addEventListener('did-dismiss', () => isOpen.value = false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
return h(
|
||||
'ion-modal',
|
||||
{ ...attrs, ref: elementRef },
|
||||
(isOpen.value) ? slots : undefined
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
@ -5,7 +5,6 @@ import {
|
||||
actionSheetController,
|
||||
alertController,
|
||||
loadingController,
|
||||
modalController,
|
||||
pickerController,
|
||||
toastController
|
||||
} from '@ionic/core';
|
||||
@ -18,8 +17,6 @@ export const IonAlert = /*@__PURE__*/defineOverlayContainer<JSX.IonAlert>('ion-a
|
||||
|
||||
export const IonLoading = /*@__PURE__*/defineOverlayContainer<JSX.IonLoading>('ion-loading', ['animated', 'backdropDismiss', 'cssClass', 'duration', 'enterAnimation', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'showBackdrop', 'spinner', 'translucent'], loadingController);
|
||||
|
||||
export const IonModal = /*@__PURE__*/defineOverlayContainer<JSX.IonModal>('ion-modal', ['animated', 'backdropDismiss', 'component', 'componentProps', 'cssClass', 'enterAnimation', 'keyboardClose', 'leaveAnimation', 'mode', 'presentingElement', 'showBackdrop', 'swipeToClose'], modalController);
|
||||
|
||||
export const IonPicker = /*@__PURE__*/defineOverlayContainer<JSX.IonPicker>('ion-picker', ['animated', 'backdropDismiss', 'buttons', 'columns', 'cssClass', 'duration', 'enterAnimation', 'keyboardClose', 'leaveAnimation', 'mode', 'showBackdrop'], pickerController);
|
||||
|
||||
export const IonToast = /*@__PURE__*/defineOverlayContainer<JSX.IonToast>('ion-toast', ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'position', 'translucent'], toastController);
|
||||
|
@ -14,6 +14,7 @@ export { IonNav } from './components/IonNav';
|
||||
export { IonIcon } from './components/IonIcon';
|
||||
export { IonApp } from './components/IonApp';
|
||||
export { IonPopover } from './components/IonPopover';
|
||||
export { IonModal } from './components/IonModal';
|
||||
|
||||
export * from './components/Overlays';
|
||||
|
||||
|
@ -99,13 +99,12 @@
|
||||
|
||||
<ion-modal
|
||||
:is-open="isModalOpen"
|
||||
:componentProps="overlayProps"
|
||||
@willPresent="onModalWillPresent"
|
||||
@didPresent="onModalDidPresent"
|
||||
@willDismiss="onModalWillDismiss"
|
||||
@didDismiss="onModalDidDismiss"
|
||||
>
|
||||
<ModalContent></ModalContent>
|
||||
<ModalContent :title="overlayProps.title"></ModalContent>
|
||||
</ion-modal>
|
||||
|
||||
<ion-popover
|
||||
|
@ -26,9 +26,15 @@ const testComponent = (overlay, shadow = false) => {
|
||||
cy.get(overlay).shadow().find('ion-backdrop').click({ force: true });
|
||||
} else {
|
||||
cy.get(`${overlay} ion-backdrop`).click({ force: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Overlay components that are shadow can be used inline
|
||||
* so they should not be removed from the DOM. This test
|
||||
* might need to be revisited if other overlay components
|
||||
* are converted to shadow as well.
|
||||
*/
|
||||
cy.get(overlay).should('not.exist');
|
||||
}
|
||||
}
|
||||
|
||||
describe('Overlays', () => {
|
||||
@ -50,7 +56,7 @@ describe('Overlays', () => {
|
||||
});
|
||||
|
||||
it(`should open and close ion-modal via controller`, () => {
|
||||
testController('ion-modal');
|
||||
testController('ion-modal', true);
|
||||
});
|
||||
|
||||
it(`should open and close ion-popover via controller`, () => {
|
||||
@ -82,7 +88,7 @@ describe('Overlays', () => {
|
||||
});
|
||||
|
||||
it(`should open and close ion-modal via component`, () => {
|
||||
testComponent('ion-modal');
|
||||
testComponent('ion-modal', true);
|
||||
});
|
||||
|
||||
it(`should open and close ion-popover via component`, () => {
|
||||
|
Reference in New Issue
Block a user