Skip to content

Commit

Permalink
feat(modal): modals can now be used inline (#23341)
Browse files Browse the repository at this point in the history
resolves #20117, resolves #20263
  • Loading branch information
liamdebeasi authored Jun 1, 2021
1 parent 8c6163c commit 3be1c3d
Show file tree
Hide file tree
Showing 41 changed files with 860 additions and 205 deletions.
9 changes: 8 additions & 1 deletion BREAKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
39 changes: 39 additions & 0 deletions angular/src/directives/overlays/ion-modal.ts
Original file line number Diff line number Diff line change
@@ -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"]);
}
}
2 changes: 2 additions & 0 deletions angular/src/ionic-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -68,6 +69,7 @@ const DECLARATIONS = [
IonMenu,
IonMenuButton,
IonMenuToggle,
IonModal,
IonNav,
IonNavLink,
IonNote,
Expand Down
13 changes: 9 additions & 4 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
38 changes: 36 additions & 2 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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.
*/
Expand All @@ -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.
*/
Expand All @@ -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.
Expand All @@ -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 {
/**
Expand Down
11 changes: 7 additions & 4 deletions core/src/components/modal/animations/ios.enter.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand All @@ -9,16 +10,17 @@ 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'
})
.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)');

Expand All @@ -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({
Expand Down Expand Up @@ -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 }
Expand All @@ -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 }
Expand Down
11 changes: 7 additions & 4 deletions core/src/components/modal/animations/ios.leave.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand All @@ -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)');

Expand All @@ -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'])
Expand Down Expand Up @@ -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)'
})
Expand All @@ -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)'
})
Expand Down
6 changes: 4 additions & 2 deletions core/src/components/modal/animations/md.enter.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
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'
})
.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)' }
Expand Down
6 changes: 4 additions & 2 deletions core/src/components/modal/animations/md.leave.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading

0 comments on commit 3be1c3d

Please sign in to comment.