From 111f526d055428ec29553afa24d8657674fea180 Mon Sep 17 00:00:00 2001 From: "Manu Mtz.-Almeida" Date: Fri, 23 Feb 2018 00:49:45 +0100 Subject: [PATCH] fix(overlay): fix overlay indexing --- packages/core/src/components.d.ts | 7 +++ .../action-sheet-controller.tsx | 58 ++++------------- .../components/action-sheet/action-sheet.scss | 2 + .../components/action-sheet/action-sheet.tsx | 10 +-- .../src/components/action-sheet/readme.md | 10 +++ .../alert-controller/alert-controller.tsx | 55 ++++------------ .../core/src/components/alert/alert.ios.scss | 1 + packages/core/src/components/alert/alert.tsx | 19 +++--- packages/core/src/components/alert/readme.md | 10 +++ .../core/src/components/backdrop/backdrop.tsx | 1 + .../loading-controller/loading-controller.tsx | 50 +++------------ .../core/src/components/loading/loading.tsx | 10 +-- .../core/src/components/loading/readme.md | 10 +++ .../modal-controller/modal-controller.tsx | 51 +++------------ packages/core/src/components/modal/modal.tsx | 13 ++-- packages/core/src/components/modal/readme.md | 10 +++ .../picker-controller/picker-controller.tsx | 51 +++------------ .../core/src/components/picker/picker.tsx | 12 ++-- packages/core/src/components/picker/readme.md | 10 +++ .../popover-controller/popover-controller.tsx | 52 +++------------- .../core/src/components/popover/popover.tsx | 9 +-- .../core/src/components/popover/readme.md | 10 +++ .../toast-controller/toast-controller.tsx | 51 +++------------ packages/core/src/components/toast/readme.md | 10 +++ packages/core/src/components/toast/toast.tsx | 9 +-- packages/core/src/utils/helpers.ts | 10 +++ packages/core/src/utils/overlay-constants.ts | 2 - packages/core/src/utils/overlays.ts | 62 +++++++++++++++++++ 28 files changed, 266 insertions(+), 339 deletions(-) delete mode 100644 packages/core/src/utils/overlay-constants.ts create mode 100644 packages/core/src/utils/overlays.ts diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 1db2ab8c07..7e43dbf63c 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -118,6 +118,7 @@ declare global { enableBackdropDismiss?: boolean; enterAnimation?: AnimationBuilder; leaveAnimation?: AnimationBuilder; + overlayId?: number; subTitle?: string; title?: string; translucent?: boolean; @@ -188,6 +189,7 @@ declare global { inputs?: AlertInput[]; leaveAnimation?: AnimationBuilder; message?: string; + overlayId?: number; subTitle?: string; title?: string; translucent?: boolean; @@ -1672,6 +1674,7 @@ declare global { enableBackdropDismiss?: boolean; enterAnimation?: AnimationBuilder; leaveAnimation?: AnimationBuilder; + overlayId?: number; showBackdrop?: boolean; spinner?: string; translucent?: boolean; @@ -1842,6 +1845,7 @@ declare global { enterAnimation?: AnimationBuilder; leaveAnimation?: AnimationBuilder; mode?: 'ios' | 'md'; + overlayId?: number; showBackdrop?: boolean; willAnimate?: boolean; } @@ -2131,6 +2135,7 @@ declare global { enableBackdropDismiss?: boolean; enterAnimation?: AnimationBuilder; leaveAnimation?: AnimationBuilder; + overlayId?: number; showBackdrop?: boolean; willAnimate?: boolean; } @@ -2232,6 +2237,7 @@ declare global { ev?: Event; leaveAnimation?: AnimationBuilder; mode?: 'ios' | 'md'; + overlayId?: number; showBackdrop?: boolean; translucent?: boolean; willAnimate?: boolean; @@ -3510,6 +3516,7 @@ declare global { enterAnimation?: AnimationBuilder; leaveAnimation?: AnimationBuilder; message?: string; + overlayId?: number; position?: string; showCloseButton?: boolean; translucent?: boolean; diff --git a/packages/core/src/components/action-sheet-controller/action-sheet-controller.tsx b/packages/core/src/components/action-sheet-controller/action-sheet-controller.tsx index 6ebbfed3c5..071b0be304 100644 --- a/packages/core/src/components/action-sheet-controller/action-sheet-controller.tsx +++ b/packages/core/src/components/action-sheet-controller/action-sheet-controller.tsx @@ -1,29 +1,31 @@ import { Component, Listen, Method } from '@stencil/core'; import { ActionSheetEvent, ActionSheetOptions, OverlayController } from '../../index'; - -let ids = 0; -const actionSheets = new Map(); +import { createOverlay, getTopOverlay, dismissOverlay, removeLastOverlay } from '../../utils/overlays'; @Component({ tag: 'ion-action-sheet-controller' }) export class ActionSheetController implements OverlayController { + private actionSheets = new Map(); + + @Listen('body:ionActionSheetWillPresent') protected actionSheetWillPresent(ev: ActionSheetEvent) { - actionSheets.set(ev.target.actionSheetId, ev.target); + this.actionSheets.set(ev.target.overlayId, ev.target); } - @Listen('body:ionActionSheetWillDismiss, body:ionActionSheetDidUnload') + @Listen('body:ionActionSheetWillDismiss') + @Listen('body:ionActionSheetDidUnload') protected actionSheetWillDismiss(ev: ActionSheetEvent) { - actionSheets.delete(ev.target.actionSheetId); + this.actionSheets.delete(ev.target.overlayId); } @Listen('body:keyup.escape') protected escapeKeyUp() { - removeLastActionSheet(); + removeLastOverlay(this.actionSheets); } /* @@ -31,34 +33,15 @@ export class ActionSheetController implements OverlayController { */ @Method() create(opts?: ActionSheetOptions): Promise { - // create ionic's wrapping ion-actionSheet component - const actionSheetElement = document.createElement('ion-action-sheet'); - - // give this actionSheet a unique id - actionSheetElement.actionSheetId = ids++; - - // convert the passed in actionSheet options into props - // that get passed down into the new actionSheet - Object.assign(actionSheetElement, opts); - - // append the actionSheet element to the document body - const appRoot = document.querySelector('ion-app') || document.body; - appRoot.appendChild(actionSheetElement); - - return actionSheetElement.componentOnReady(); + return createOverlay('ion-action-sheet', opts); } /* * Dismiss the open action sheet overlay. */ @Method() - dismiss(data?: any, role?: any, actionSheetId = -1) { - actionSheetId = actionSheetId >= 0 ? actionSheetId : getHighestId(); - const actionSheet = actionSheets.get(actionSheetId); - if (!actionSheet) { - return Promise.reject('action-sheet does not exist'); - } - return actionSheet.dismiss(data, role); + dismiss(data?: any, role?: string, actionSheetId = -1) { + return dismissOverlay(data, role, this.actionSheets, actionSheetId); } /* @@ -66,21 +49,6 @@ export class ActionSheetController implements OverlayController { */ @Method() getTop() { - return actionSheets.get(getHighestId()); + return getTopOverlay(this.actionSheets); } } - -function getHighestId() { - let minimum = -1; - actionSheets.forEach((_actionSheet: HTMLIonActionSheetElement, id: number) => { - if (id > minimum) { - minimum = id; - } - }); - return minimum; -} - -function removeLastActionSheet() { - const toRemove = actionSheets.get(getHighestId()); - return toRemove ? toRemove.dismiss() : Promise.resolve(); -} diff --git a/packages/core/src/components/action-sheet/action-sheet.scss b/packages/core/src/components/action-sheet/action-sheet.scss index d21b3f9bb1..75f5e08a69 100644 --- a/packages/core/src/components/action-sheet/action-sheet.scss +++ b/packages/core/src/components/action-sheet/action-sheet.scss @@ -76,6 +76,8 @@ ion-action-sheet { flex-shrink: 2; pointer-events: all; + + overscroll-behavior: contain; } .action-sheet-group-cancel { diff --git a/packages/core/src/components/action-sheet/action-sheet.tsx b/packages/core/src/components/action-sheet/action-sheet.tsx index 08d97fa074..48ac1ae527 100644 --- a/packages/core/src/components/action-sheet/action-sheet.tsx +++ b/packages/core/src/components/action-sheet/action-sheet.tsx @@ -3,6 +3,7 @@ import { Animation, AnimationBuilder, AnimationController, Config, DomController import { domControllerAsync, isDef, playAnimationAsync } from '../../utils/helpers'; import { createThemedClasses, getClassMap } from '../../utils/theme'; +import { OverlayInterface, BACKDROP } from '../../utils/overlays'; import iosEnterAnimation from './animations/ios.enter'; import iosLeaveAnimation from './animations/ios.leave'; @@ -20,10 +21,10 @@ import mdLeaveAnimation from './animations/md.leave'; theme: 'action-sheet' } }) -export class ActionSheet { +export class ActionSheet implements OverlayInterface { + mode: string; color: string; - actionSheetId: number; private animation: Animation | null = null; @@ -32,6 +33,7 @@ export class ActionSheet { @Prop({ connect: 'ion-animation-controller' }) animationCtrl: AnimationController; @Prop({ context: 'config' }) config: Config; @Prop({ context: 'dom' }) dom: DomController; + @Prop() overlayId: number; /** * Animation to use when the action sheet is presented. @@ -128,7 +130,7 @@ export class ActionSheet { @Listen('ionBackdropTap') protected onBackdropTap() { - this.dismiss(); + this.dismiss(null, BACKDROP); } /** @@ -138,7 +140,7 @@ export class ActionSheet { present() { this.ionActionSheetWillPresent.emit(); - this.el.style.zIndex = `${20000 + this.actionSheetId}`; + this.el.style.zIndex = `${20000 + this.overlayId}`; // get the user's animation fn if one was provided const animationBuilder = this.enterAnimation || this.config.get('actionSheetEnter', this.mode === 'ios' ? iosEnterAnimation : mdEnterAnimation); diff --git a/packages/core/src/components/action-sheet/readme.md b/packages/core/src/components/action-sheet/readme.md index 76758996fd..fc9c08a83b 100644 --- a/packages/core/src/components/action-sheet/readme.md +++ b/packages/core/src/components/action-sheet/readme.md @@ -96,6 +96,11 @@ Animation to use when the action sheet is presented. Animation to use when the action sheet is dismissed. +#### overlayId + +number + + #### subTitle string @@ -162,6 +167,11 @@ Animation to use when the action sheet is presented. Animation to use when the action sheet is dismissed. +#### overlay-id + +number + + #### sub-title string diff --git a/packages/core/src/components/alert-controller/alert-controller.tsx b/packages/core/src/components/alert-controller/alert-controller.tsx index 95ff2accf5..2dd15eb9e5 100644 --- a/packages/core/src/components/alert-controller/alert-controller.tsx +++ b/packages/core/src/components/alert-controller/alert-controller.tsx @@ -1,27 +1,28 @@ import { Component, Listen, Method } from '@stencil/core'; import { AlertEvent, AlertOptions, OverlayController } from '../../index'; - -let ids = 0; -const alerts = new Map(); +import { createOverlay, removeLastOverlay, dismissOverlay, getTopOverlay } from '../../utils/overlays'; @Component({ tag: 'ion-alert-controller' }) export class AlertController implements OverlayController { + private alerts = new Map(); + @Listen('body:ionAlertWillPresent') protected alertWillPresent(ev: AlertEvent) { - alerts.set(ev.target.alertId, ev.target); + this.alerts.set(ev.target.overlayId, ev.target); } - @Listen('body:ionAlertWillDismiss, body:ionAlertDidUnload') + @Listen('body:ionAlertWillDismiss') + @Listen('body:ionAlertDidUnload') protected alertWillDismiss(ev: AlertEvent) { - alerts.delete(ev.target.alertId); + this.alerts.delete(ev.target.overlayId); } @Listen('body:keyup.escape') protected escapeKeyUp() { - removeLastAlert(); + removeLastOverlay(this.alerts); } /* @@ -29,21 +30,7 @@ export class AlertController implements OverlayController { */ @Method() create(opts?: AlertOptions): Promise { - // create ionic's wrapping ion-alert component - const alertElement = document.createElement('ion-alert'); - - // give this alert a unique id - alertElement.alertId = ids++; - - // convert the passed in alert options into props - // that get passed down into the new alert - Object.assign(alertElement, opts); - - // append the alert element to the document body - const appRoot = document.querySelector('ion-app') || document.body; - appRoot.appendChild(alertElement); - - return alertElement.componentOnReady(); + return createOverlay('ion-alert', opts); } /* @@ -51,12 +38,7 @@ export class AlertController implements OverlayController { */ @Method() dismiss(data?: any, role?: any, alertId = -1) { - alertId = alertId >= 0 ? alertId : getHighestId(); - const alert = alerts.get(alertId); - if (!alert) { - return Promise.reject('alert does not exist'); - } - return alert.dismiss(data, role); + return dismissOverlay(data, role, this.alerts, alertId); } /* @@ -64,21 +46,6 @@ export class AlertController implements OverlayController { */ @Method() getTop() { - return alerts.get(getHighestId()); + return getTopOverlay(this.alerts); } } - -function getHighestId() { - let minimum = -1; - alerts.forEach((_alert: HTMLIonAlertElement, id: number) => { - if (id > minimum) { - minimum = id; - } - }); - return minimum; -} - -function removeLastAlert() { - const toRemove = alerts.get(getHighestId()); - return toRemove ? toRemove.dismiss() : Promise.resolve(); -} diff --git a/packages/core/src/components/alert/alert.ios.scss b/packages/core/src/components/alert/alert.ios.scss index 55eec58de7..600f2c0545 100644 --- a/packages/core/src/components/alert/alert.ios.scss +++ b/packages/core/src/components/alert/alert.ios.scss @@ -108,6 +108,7 @@ -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; } .alert-ios .alert-tappable { diff --git a/packages/core/src/components/alert/alert.tsx b/packages/core/src/components/alert/alert.tsx index 131a93f0cc..5e19d085f9 100644 --- a/packages/core/src/components/alert/alert.tsx +++ b/packages/core/src/components/alert/alert.tsx @@ -1,9 +1,8 @@ import { Component, CssClassMap, Element, Event, EventEmitter, Listen, Method, Prop } from '@stencil/core'; import { Animation, AnimationBuilder, AnimationController, Config, DomController, OverlayDismissEvent, OverlayDismissEventDetail } from '../../index'; import { domControllerAsync, playAnimationAsync } from '../../utils/helpers'; - -import { BACKDROP } from '../../utils/overlay-constants'; import { createThemedClasses, getClassMap } from '../../utils/theme'; +import { OverlayInterface, BACKDROP } from '../../utils/overlays'; import iosEnterAnimation from './animations/ios.enter'; import iosLeaveAnimation from './animations/ios.leave'; @@ -21,8 +20,7 @@ import mdLeaveAnimation from './animations/md.leave'; theme: 'alert' } }) -export class Alert { - alertId: number; +export class Alert implements OverlayInterface { mode: string; color: string; @@ -36,6 +34,7 @@ export class Alert { @Prop({ connect: 'ion-animation-controller' }) animationCtrl: AnimationController; @Prop({ context: 'config' }) config: Config; @Prop({ context: 'dom' }) dom: DomController; + @Prop() overlayId: number; /** * Animation to use when the alert is presented. @@ -147,7 +146,7 @@ export class Alert { present() { this.ionAlertWillPresent.emit(); - this.el.style.zIndex = `${20000 + this.alertId}`; + this.el.style.zIndex = `${20000 + this.overlayId}`; // get the user's animation fn if one was provided const animationBuilder = this.enterAnimation || this.config.get('alertEnter', this.mode === 'ios' ? iosEnterAnimation : mdEnterAnimation); @@ -347,14 +346,14 @@ export class Alert { ...themedClasses, ...getClassMap(this.cssClass) }, - id: this.alertId + id: this.overlayId }; } render() { - const hdrId = `${this.alertId}-hdr`; - const subHdrId = `${this.alertId}-sub-hdr`; - const msgId = `${this.alertId}-msg`; + const hdrId = `alert-${this.overlayId}-hdr`; + const subHdrId = `alert-${this.overlayId}-sub-hdr`; + const msgId = `alert-${this.overlayId}-msg`; if (this.title || !this.subTitle) { this.hdrId = hdrId; @@ -385,7 +384,7 @@ export class Alert { label: i.label, checked: !!i.checked, disabled: !!i.disabled, - id: i.id ? i.id : `alert-input-${this.alertId}-${index}`, + id: i.id ? i.id : `alert-input-${this.overlayId}-${index}`, handler: i.handler ? i.handler : null, min: i.min ? i.min : null, max: i.max ? i.max : null diff --git a/packages/core/src/components/alert/readme.md b/packages/core/src/components/alert/readme.md index 23e3aafab3..acc1573138 100644 --- a/packages/core/src/components/alert/readme.md +++ b/packages/core/src/components/alert/readme.md @@ -90,6 +90,11 @@ string The main message to be displayed in the alert. +#### overlayId + +number + + #### subTitle string @@ -170,6 +175,11 @@ string The main message to be displayed in the alert. +#### overlay-id + +number + + #### sub-title string diff --git a/packages/core/src/components/backdrop/backdrop.tsx b/packages/core/src/components/backdrop/backdrop.tsx index f94cf54670..3554382bfe 100644 --- a/packages/core/src/components/backdrop/backdrop.tsx +++ b/packages/core/src/components/backdrop/backdrop.tsx @@ -54,6 +54,7 @@ export class Backdrop { hostData() { return { + tabindex: '-1', class: { 'backdrop-hide': !this.visible, 'backdrop-no-tappable': !this.tappable, diff --git a/packages/core/src/components/loading-controller/loading-controller.tsx b/packages/core/src/components/loading-controller/loading-controller.tsx index a650a9da3a..fa9500c0fd 100644 --- a/packages/core/src/components/loading-controller/loading-controller.tsx +++ b/packages/core/src/components/loading-controller/loading-controller.tsx @@ -1,27 +1,28 @@ import { Component, Listen, Method } from '@stencil/core'; import { LoadingEvent, LoadingOptions, OverlayController } from '../../index'; +import { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from '../../utils/overlays'; -let ids = 0; -const loadings = new Map(); @Component({ tag: 'ion-loading-controller' }) export class LoadingController implements OverlayController { + private loadings = new Map(); + @Listen('body:ionLoadingWillPresent') protected loadingWillPresent(ev: LoadingEvent) { - loadings.set(ev.target.loadingId, ev.target); + this.loadings.set(ev.target.overlayId, ev.target); } @Listen('body:ionLoadingWillDismiss, body:ionLoadingDidUnload') protected loadingWillDismiss(ev: LoadingEvent) { - loadings.delete(ev.target.loadingId); + this.loadings.delete(ev.target.overlayId); } @Listen('body:keyup.escape') protected escapeKeyUp() { - removeLastLoading(); + removeLastOverlay(this.loadings); } /* @@ -29,31 +30,15 @@ export class LoadingController implements OverlayController { */ @Method() create(opts?: LoadingOptions): Promise { - // create ionic's wrapping ion-loading component - const loadingElement = document.createElement('ion-loading'); - - // give this loading a unique id - loadingElement.loadingId = ids++; - - // convert the passed in loading options into props - // that get passed down into the new loading - Object.assign(loadingElement, opts); - - // append the loading element to the document body - const appRoot = document.querySelector('ion-app') || document.body; - appRoot.appendChild(loadingElement); - - return loadingElement.componentOnReady(); + return createOverlay('ion-loading', opts); } /* * Dismiss the open loading overlay. */ @Method() - dismiss(data?: any, role?: any, loadingId = -1) { - loadingId = loadingId >= 0 ? loadingId : getHighestId(); - const loading = loadings.get(loadingId); - return loading.dismiss(data, role); + dismiss(data?: any, role?: string, loadingId = -1) { + return dismissOverlay(data, role, this.loadings, loadingId); } /* @@ -61,21 +46,6 @@ export class LoadingController implements OverlayController { */ @Method() getTop() { - return loadings.get(getHighestId()); + return getTopOverlay(this.loadings); } } - -function getHighestId() { - let minimum = -1; - loadings.forEach((_loading: HTMLIonLoadingElement, id: number) => { - if (id > minimum) { - minimum = id; - } - }); - return minimum; -} - -function removeLastLoading() { - const toRemove = loadings.get(getHighestId()); - return toRemove ? toRemove.dismiss() : Promise.resolve(); -} diff --git a/packages/core/src/components/loading/loading.tsx b/packages/core/src/components/loading/loading.tsx index 919806acad..497c83f0e1 100644 --- a/packages/core/src/components/loading/loading.tsx +++ b/packages/core/src/components/loading/loading.tsx @@ -7,6 +7,7 @@ import iosEnterAnimation from './animations/ios.enter'; import iosLeaveAnimation from './animations/ios.leave'; import mdEnterAnimation from './animations/md.enter'; import mdLeaveAnimation from './animations/md.leave'; +import { OverlayInterface, BACKDROP } from '../../utils/overlays'; @Component({ tag: 'ion-loading', @@ -18,10 +19,10 @@ import mdLeaveAnimation from './animations/md.leave'; theme: 'loading' } }) -export class Loading { + +export class Loading implements OverlayInterface { color: string; mode: string; - loadingId: number; private animation: Animation; private durationTimeout: any; @@ -31,6 +32,7 @@ export class Loading { @Prop({ connect: 'ion-animation-controller' }) animationCtrl: AnimationController; @Prop({ context: 'config' }) config: Config; @Prop({ context: 'dom' }) dom: DomController; + @Prop() overlayId: number; /** * Animation to use when the loading indicator is presented. @@ -153,7 +155,7 @@ export class Loading { @Listen('ionBackdropTap') protected onBackdropTap() { - this.dismiss(); + this.dismiss(null, BACKDROP); } /** @@ -163,7 +165,7 @@ export class Loading { present() { this.ionLoadingWillPresent.emit(); - this.el.style.zIndex = `${20000 + this.loadingId}`; + this.el.style.zIndex = `${20000 + this.overlayId}`; // get the user's animation fn if one was provided const animationBuilder = this.enterAnimation || this.config.get('loadingEnter', this.mode === 'ios' ? iosEnterAnimation : mdEnterAnimation); diff --git a/packages/core/src/components/loading/readme.md b/packages/core/src/components/loading/readme.md index 6eb789c271..78fff64dd7 100644 --- a/packages/core/src/components/loading/readme.md +++ b/packages/core/src/components/loading/readme.md @@ -83,6 +83,11 @@ Animation to use when the loading indicator is presented. Animation to use when the loading indicator is dismissed. +#### overlayId + +number + + #### showBackdrop boolean @@ -164,6 +169,11 @@ Animation to use when the loading indicator is presented. Animation to use when the loading indicator is dismissed. +#### overlay-id + +number + + #### show-backdrop boolean diff --git a/packages/core/src/components/modal-controller/modal-controller.tsx b/packages/core/src/components/modal-controller/modal-controller.tsx index 7e91c141bd..a1e2851732 100644 --- a/packages/core/src/components/modal-controller/modal-controller.tsx +++ b/packages/core/src/components/modal-controller/modal-controller.tsx @@ -1,27 +1,28 @@ import { Component, Listen, Method } from '@stencil/core'; import { ModalEvent, ModalOptions, OverlayController } from '../../index'; +import { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from '../../utils/overlays'; -let ids = 0; -const modals = new Map(); @Component({ tag: 'ion-modal-controller' }) export class ModalController implements OverlayController { + private modals = new Map(); + @Listen('body:ionModalWillPresent') protected modalWillPresent(ev: ModalEvent) { - modals.set(ev.target.modalId, ev.target); + this.modals.set(ev.target.overlayId, ev.target); } @Listen('body:ionModalWillDismiss, body:ionModalDidUnload') protected modalWillDismiss(ev: ModalEvent) { - modals.delete(ev.target.modalId); + this.modals.delete(ev.target.overlayId); } @Listen('body:keyup.escape') protected escapeKeyUp() { - removeLastModal(); + removeLastOverlay(this.modals); } /* @@ -29,21 +30,7 @@ export class ModalController implements OverlayController { */ @Method() create(opts?: ModalOptions): Promise { - // create ionic's wrapping ion-modal component - const modalElement = document.createElement('ion-modal'); - - // give this modal a unique id - modalElement.modalId = ids++; - - // convert the passed in modal options into props - // that get passed down into the new modal - Object.assign(modalElement, opts); - - // append the modal element to the document body - const appRoot = document.querySelector('ion-app') || document.body; - appRoot.appendChild(modalElement); - - return modalElement.componentOnReady(); + return createOverlay('ion-modal', opts); } /* @@ -51,12 +38,7 @@ export class ModalController implements OverlayController { */ @Method() dismiss(data?: any, role?: any, modalId = -1) { - modalId = modalId >= 0 ? modalId : getHighestId(); - const modal = modals.get(modalId); - if (!modal) { - return Promise.reject('modal does not exist'); - } - return modal.dismiss(data, role); + return dismissOverlay(data, role, this.modals, modalId); } /* @@ -64,21 +46,6 @@ export class ModalController implements OverlayController { */ @Method() getTop() { - return modals.get(getHighestId()); + return getTopOverlay(this.modals); } } - -function getHighestId() { - let minimum = -1; - modals.forEach((_modal: HTMLIonModalElement, id: number) => { - if (id > minimum) { - minimum = id; - } - }); - return minimum; -} - -function removeLastModal() { - const toRemove = modals.get(getHighestId()); - return toRemove ? toRemove.dismiss() : Promise.resolve(); -} diff --git a/packages/core/src/components/modal/modal.tsx b/packages/core/src/components/modal/modal.tsx index 2d5950fd07..b5d4c26957 100644 --- a/packages/core/src/components/modal/modal.tsx +++ b/packages/core/src/components/modal/modal.tsx @@ -4,6 +4,7 @@ import { Animation, AnimationBuilder, AnimationController, Config, DomController import { DomFrameworkDelegate } from '../../utils/dom-framework-delegate'; import { domControllerAsync, playAnimationAsync } from '../../utils/helpers'; import { createThemedClasses } from '../../utils/theme'; +import { OverlayInterface, BACKDROP } from '../../utils/overlays'; import iosEnterAnimation from './animations/ios.enter'; import iosLeaveAnimation from './animations/ios.leave'; @@ -21,8 +22,7 @@ import mdLeaveAnimation from './animations/md.leave'; theme: 'modal' } }) -export class Modal { - modalId: number; +export class Modal implements OverlayInterface { private animation: Animation; private usersComponentElement: HTMLElement; @@ -32,7 +32,7 @@ export class Modal { @Prop({ connect: 'ion-animation-controller' }) animationCtrl: AnimationController; @Prop({ context: 'config' }) config: Config; @Prop({ context: 'dom' }) dom: DomController; - + @Prop() overlayId: number; @Prop({ mutable: true }) delegate: FrameworkDelegate; /** @@ -138,10 +138,7 @@ export class Modal { @Listen('ionBackdropTap') protected onBackdropTap() { - // const opts: NavOptions = { - // minClickBlockDuration: 400 - // }; - this.dismiss(); + this.dismiss(null, BACKDROP); } /** @@ -156,7 +153,7 @@ export class Modal { this.ionModalWillPresent.emit(); - this.el.style.zIndex = `${20000 + this.modalId}`; + this.el.style.zIndex = `${20000 + this.overlayId}`; // get the user's animation fn if one was provided const animationBuilder = this.enterAnimation || this.config.get('modalEnter', this.mode === 'ios' ? iosEnterAnimation : mdEnterAnimation); diff --git a/packages/core/src/components/modal/readme.md b/packages/core/src/components/modal/readme.md index 36a2f5c77e..05952598be 100644 --- a/packages/core/src/components/modal/readme.md +++ b/packages/core/src/components/modal/readme.md @@ -114,6 +114,11 @@ Possible values are: `"ios"` or `"md"`. For more information, see [Platform Styles](/docs/theming/platform-specific-styles). +#### overlayId + +number + + #### showBackdrop boolean @@ -196,6 +201,11 @@ Possible values are: `"ios"` or `"md"`. For more information, see [Platform Styles](/docs/theming/platform-specific-styles). +#### overlay-id + +number + + #### show-backdrop boolean diff --git a/packages/core/src/components/picker-controller/picker-controller.tsx b/packages/core/src/components/picker-controller/picker-controller.tsx index 103e174e81..2d00216c49 100644 --- a/packages/core/src/components/picker-controller/picker-controller.tsx +++ b/packages/core/src/components/picker-controller/picker-controller.tsx @@ -1,27 +1,28 @@ import { Component, Listen, Method } from '@stencil/core'; import { OverlayController, PickerEvent, PickerOptions } from '../../index'; +import { createOverlay, getTopOverlay, removeLastOverlay, dismissOverlay } from '../../utils/overlays'; -let ids = 0; -const pickers = new Map(); @Component({ tag: 'ion-picker-controller' }) export class PickerController implements OverlayController { + private pickers = new Map(); + @Listen('body:ionPickerWillPresent') protected pickerWillPresent(ev: PickerEvent) { - pickers.set(ev.target.pickerId, ev.target); + this.pickers.set(ev.target.overlayId, ev.target); } @Listen('body:ionPickerWillDismiss, body:ionPickerDidUnload') protected pickerWillDismiss(ev: PickerEvent) { - pickers.delete(ev.target.pickerId); + this.pickers.delete(ev.target.overlayId); } @Listen('body:keyup.escape') protected escapeKeyUp() { - removeLastPicker(); + removeLastOverlay(this.pickers); } /* @@ -29,21 +30,7 @@ export class PickerController implements OverlayController { */ @Method() create(opts?: PickerOptions): Promise { - // create ionic's wrapping ion-picker component - const pickerElement = document.createElement('ion-picker'); - - // give this picker a unique id - pickerElement.pickerId = ids++; - - // convert the passed in picker options into props - // that get passed down into the new picker - Object.assign(pickerElement, opts); - - // append the picker element to the document body - const appRoot = document.querySelector('ion-app') || document.body; - appRoot.appendChild(pickerElement); - - return pickerElement.componentOnReady(); + return createOverlay('ion-picker', opts); } /* @@ -51,12 +38,7 @@ export class PickerController implements OverlayController { */ @Method() dismiss(data?: any, role?: any, pickerId = -1) { - pickerId = pickerId >= 0 ? pickerId : getHighestId(); - const picker = pickers.get(pickerId); - if (!picker) { - return Promise.reject('picker does not exist'); - } - return picker.dismiss(data, role); + return dismissOverlay(data, role, this.pickers, pickerId); } /* @@ -64,21 +46,6 @@ export class PickerController implements OverlayController { */ @Method() getTop() { - return pickers.get(getHighestId()); + return getTopOverlay(this.pickers); } } - -function getHighestId() { - let minimum = -1; - pickers.forEach((_picker: HTMLIonPickerElement, id: number) => { - if (id > minimum) { - minimum = id; - } - }); - return minimum; -} - -function removeLastPicker() { - const toRemove = pickers.get(getHighestId()); - return toRemove ? toRemove.dismiss() : Promise.resolve(); -} diff --git a/packages/core/src/components/picker/picker.tsx b/packages/core/src/components/picker/picker.tsx index fcf711c4fb..5ee15699c5 100644 --- a/packages/core/src/components/picker/picker.tsx +++ b/packages/core/src/components/picker/picker.tsx @@ -1,10 +1,12 @@ import { Component, CssClassMap, Element, Event, EventEmitter, Listen, Method, Prop, State } from '@stencil/core'; import { Animation, AnimationBuilder, AnimationController, Config, DomController, OverlayDismissEvent, OverlayDismissEventDetail } from '../../index'; + import { domControllerAsync, playAnimationAsync } from '../../utils/helpers'; +import { getClassMap } from '../../utils/theme'; +import { OverlayInterface } from '../../utils/overlays'; import iosEnterAnimation from './animations/ios.enter'; import iosLeaveAnimation from './animations/ios.leave'; -import { getClassMap } from '../../utils/theme'; @Component({ tag: 'ion-picker', @@ -16,13 +18,12 @@ import { getClassMap } from '../../utils/theme'; theme: 'picker' } }) -export class Picker { +export class Picker implements OverlayInterface { + private animation: Animation; private durationTimeout: any; private mode: string; - pickerId: number; - @Element() private el: HTMLElement; @State() private showSpinner: boolean = null; @@ -31,6 +32,7 @@ export class Picker { @Prop({ connect: 'ion-animation-controller' }) animationCtrl: AnimationController; @Prop({ context: 'config' }) config: Config; @Prop({ context: 'dom' }) dom: DomController; + @Prop() overlayId: number; /** * Animation to use when the picker is presented. @@ -120,7 +122,7 @@ export class Picker { this.ionPickerWillPresent.emit(); - this.el.style.zIndex = `${20000 + this.pickerId}`; + this.el.style.zIndex = `${20000 + this.overlayId}`; // get the user's animation fn if one was provided const animationBuilder = this.enterAnimation || this.config.get('pickerEnter', iosEnterAnimation); diff --git a/packages/core/src/components/picker/readme.md b/packages/core/src/components/picker/readme.md index ae9c2354a0..ab1eccbb40 100644 --- a/packages/core/src/components/picker/readme.md +++ b/packages/core/src/components/picker/readme.md @@ -59,6 +59,11 @@ Animation to use when the picker is presented. Animation to use when the picker is dismissed. +#### overlayId + +number + + #### showBackdrop boolean @@ -125,6 +130,11 @@ Animation to use when the picker is presented. Animation to use when the picker is dismissed. +#### overlay-id + +number + + #### show-backdrop boolean diff --git a/packages/core/src/components/popover-controller/popover-controller.tsx b/packages/core/src/components/popover-controller/popover-controller.tsx index 584342965f..6dce697070 100644 --- a/packages/core/src/components/popover-controller/popover-controller.tsx +++ b/packages/core/src/components/popover-controller/popover-controller.tsx @@ -1,27 +1,27 @@ import { Component, Listen, Method } from '@stencil/core'; import { OverlayController, PopoverEvent, PopoverOptions } from '../../index'; - -let ids = 0; -const popovers = new Map(); +import { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from '../../utils/overlays'; @Component({ tag: 'ion-popover-controller' }) export class PopoverController implements OverlayController { + private popovers = new Map(); + @Listen('body:ionPopoverWillPresent') protected popoverWillPresent(ev: PopoverEvent) { - popovers.set(ev.target.popoverId, ev.target); + this.popovers.set(ev.target.overlayId, ev.target); } @Listen('body:ionPopoverWillDismiss, body:ionPopoverDidUnload') protected popoverWillDismiss(ev: PopoverEvent) { - popovers.delete(ev.target.popoverId); + this.popovers.delete(ev.target.overlayId); } @Listen('body:keyup.escape') protected escapeKeyUp() { - removeLastPopover(); + removeLastOverlay(this.popovers); } /* @@ -29,21 +29,7 @@ export class PopoverController implements OverlayController { */ @Method() create(opts?: PopoverOptions): Promise { - // create ionic's wrapping ion-popover component - const popoverElement = document.createElement('ion-popover'); - - // give this popover a unique id - popoverElement.popoverId = ids++; - - // convert the passed in popover options into props - // that get passed down into the new popover - Object.assign(popoverElement, opts); - - // append the popover element to the document body - const appRoot = document.querySelector('ion-app') || document.body; - appRoot.appendChild(popoverElement); - - return popoverElement.componentOnReady(); + return createOverlay('ion-popover', opts); } /* @@ -51,12 +37,7 @@ export class PopoverController implements OverlayController { */ @Method() dismiss(data?: any, role?: any, popoverId = -1) { - popoverId = popoverId >= 0 ? popoverId : getHighestId(); - const popover = popovers.get(popoverId); - if (!popover) { - return Promise.reject('popover does not exist'); - } - return popover.dismiss(data, role); + return dismissOverlay(data, role, this.popovers, popoverId); } /* @@ -64,21 +45,6 @@ export class PopoverController implements OverlayController { */ @Method() getTop() { - return popovers.get(getHighestId()); + return getTopOverlay(this.popovers); } } - -function getHighestId() { - let minimum = -1; - popovers.forEach((_popover: HTMLIonPopoverElement, id: number) => { - if (id > minimum) { - minimum = id; - } - }); - return minimum; -} - -function removeLastPopover() { - const toRemove = popovers.get(getHighestId()); - return toRemove ? toRemove.dismiss() : Promise.resolve(); -} diff --git a/packages/core/src/components/popover/popover.tsx b/packages/core/src/components/popover/popover.tsx index c91ca4ba3f..1f8542ea25 100644 --- a/packages/core/src/components/popover/popover.tsx +++ b/packages/core/src/components/popover/popover.tsx @@ -4,6 +4,7 @@ import { Animation, AnimationBuilder, AnimationController, Config, DomController import { DomFrameworkDelegate } from '../../utils/dom-framework-delegate'; import { domControllerAsync, playAnimationAsync } from '../../utils/helpers'; import { createThemedClasses } from '../../utils/theme'; +import { OverlayInterface, BACKDROP } from '../../utils/overlays'; import iosEnterAnimation from './animations/ios.enter'; import iosLeaveAnimation from './animations/ios.leave'; @@ -20,8 +21,7 @@ import mdLeaveAnimation from './animations/md.leave'; theme: 'popover' } }) -export class Popover { - popoverId: number; +export class Popover implements OverlayInterface { private animation: Animation; private usersComponentElement: HTMLElement; @@ -32,6 +32,7 @@ export class Popover { @Prop({ context: 'config' }) config: Config; @Prop({ context: 'dom' }) dom: DomController; @Prop({ mutable: true }) delegate: FrameworkDelegate; + @Prop() overlayId: number; /** * The color to use from your Sass `$colors` map. @@ -150,7 +151,7 @@ export class Popover { @Listen('ionBackdropTap') protected onBackdropTap() { - this.dismiss(); + this.dismiss(null, BACKDROP); } /** @@ -164,7 +165,7 @@ export class Popover { } this.ionPopoverWillPresent.emit(); - this.el.style.zIndex = `${10000 + this.popoverId}`; + this.el.style.zIndex = `${10000 + this.overlayId}`; // get the user's animation fn if one was provided const animationBuilder = this.enterAnimation || this.config.get('popoverEnter', this.mode === 'ios' ? iosEnterAnimation : mdEnterAnimation); diff --git a/packages/core/src/components/popover/readme.md b/packages/core/src/components/popover/readme.md index 12e73c7a76..00ba8f84c8 100644 --- a/packages/core/src/components/popover/readme.md +++ b/packages/core/src/components/popover/readme.md @@ -101,6 +101,11 @@ Possible values are: `"ios"` or `"md"`. For more information, see [Platform Styles](/docs/theming/platform-specific-styles). +#### overlayId + +number + + #### showBackdrop boolean @@ -197,6 +202,11 @@ Possible values are: `"ios"` or `"md"`. For more information, see [Platform Styles](/docs/theming/platform-specific-styles). +#### overlay-id + +number + + #### show-backdrop boolean diff --git a/packages/core/src/components/toast-controller/toast-controller.tsx b/packages/core/src/components/toast-controller/toast-controller.tsx index cd2bdc676e..954ad6d1e7 100644 --- a/packages/core/src/components/toast-controller/toast-controller.tsx +++ b/packages/core/src/components/toast-controller/toast-controller.tsx @@ -1,27 +1,28 @@ import { Component, Listen, Method } from '@stencil/core'; import { OverlayController, ToastEvent, ToastOptions } from '../../index'; +import { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from '../../utils/overlays'; -let ids = 0; -const toasts = new Map(); @Component({ tag: 'ion-toast-controller' }) export class ToastController implements OverlayController { + private toasts = new Map(); + @Listen('body:ionToastWillPresent') protected toastWillPresent(ev: ToastEvent) { - toasts.set(ev.target.toastId, ev.target); + this.toasts.set(ev.target.overlayId, ev.target); } @Listen('body:ionToastWillDismiss, body:ionToastDidUnload') protected toastWillDismiss(ev: ToastEvent) { - toasts.delete(ev.target.toastId); + this.toasts.delete(ev.target.overlayId); } @Listen('body:keyup.escape') protected escapeKeyUp() { - removeLastToast(); + removeLastOverlay(this.toasts); } /* @@ -29,21 +30,7 @@ export class ToastController implements OverlayController { */ @Method() create(opts?: ToastOptions): Promise { - // create ionic's wrapping ion-toast component - const toastElement = document.createElement('ion-toast'); - - // give this toast a unique id - toastElement.toastId = ids++; - - // convert the passed in toast options into props - // that get passed down into the new toast - Object.assign(toastElement, opts); - - // append the toast element to the document body - const appRoot = document.querySelector('ion-app') || document.body; - appRoot.appendChild(toastElement); - - return toastElement.componentOnReady(); + return createOverlay('ion-toast', opts); } /* @@ -51,12 +38,7 @@ export class ToastController implements OverlayController { */ @Method() dismiss(data?: any, role?: any, toastId = -1) { - toastId = toastId >= 0 ? toastId : getHighestId(); - const toast = toasts.get(toastId); - if (!toast) { - return Promise.reject('toast does not exist'); - } - return toast.dismiss(data, role); + return dismissOverlay(data, role, this.toasts, toastId); } /* @@ -64,21 +46,6 @@ export class ToastController implements OverlayController { */ @Method() getTop() { - return toasts.get(getHighestId()); + return getTopOverlay(this.toasts); } } - -function getHighestId() { - let minimum = -1; - toasts.forEach((_toast: HTMLIonToastElement, id: number) => { - if (id > minimum) { - minimum = id; - } - }); - return minimum; -} - -function removeLastToast() { - const toRemove = toasts.get(getHighestId()); - return toRemove ? toRemove.dismiss() : Promise.resolve(); -} diff --git a/packages/core/src/components/toast/readme.md b/packages/core/src/components/toast/readme.md index e7dc59f7aa..6f29507144 100644 --- a/packages/core/src/components/toast/readme.md +++ b/packages/core/src/components/toast/readme.md @@ -94,6 +94,11 @@ string Message to be shown in the toast. +#### overlayId + +number + + #### position string @@ -175,6 +180,11 @@ string Message to be shown in the toast. +#### overlay-id + +number + + #### position string diff --git a/packages/core/src/components/toast/toast.tsx b/packages/core/src/components/toast/toast.tsx index a54f7047de..81243fb8cc 100644 --- a/packages/core/src/components/toast/toast.tsx +++ b/packages/core/src/components/toast/toast.tsx @@ -1,8 +1,9 @@ import { Component, Element, Event, EventEmitter, Listen, Method, Prop } from '@stencil/core'; import { Animation, AnimationBuilder, AnimationController, Config, CssClassMap, DomController, OverlayDismissEvent, OverlayDismissEventDetail } from '../../index'; -import { domControllerAsync, playAnimationAsync } from '../../utils/helpers'; +import { domControllerAsync, playAnimationAsync } from '../../utils/helpers'; import { createThemedClasses, getClassMap } from '../../utils/theme'; +import { OverlayInterface } from '../../utils/overlays'; import iosEnterAnimation from './animations/ios.enter'; import iosLeaveAnimation from './animations/ios.leave'; @@ -20,10 +21,9 @@ import mdLeaveAnimation from './animations/md.leave'; theme: 'toast' } }) -export class Toast { - private animation: Animation | null; +export class Toast implements OverlayInterface { - toastId: number; + private animation: Animation | null; @Element() private el: HTMLElement; @@ -33,6 +33,7 @@ export class Toast { @Prop({ connect: 'ion-animation-controller' }) animationCtrl: AnimationController; @Prop({ context: 'config' }) config: Config; @Prop({ context: 'dom' }) dom: DomController; + @Prop() overlayId: number; /** * Animation to use when the toast is presented. diff --git a/packages/core/src/utils/helpers.ts b/packages/core/src/utils/helpers.ts index 4a5c03c9d7..34fc2a1557 100644 --- a/packages/core/src/utils/helpers.ts +++ b/packages/core/src/utils/helpers.ts @@ -50,6 +50,16 @@ export function assert(bool: boolean, msg: string) { } } +export function autoFocus(containerEl: HTMLElement): HTMLElement { + const focusableEls = containerEl.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]'); + if (focusableEls.length > 0) { + const el = focusableEls[0] as HTMLInputElement; + el.focus(); + return el; + } + return null; +} + export function toDashCase(str: string) { return str.replace(/([A-Z])/g, (g) => '-' + g[0].toLowerCase()); } diff --git a/packages/core/src/utils/overlay-constants.ts b/packages/core/src/utils/overlay-constants.ts deleted file mode 100644 index 80fd8dae02..0000000000 --- a/packages/core/src/utils/overlay-constants.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export const BACKDROP = 'backdrop'; diff --git a/packages/core/src/utils/overlays.ts b/packages/core/src/utils/overlays.ts new file mode 100644 index 0000000000..570507da02 --- /dev/null +++ b/packages/core/src/utils/overlays.ts @@ -0,0 +1,62 @@ + +export const BACKDROP = 'backdrop'; + +export interface OverlayInterface { + overlayId: number; + + present(): Promise; + dismiss(data?: any, role?: string): Promise; +} + +export interface HTMLIonOverlayElement extends HTMLStencilElement, OverlayInterface {} + +export type OverlayMap = Map; + +let lastId = 1; + +export function createOverlay +(tagName: string, opts: any): Promise { + // create ionic's wrapping ion-alert component + const element = document.createElement(tagName) as T; + + // give this alert a unique id + element.overlayId = lastId++; + + // convert the passed in alert options into props + // that get passed down into the new alert + Object.assign(element, opts); + + // append the alert element to the document body + const appRoot = document.querySelector('ion-app') || document.body; + appRoot.appendChild(element); + + return element.componentOnReady(); +} + +export function dismissOverlay(data: any, role: any, overlays: OverlayMap, id: number): Promise { + id = id >= 0 ? id : getHighestId(overlays); + const overlay = overlays.get(id); + if (!overlay) { + return Promise.reject('overlay does not exist'); + } + return overlay.dismiss(data, role); +} + +export function getTopOverlay(overlays: Map) { + return overlays.get(getHighestId(overlays)); +} + +export function getHighestId(overlays: Map) { + let minimum = -1; + overlays.forEach((_, id: number) => { + if (id > minimum) { + minimum = id; + } + }); + return minimum; +} + +export function removeLastOverlay(overlays: OverlayMap) { + const toRemove = getTopOverlay(overlays); + return toRemove ? toRemove.dismiss() : Promise.resolve(); +}