fix(overlays): esc button works closed top overlays

fixes #14662
This commit is contained in:
Manu Mtz.-Almeida
2018-08-21 17:58:49 +02:00
parent d83e7f8840
commit c567a82bfc
9 changed files with 80 additions and 179 deletions

View File

@ -1,33 +1,15 @@
import { Component, Listen, Method, Prop } from '@stencil/core'; import { Component, Method, Prop } from '@stencil/core';
import { ActionSheetOptions, OverlayController } from '../../interface'; import { ActionSheetOptions, OverlayController } from '../../interface';
import { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from '../../utils/overlays'; import { createOverlay, dismissOverlay, getOverlay } from '../../utils/overlays';
@Component({ @Component({
tag: 'ion-action-sheet-controller' tag: 'ion-action-sheet-controller'
}) })
export class ActionSheetController implements OverlayController { export class ActionSheetController implements OverlayController {
private actionSheets = new Map<number, HTMLIonActionSheetElement>();
@Prop({ context: 'document' }) doc!: Document; @Prop({ context: 'document' }) doc!: Document;
@Listen('body:ionActionSheetWillPresent')
protected actionSheetWillPresent(ev: any) {
this.actionSheets.set(ev.target.overlayId, ev.target);
}
@Listen('body:ionActionSheetWillDismiss')
@Listen('body:ionActionSheetDidUnload')
protected actionSheetWillDismiss(ev: any) {
this.actionSheets.delete(ev.target.overlayId);
}
@Listen('body:keyup.escape')
protected escapeKeyUp() {
removeLastOverlay(this.actionSheets);
}
/** /**
* Create an action sheet overlay with action sheet options. * Create an action sheet overlay with action sheet options.
*/ */
@ -41,7 +23,7 @@ export class ActionSheetController implements OverlayController {
*/ */
@Method() @Method()
dismiss(data?: any, role?: string, actionSheetId = -1) { dismiss(data?: any, role?: string, actionSheetId = -1) {
return dismissOverlay(data, role, this.actionSheets, actionSheetId); return dismissOverlay(this.doc, data, role, 'ion-action-sheet', actionSheetId);
} }
/** /**
@ -49,6 +31,6 @@ export class ActionSheetController implements OverlayController {
*/ */
@Method() @Method()
getTop(): HTMLIonActionSheetElement { getTop(): HTMLIonActionSheetElement {
return getTopOverlay(this.actionSheets); return getOverlay(this.doc, 'ion-action-sheet') as HTMLIonActionSheetElement;
} }
} }

View File

@ -1,33 +1,15 @@
import { Component, Listen, Method, Prop } from '@stencil/core'; import { Component, Method, Prop } from '@stencil/core';
import { AlertOptions, OverlayController } from '../../interface'; import { AlertOptions, OverlayController } from '../../interface';
import { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from '../../utils/overlays'; import { createOverlay, dismissOverlay, getOverlay } from '../../utils/overlays';
@Component({ @Component({
tag: 'ion-alert-controller' tag: 'ion-alert-controller'
}) })
export class AlertController implements OverlayController { export class AlertController implements OverlayController {
private alerts = new Map<number, HTMLIonAlertElement>();
@Prop({ context: 'document' }) doc!: Document; @Prop({ context: 'document' }) doc!: Document;
@Listen('body:ionAlertWillPresent')
protected alertWillPresent(ev: any) {
this.alerts.set(ev.target.overlayId, ev.target);
}
@Listen('body:ionAlertWillDismiss')
@Listen('body:ionAlertDidUnload')
protected alertWillDismiss(ev: any) {
this.alerts.delete(ev.target.overlayId);
}
@Listen('body:keyup.escape')
protected escapeKeyUp() {
removeLastOverlay(this.alerts);
}
/** /**
* Create an alert overlay with alert options * Create an alert overlay with alert options
*/ */
@ -41,7 +23,7 @@ export class AlertController implements OverlayController {
*/ */
@Method() @Method()
dismiss(data?: any, role?: string, alertId = -1) { dismiss(data?: any, role?: string, alertId = -1) {
return dismissOverlay(data, role, this.alerts, alertId); return dismissOverlay(this.doc, data, role, 'ion-alert', alertId);
} }
/** /**
@ -49,6 +31,6 @@ export class AlertController implements OverlayController {
*/ */
@Method() @Method()
getTop(): HTMLIonAlertElement { getTop(): HTMLIonAlertElement {
return getTopOverlay(this.alerts); return getOverlay(this.doc, 'ion-alert') as HTMLIonAlertElement;
} }
} }

View File

@ -1,33 +1,15 @@
import { Component, Listen, Method, Prop } from '@stencil/core'; import { Component, Method, Prop } from '@stencil/core';
import { LoadingOptions, OverlayController } from '../../interface'; import { LoadingOptions, OverlayController } from '../../interface';
import { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from '../../utils/overlays'; import { createOverlay, dismissOverlay, getOverlay } from '../../utils/overlays';
@Component({ @Component({
tag: 'ion-loading-controller' tag: 'ion-loading-controller'
}) })
export class LoadingController implements OverlayController { export class LoadingController implements OverlayController {
private loadings = new Map<number, HTMLIonLoadingElement>();
@Prop({ context: 'document' }) doc!: Document; @Prop({ context: 'document' }) doc!: Document;
@Listen('body:ionLoadingWillPresent')
protected loadingWillPresent(ev: any) {
this.loadings.set(ev.target.overlayId, ev.target);
}
@Listen('body:ionLoadingWillDismiss')
@Listen('body:ionLoadingDidUnload')
protected loadingWillDismiss(ev: any) {
this.loadings.delete(ev.target.overlayId);
}
@Listen('body:keyup.escape')
protected escapeKeyUp() {
removeLastOverlay(this.loadings);
}
/** /**
* Create a loading overlay with loading options. * Create a loading overlay with loading options.
*/ */
@ -41,7 +23,7 @@ export class LoadingController implements OverlayController {
*/ */
@Method() @Method()
dismiss(data?: any, role?: string, loadingId = -1) { dismiss(data?: any, role?: string, loadingId = -1) {
return dismissOverlay(data, role, this.loadings, loadingId); return dismissOverlay(this.doc, data, role, 'ion-loading', loadingId);
} }
/** /**
@ -49,6 +31,6 @@ export class LoadingController implements OverlayController {
*/ */
@Method() @Method()
getTop(): HTMLIonLoadingElement { getTop(): HTMLIonLoadingElement {
return getTopOverlay(this.loadings); return getOverlay(this.doc, 'ion-loading') as HTMLIonLoadingElement;
} }
} }

View File

@ -1,33 +1,15 @@
import { Component, Listen, Method, Prop } from '@stencil/core'; import { Component, Method, Prop } from '@stencil/core';
import { ModalOptions, OverlayController } from '../../interface'; import { ModalOptions, OverlayController } from '../../interface';
import { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from '../../utils/overlays'; import { createOverlay, dismissOverlay, getOverlay } from '../../utils/overlays';
@Component({ @Component({
tag: 'ion-modal-controller' tag: 'ion-modal-controller'
}) })
export class ModalController implements OverlayController { export class ModalController implements OverlayController {
private modals = new Map<number, HTMLIonModalElement>();
@Prop({ context: 'document' }) doc!: Document; @Prop({ context: 'document' }) doc!: Document;
@Listen('body:ionModalWillPresent')
protected modalWillPresent(ev: any) {
this.modals.set(ev.target.overlayId, ev.target);
}
@Listen('body:ionModalWillDismiss')
@Listen('body:ionModalDidUnload')
protected modalWillDismiss(ev: any) {
this.modals.delete(ev.target.overlayId);
}
@Listen('body:keyup.escape')
protected escapeKeyUp() {
removeLastOverlay(this.modals);
}
/** /**
* Create a modal overlay with modal options. * Create a modal overlay with modal options.
*/ */
@ -41,7 +23,7 @@ export class ModalController implements OverlayController {
*/ */
@Method() @Method()
dismiss(data?: any, role?: string, modalId = -1) { dismiss(data?: any, role?: string, modalId = -1) {
return dismissOverlay(data, role, this.modals, modalId); return dismissOverlay(this.doc, data, role, 'ion-modal', modalId);
} }
/** /**
@ -49,6 +31,6 @@ export class ModalController implements OverlayController {
*/ */
@Method() @Method()
getTop(): HTMLIonModalElement { getTop(): HTMLIonModalElement {
return getTopOverlay(this.modals); return getOverlay(this.doc, 'ion-modal') as HTMLIonModalElement;
} }
} }

View File

@ -1,7 +1,7 @@
import { Component, Listen, Method, Prop } from '@stencil/core'; import { Component, Method, Prop } from '@stencil/core';
import { OverlayController, PickerOptions } from '../../interface'; import { OverlayController, PickerOptions } from '../../interface';
import { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from '../../utils/overlays'; import { createOverlay, dismissOverlay, getOverlay } from '../../utils/overlays';
/** @hidden */ /** @hidden */
@Component({ @Component({
@ -9,26 +9,8 @@ import { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from
}) })
export class PickerController implements OverlayController { export class PickerController implements OverlayController {
private pickers = new Map<number, HTMLIonPickerElement>();
@Prop({ context: 'document' }) doc!: Document; @Prop({ context: 'document' }) doc!: Document;
@Listen('body:ionPickerWillPresent')
protected pickerWillPresent(ev: any) {
this.pickers.set(ev.target.overlayId, ev.target);
}
@Listen('body:ionPickerWillDismiss')
@Listen('body:ionPickerDidUnload')
protected pickerWillDismiss(ev: any) {
this.pickers.delete(ev.target.overlayId);
}
@Listen('body:keyup.escape')
protected escapeKeyUp() {
removeLastOverlay(this.pickers);
}
/* /*
* Create a picker overlay with picker options. * Create a picker overlay with picker options.
*/ */
@ -42,7 +24,7 @@ export class PickerController implements OverlayController {
*/ */
@Method() @Method()
dismiss(data?: any, role?: string, pickerId = -1) { dismiss(data?: any, role?: string, pickerId = -1) {
return dismissOverlay(data, role, this.pickers, pickerId); return dismissOverlay(this.doc, data, role, 'ion-picker', pickerId);
} }
/* /*
@ -50,6 +32,6 @@ export class PickerController implements OverlayController {
*/ */
@Method() @Method()
getTop(): HTMLIonPickerElement { getTop(): HTMLIonPickerElement {
return getTopOverlay(this.pickers); return getOverlay(this.doc, 'ion-picker') as HTMLIonPickerElement;
} }
} }

View File

@ -1,33 +1,15 @@
import { Component, Listen, Method, Prop } from '@stencil/core'; import { Component, Method, Prop } from '@stencil/core';
import { OverlayController, PopoverOptions } from '../../interface'; import { OverlayController, PopoverOptions } from '../../interface';
import { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from '../../utils/overlays'; import { createOverlay, dismissOverlay, getOverlay } from '../../utils/overlays';
@Component({ @Component({
tag: 'ion-popover-controller' tag: 'ion-popover-controller'
}) })
export class PopoverController implements OverlayController { export class PopoverController implements OverlayController {
private popovers = new Map<number, HTMLIonPopoverElement>();
@Prop({ context: 'document' }) doc!: Document; @Prop({ context: 'document' }) doc!: Document;
@Listen('body:ionPopoverWillPresent')
protected popoverWillPresent(ev: any) {
this.popovers.set(ev.target.overlayId, ev.target);
}
@Listen('body:ionPopoverWillDismiss')
@Listen('body:ionPopoverDidUnload')
protected popoverWillDismiss(ev: any) {
this.popovers.delete(ev.target.overlayId);
}
@Listen('body:keyup.escape')
protected escapeKeyUp() {
removeLastOverlay(this.popovers);
}
/** /**
* Create a popover overlay with popover options. * Create a popover overlay with popover options.
*/ */
@ -41,7 +23,7 @@ export class PopoverController implements OverlayController {
*/ */
@Method() @Method()
dismiss(data?: any, role?: string, popoverId = -1) { dismiss(data?: any, role?: string, popoverId = -1) {
return dismissOverlay(data, role, this.popovers, popoverId); return dismissOverlay(this.doc, data, role, 'ion-popover', popoverId);
} }
/** /**
@ -49,6 +31,6 @@ export class PopoverController implements OverlayController {
*/ */
@Method() @Method()
getTop(): HTMLIonPopoverElement { getTop(): HTMLIonPopoverElement {
return getTopOverlay(this.popovers); return getOverlay(this.doc, 'ion-popover') as HTMLIonPopoverElement;
} }
} }

View File

@ -1,33 +1,15 @@
import { Component, Listen, Method, Prop } from '@stencil/core'; import { Component, Method, Prop } from '@stencil/core';
import { OverlayController, ToastOptions } from '../../interface'; import { OverlayController, ToastOptions } from '../../interface';
import { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from '../../utils/overlays'; import { createOverlay, dismissOverlay, getOverlay } from '../../utils/overlays';
@Component({ @Component({
tag: 'ion-toast-controller' tag: 'ion-toast-controller'
}) })
export class ToastController implements OverlayController { export class ToastController implements OverlayController {
private toasts = new Map<number, HTMLIonToastElement>();
@Prop({ context: 'document' }) doc!: Document; @Prop({ context: 'document' }) doc!: Document;
@Listen('body:ionToastWillPresent')
protected toastWillPresent(ev: any) {
this.toasts.set(ev.target.overlayId, ev.target);
}
@Listen('body:ionToastWillDismiss')
@Listen('body:ionToastDidUnload')
protected toastWillDismiss(ev: any) {
this.toasts.delete(ev.target.overlayId);
}
@Listen('body:keyup.escape')
protected escapeKeyUp() {
removeLastOverlay(this.toasts);
}
/** /**
* Create a toast overlay with toast options. * Create a toast overlay with toast options.
*/ */
@ -41,7 +23,7 @@ export class ToastController implements OverlayController {
*/ */
@Method() @Method()
dismiss(data?: any, role?: string, toastId = -1) { dismiss(data?: any, role?: string, toastId = -1) {
return dismissOverlay(data, role, this.toasts, toastId); return dismissOverlay(this.doc, data, role, 'ion-toast', toastId);
} }
/** /**
@ -49,6 +31,6 @@ export class ToastController implements OverlayController {
*/ */
@Method() @Method()
getTop(): HTMLIonToastElement { getTop(): HTMLIonToastElement {
return getTopOverlay(this.toasts); return getOverlay(this.doc, 'ion-toast') as HTMLIonToastElement;
} }
} }

View File

@ -38,7 +38,7 @@ export interface OverlayController {
export interface HTMLIonOverlayElement extends HTMLStencilElement { export interface HTMLIonOverlayElement extends HTMLStencilElement {
overlayId: number; overlayId: number;
backdropDismiss?: boolean;
dismiss(data?: any, role?: string): Promise<void>; dismiss(data?: any, role?: string): Promise<void>;
} }
export type OverlayMap = Map<number, HTMLIonOverlayElement>;

View File

@ -1,8 +1,11 @@
import { AnimationBuilder, HTMLIonOverlayElement, IonicConfig, OverlayInterface, OverlayMap } from '../interface'; import { AnimationBuilder, HTMLIonOverlayElement, IonicConfig, OverlayInterface } from '../interface';
let lastId = 1; let lastId = 0;
export function createOverlay<T extends HTMLIonOverlayElement & Required<B>, B>(element: T, opts: B): Promise<T> { export function createOverlay<T extends HTMLIonOverlayElement & Required<B>, B>(element: T, opts: B): Promise<T> {
const doc = element.ownerDocument;
connectListeners(doc);
// convert the passed in overlay options into props // convert the passed in overlay options into props
// that get passed down into the new overlay // that get passed down into the new overlay
Object.assign(element, opts); Object.assign(element, opts);
@ -10,39 +13,59 @@ export function createOverlay<T extends HTMLIonOverlayElement & Required<B>, B>(
element.overlayId = lastId++; element.overlayId = lastId++;
// append the overlay element to the document body // append the overlay element to the document body
const doc = element.ownerDocument; getAppRoot(doc).appendChild(element);
const appRoot = doc.querySelector('ion-app') || doc.body;
appRoot.appendChild(element); doc.body.addEventListener('keyup', ev => {
if (ev.key === 'Escape') {
const lastOverlay = getOverlay(doc);
if (lastOverlay && lastOverlay.backdropDismiss) {
lastOverlay.dismiss(null, BACKDROP);
}
}
});
return element.componentOnReady(); return element.componentOnReady();
} }
export function dismissOverlay(data: any, role: string | undefined, overlays: OverlayMap, id: number): Promise<void> { export function connectListeners(doc: Document) {
id = id >= 0 ? id : getHighestId(overlays); if (lastId === 0) {
const overlay = overlays.get(id); lastId = 1;
doc.body.addEventListener('keyup', ev => {
if (ev.key === 'Escape') {
const lastOverlay = getOverlay(doc);
if (lastOverlay && lastOverlay.backdropDismiss === true) {
lastOverlay.dismiss('backdrop');
}
}
});
}
}
export function dismissOverlay(doc: Document, data: any, role: string | undefined, overlayTag: string, id: number): Promise<void> {
const overlay = getOverlay(doc, overlayTag, id);
if (!overlay) { if (!overlay) {
return Promise.reject('overlay does not exist'); return Promise.reject('overlay does not exist');
} }
return overlay.dismiss(data, role); return overlay.dismiss(data, role);
} }
export function getTopOverlay<T extends HTMLIonOverlayElement>(overlays: OverlayMap): T { export function getOverlays(doc: Document, overlayTag?: string): HTMLIonOverlayElement[] {
return overlays.get(getHighestId(overlays)) as T; const overlays = Array.from(getAppRoot(doc).children) as HTMLIonOverlayElement[];
if (overlayTag == null) {
return overlays;
}
overlayTag = overlayTag.toUpperCase();
return overlays.filter(c => c.tagName === overlayTag);
} }
export function getHighestId(overlays: OverlayMap) { export function getOverlay(doc: Document, overlayTag?: string, id?: number): HTMLIonOverlayElement | undefined {
let minimum = -1; const overlays = getOverlays(doc, overlayTag);
overlays.forEach((_, id) => { if (id != null) {
if (id > minimum) { return overlays.find(o => o.overlayId === id);
minimum = id; }
} return (id == null)
}); ? overlays[overlays.length - 1]
return minimum; : overlays.find(o => o.overlayId === id);
}
export function removeLastOverlay(overlays: OverlayMap) {
const toRemove = getTopOverlay(overlays);
return toRemove ? toRemove.dismiss() : Promise.resolve();
} }
export async function present( export async function present(
@ -94,6 +117,10 @@ export async function dismiss(
overlay.el.remove(); overlay.el.remove();
} }
function getAppRoot(doc: Document) {
return doc.querySelector('ion-app') || doc.body;
}
async function overlayAnimation( async function overlayAnimation(
overlay: OverlayInterface, overlay: OverlayInterface,
animationBuilder: AnimationBuilder, animationBuilder: AnimationBuilder,