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 { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from '../../utils/overlays';
import { createOverlay, dismissOverlay, getOverlay } from '../../utils/overlays';
@Component({
tag: 'ion-action-sheet-controller'
})
export class ActionSheetController implements OverlayController {
private actionSheets = new Map<number, HTMLIonActionSheetElement>();
@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.
*/
@ -41,7 +23,7 @@ export class ActionSheetController implements OverlayController {
*/
@Method()
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()
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 { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from '../../utils/overlays';
import { createOverlay, dismissOverlay, getOverlay } from '../../utils/overlays';
@Component({
tag: 'ion-alert-controller'
})
export class AlertController implements OverlayController {
private alerts = new Map<number, HTMLIonAlertElement>();
@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
*/
@ -41,7 +23,7 @@ export class AlertController implements OverlayController {
*/
@Method()
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()
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 { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from '../../utils/overlays';
import { createOverlay, dismissOverlay, getOverlay } from '../../utils/overlays';
@Component({
tag: 'ion-loading-controller'
})
export class LoadingController implements OverlayController {
private loadings = new Map<number, HTMLIonLoadingElement>();
@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.
*/
@ -41,7 +23,7 @@ export class LoadingController implements OverlayController {
*/
@Method()
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()
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 { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from '../../utils/overlays';
import { createOverlay, dismissOverlay, getOverlay } from '../../utils/overlays';
@Component({
tag: 'ion-modal-controller'
})
export class ModalController implements OverlayController {
private modals = new Map<number, HTMLIonModalElement>();
@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.
*/
@ -41,7 +23,7 @@ export class ModalController implements OverlayController {
*/
@Method()
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()
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 { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from '../../utils/overlays';
import { createOverlay, dismissOverlay, getOverlay } from '../../utils/overlays';
/** @hidden */
@Component({
@ -9,26 +9,8 @@ import { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from
})
export class PickerController implements OverlayController {
private pickers = new Map<number, HTMLIonPickerElement>();
@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.
*/
@ -42,7 +24,7 @@ export class PickerController implements OverlayController {
*/
@Method()
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()
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 { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from '../../utils/overlays';
import { createOverlay, dismissOverlay, getOverlay } from '../../utils/overlays';
@Component({
tag: 'ion-popover-controller'
})
export class PopoverController implements OverlayController {
private popovers = new Map<number, HTMLIonPopoverElement>();
@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.
*/
@ -41,7 +23,7 @@ export class PopoverController implements OverlayController {
*/
@Method()
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()
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 { createOverlay, dismissOverlay, getTopOverlay, removeLastOverlay } from '../../utils/overlays';
import { createOverlay, dismissOverlay, getOverlay } from '../../utils/overlays';
@Component({
tag: 'ion-toast-controller'
})
export class ToastController implements OverlayController {
private toasts = new Map<number, HTMLIonToastElement>();
@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.
*/
@ -41,7 +23,7 @@ export class ToastController implements OverlayController {
*/
@Method()
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()
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 {
overlayId: number;
backdropDismiss?: boolean;
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> {
const doc = element.ownerDocument;
connectListeners(doc);
// convert the passed in overlay options into props
// that get passed down into the new overlay
Object.assign(element, opts);
@ -10,39 +13,59 @@ export function createOverlay<T extends HTMLIonOverlayElement & Required<B>, B>(
element.overlayId = lastId++;
// append the overlay element to the document body
const doc = element.ownerDocument;
const appRoot = doc.querySelector('ion-app') || doc.body;
appRoot.appendChild(element);
getAppRoot(doc).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();
}
export function dismissOverlay(data: any, role: string | undefined, overlays: OverlayMap, id: number): Promise<void> {
id = id >= 0 ? id : getHighestId(overlays);
const overlay = overlays.get(id);
export function connectListeners(doc: Document) {
if (lastId === 0) {
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) {
return Promise.reject('overlay does not exist');
}
return overlay.dismiss(data, role);
}
export function getTopOverlay<T extends HTMLIonOverlayElement>(overlays: OverlayMap): T {
return overlays.get(getHighestId(overlays)) as T;
export function getOverlays(doc: Document, overlayTag?: string): HTMLIonOverlayElement[] {
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) {
let minimum = -1;
overlays.forEach((_, id) => {
if (id > minimum) {
minimum = id;
}
});
return minimum;
}
export function removeLastOverlay(overlays: OverlayMap) {
const toRemove = getTopOverlay(overlays);
return toRemove ? toRemove.dismiss() : Promise.resolve();
export function getOverlay(doc: Document, overlayTag?: string, id?: number): HTMLIonOverlayElement | undefined {
const overlays = getOverlays(doc, overlayTag);
if (id != null) {
return overlays.find(o => o.overlayId === id);
}
return (id == null)
? overlays[overlays.length - 1]
: overlays.find(o => o.overlayId === id);
}
export async function present(
@ -94,6 +117,10 @@ export async function dismiss(
overlay.el.remove();
}
function getAppRoot(doc: Document) {
return doc.querySelector('ion-app') || doc.body;
}
async function overlayAnimation(
overlay: OverlayInterface,
animationBuilder: AnimationBuilder,