feat(): add ability to continue processing hardware back button events (#20613)

fixes #17824
This commit is contained in:
Liam DeBeasi
2020-04-27 12:58:37 -04:00
committed by GitHub
parent 429edb053b
commit 3821c0463a
8 changed files with 115 additions and 37 deletions

View File

@ -45,7 +45,10 @@ export class NavController {
} }
// Subscribe to backButton events // Subscribe to backButton events
platform.backButton.subscribeWithPriority(0, () => this.pop()); platform.backButton.subscribeWithPriority(0, processNextHandler => {
this.pop();
processNextHandler();
});
} }
/** /**

View File

@ -4,7 +4,7 @@ import { BackButtonEventDetail, Platforms, getPlatforms, isPlatform } from '@ion
import { Subject, Subscription } from 'rxjs'; import { Subject, Subscription } from 'rxjs';
export interface BackButtonEmitter extends Subject<BackButtonEventDetail> { export interface BackButtonEmitter extends Subject<BackButtonEventDetail> {
subscribeWithPriority(priority: number, callback: () => Promise<any> | void): Subscription; subscribeWithPriority(priority: number, callback: (processNextHandler: () => void) => Promise<any> | void): Subscription;
} }
@Injectable({ @Injectable({
@ -46,9 +46,9 @@ export class Platform {
zone.run(() => { zone.run(() => {
this.win = doc.defaultView; this.win = doc.defaultView;
this.backButton.subscribeWithPriority = function(priority, callback) { this.backButton.subscribeWithPriority = function(priority, callback) {
return this.subscribe(ev => ( return this.subscribe(ev => {
ev.register(priority, () => zone.run(callback)) return ev.register(priority, processNextHandler => zone.run(() => callback(processNextHandler)));
)); });
}; };
proxyEvent(this.pause, doc, 'pause'); proxyEvent(this.pause, doc, 'pause');

View File

@ -79,7 +79,10 @@ export class Router implements ComponentInterface {
@Listen('ionBackButton', { target: 'document' }) @Listen('ionBackButton', { target: 'document' })
protected onBackButton(ev: BackButtonEvent) { protected onBackButton(ev: BackButtonEvent) {
ev.detail.register(0, () => this.back()); ev.detail.register(0, processNextHandler => {
this.back();
processNextHandler();
});
} }
/** /**

View File

@ -53,7 +53,7 @@ export interface FrameworkDelegate {
} }
export interface BackButtonEventDetail { export interface BackButtonEventDetail {
register(priority: number, handler: () => Promise<any> | void): void; register(priority: number, handler: (processNextHandler: () => void) => Promise<any> | void): void;
} }
export interface StyleEventDetail { export interface StyleEventDetail {

View File

@ -1,10 +1,11 @@
import { BackButtonEvent } from '../interface'; import { BackButtonEvent } from '../interface';
type Handler = () => Promise<any> | void | null; type Handler = (processNextHandler: () => void) => Promise<any> | void | null;
interface HandlerRegister { interface HandlerRegister {
priority: number; priority: number;
handler: Handler; handler: Handler;
id: number;
} }
export const startHardwareBackButton = () => { export const startHardwareBackButton = () => {
@ -16,37 +17,22 @@ export const startHardwareBackButton = () => {
return; return;
} }
const handlers: HandlerRegister[] = []; let index = 0;
let handlers: HandlerRegister[] = [];
const ev: BackButtonEvent = new CustomEvent('ionBackButton', { const ev: BackButtonEvent = new CustomEvent('ionBackButton', {
bubbles: false, bubbles: false,
detail: { detail: {
register(priority: number, handler: Handler) { register(priority: number, handler: Handler) {
handlers.push({ priority, handler }); handlers.push({ priority, handler, id: index++ });
} }
} }
}); });
doc.dispatchEvent(ev); doc.dispatchEvent(ev);
if (handlers.length > 0) { const executeAction = async (handlerRegister: HandlerRegister | undefined) => {
let selectedPriority = Number.MIN_SAFE_INTEGER;
let selectedHandler: Handler | undefined;
handlers.forEach(({ priority, handler }) => {
if (priority >= selectedPriority) {
selectedPriority = priority;
selectedHandler = handler;
}
});
busy = true;
executeAction(selectedHandler).then(() => busy = false);
}
});
};
const executeAction = async (handler: Handler | undefined) => {
try { try {
if (handler) { if (handlerRegister && handlerRegister.handler) {
const result = handler(); const result = handlerRegister.handler(processHandlers);
if (result != null) { if (result != null) {
await result; await result;
} }
@ -54,6 +40,29 @@ const executeAction = async (handler: Handler | undefined) => {
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
};
const processHandlers = () => {
if (handlers.length > 0) {
let selectedHandler: HandlerRegister = {
priority: Number.MIN_SAFE_INTEGER,
handler: () => undefined,
id: -1
};
handlers.forEach(handler => {
if (handler.priority >= selectedHandler.priority) {
selectedHandler = handler;
}
});
busy = true;
handlers = handlers.filter(handler => handler.id !== selectedHandler.id);
executeAction(selectedHandler).then(() => busy = false);
}
};
processHandlers();
});
}; };
export const OVERLAY_BACK_BUTTON_PRIORITY = 100; export const OVERLAY_BACK_BUTTON_PRIORITY = 100;

View File

@ -0,0 +1,59 @@
import { startHardwareBackButton } from '../hardware-back-button';
describe('Hardware Back Button', () => {
beforeEach(() => startHardwareBackButton());
it('should call handler', () => {
const cbSpy = jest.fn();
document.addEventListener('ionBackButton', (ev) => {
ev.detail.register(0, cbSpy);
});
dispatchBackButtonEvent();
expect(cbSpy).toHaveBeenCalled();
});
it('should call handlers in order of priority', () => {
const cbSpy = jest.fn();
const cbSpyTwo = jest.fn();
document.addEventListener('ionBackButton', (ev) => {
ev.detail.register(100, cbSpy);
ev.detail.register(99, cbSpyTwo);
});
dispatchBackButtonEvent();
expect(cbSpy).toHaveBeenCalled();
expect(cbSpyTwo).not.toHaveBeenCalled();
});
it('should only call last handler to be added for handlers with same priority', () => {
const cbSpy = jest.fn();
const cbSpyTwo = jest.fn();
document.addEventListener('ionBackButton', (ev) => {
ev.detail.register(100, cbSpy);
ev.detail.register(100, cbSpyTwo);
});
dispatchBackButtonEvent();
expect(cbSpy).not.toHaveBeenCalled();
expect(cbSpyTwo).toHaveBeenCalled();
});
it('should call multiple callbacks', () => {
const cbSpy = (processNextHandler) => {
processNextHandler();
}
const cbSpyTwo = jest.fn();
document.addEventListener('ionBackButton', (ev) => {
ev.detail.register(100, cbSpy);
ev.detail.register(99, cbSpyTwo);
});
dispatchBackButtonEvent();
expect(cbSpyTwo).toHaveBeenCalled();
});
});
const dispatchBackButtonEvent = () => {
const ev = new Event('backbutton');
document.dispatchEvent(ev);
}

View File

@ -36,8 +36,9 @@ export class NavManager extends React.Component<NavManagerProps, NavContextState
if (document) { if (document) {
document.addEventListener('ionBackButton', (e: any) => { document.addEventListener('ionBackButton', (e: any) => {
e.detail.register(0, () => { e.detail.register(0, (processNextHandler: () => void) => {
this.props.history.goBack(); this.props.history.goBack();
processNextHandler();
}); });
}); });
} }

View File

@ -37,7 +37,10 @@ export default class Router extends VueRouter {
// Listen to Ionic's back button event // Listen to Ionic's back button event
document.addEventListener('ionBackButton', (e: Event) => { document.addEventListener('ionBackButton', (e: Event) => {
(e as BackButtonEvent).detail.register(0, () => this.back()); (e as BackButtonEvent).detail.register(0, processNextHandler => {
this.back();
processNextHandler();
});
}); });
} }