Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(modal): modals can now be used inline #23341

Merged
merged 40 commits into from
Jun 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3a85c51
update popover to work inline
liamdebeasi Apr 23, 2021
4b96551
add basic test, fix some bugs
liamdebeasi Apr 23, 2021
cf2a79b
Add shorthand events
liamdebeasi Apr 23, 2021
0468626
add framework integrations
liamdebeasi Apr 23, 2021
753a116
add support for opening popover immediately
liamdebeasi Apr 23, 2021
ff2a207
fix angular, core tests
liamdebeasi Apr 23, 2021
ceb0555
fix vue test
liamdebeasi Apr 23, 2021
e4f08bb
add popover for ionic vue
liamdebeasi Apr 26, 2021
cc6c620
add popover for ionic react
liamdebeasi Apr 27, 2021
f9bb970
run linter
liamdebeasi Apr 27, 2021
8009183
convert popover to shadow dom
liamdebeasi Apr 28, 2021
3677e74
grab correct element for delegate
liamdebeasi Apr 28, 2021
6651c47
add popover for ionic angular
liamdebeasi Apr 28, 2021
dcf4fe4
fix vue tests
liamdebeasi Apr 28, 2021
a6fdba7
fix focus trap with shadow dom
liamdebeasi Apr 28, 2021
7667f4e
lint
liamdebeasi Apr 28, 2021
ce71633
lint angular
liamdebeasi Apr 29, 2021
ede758c
update docs
liamdebeasi Apr 29, 2021
f7a76ee
fix test
liamdebeasi Apr 29, 2021
d3497b2
convert modal to shadow
liamdebeasi Apr 30, 2021
ab262a9
improve overlay animations with shadow dom
liamdebeasi Apr 29, 2021
94645b7
do not pass in shadow root
liamdebeasi Apr 30, 2021
61389fd
Merge remote-tracking branch 'origin/inline-popover' into inline-modal
liamdebeasi Apr 30, 2021
4843056
clean up code
liamdebeasi Apr 30, 2021
dc5e7aa
add shadow parts
liamdebeasi Apr 30, 2021
1202d48
fix animation, improve styles
liamdebeasi Apr 30, 2021
b395dca
update breaking
liamdebeasi Apr 30, 2021
e69ae30
add test
liamdebeasi Apr 30, 2021
6bc8a66
update readme
liamdebeasi Apr 30, 2021
68fc304
sync with next
liamdebeasi May 4, 2021
d2de2c9
add ionic vue integration for modal
liamdebeasi May 4, 2021
3bbce37
return components to previous location rather than remove them from dom
liamdebeasi May 6, 2021
9cf673a
sync with next
liamdebeasi May 21, 2021
77b2bb5
add fw integrations
liamdebeasi May 21, 2021
e71b3a1
clean up
liamdebeasi May 21, 2021
cf1c82d
lint angular
liamdebeasi May 28, 2021
048b2dc
hide modal after dismiss
liamdebeasi May 28, 2021
80a24dc
fix vue tests
liamdebeasi May 28, 2021
0a07dca
fix core tests, add trigger prop
liamdebeasi May 28, 2021
a6d1e5f
fix popover test
liamdebeasi May 28, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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