Skip to content

Commit

Permalink
feat(): add ability to continue processing hardware back button events (
Browse files Browse the repository at this point in the history
#20613)

fixes #17824
  • Loading branch information
liamdebeasi committed Apr 27, 2020
1 parent 429edb0 commit 3821c04
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 35 deletions.
5 changes: 4 additions & 1 deletion angular/src/providers/nav-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ export class NavController {
}

// Subscribe to backButton events
platform.backButton.subscribeWithPriority(0, () => this.pop());
platform.backButton.subscribeWithPriority(0, processNextHandler => {
this.pop();
processNextHandler();
});
}

/**
Expand Down
8 changes: 4 additions & 4 deletions angular/src/providers/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { BackButtonEventDetail, Platforms, getPlatforms, isPlatform } from '@ion
import { Subject, Subscription } from 'rxjs';

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({
Expand Down Expand Up @@ -46,9 +46,9 @@ export class Platform {
zone.run(() => {
this.win = doc.defaultView;
this.backButton.subscribeWithPriority = function(priority, callback) {
return this.subscribe(ev => (
ev.register(priority, () => zone.run(callback))
));
return this.subscribe(ev => {
return ev.register(priority, processNextHandler => zone.run(() => callback(processNextHandler)));
});
};

proxyEvent(this.pause, doc, 'pause');
Expand Down
5 changes: 4 additions & 1 deletion core/src/components/router/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ export class Router implements ComponentInterface {

@Listen('ionBackButton', { target: 'document' })
protected onBackButton(ev: BackButtonEvent) {
ev.detail.register(0, () => this.back());
ev.detail.register(0, processNextHandler => {
this.back();
processNextHandler();
});
}

/**
Expand Down
2 changes: 1 addition & 1 deletion core/src/interface.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export interface FrameworkDelegate {
}

export interface BackButtonEventDetail {
register(priority: number, handler: () => Promise<any> | void): void;
register(priority: number, handler: (processNextHandler: () => void) => Promise<any> | void): void;
}

export interface StyleEventDetail {
Expand Down
61 changes: 35 additions & 26 deletions core/src/utils/hardware-back-button.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { BackButtonEvent } from '../interface';

type Handler = () => Promise<any> | void | null;
type Handler = (processNextHandler: () => void) => Promise<any> | void | null;

interface HandlerRegister {
priority: number;
handler: Handler;
id: number;
}

export const startHardwareBackButton = () => {
Expand All @@ -16,44 +17,52 @@ export const startHardwareBackButton = () => {
return;
}

const handlers: HandlerRegister[] = [];
let index = 0;
let handlers: HandlerRegister[] = [];
const ev: BackButtonEvent = new CustomEvent('ionBackButton', {
bubbles: false,
detail: {
register(priority: number, handler: Handler) {
handlers.push({ priority, handler });
handlers.push({ priority, handler, id: index++ });
}
}
});
doc.dispatchEvent(ev);

if (handlers.length > 0) {
let selectedPriority = Number.MIN_SAFE_INTEGER;
let selectedHandler: Handler | undefined;
handlers.forEach(({ priority, handler }) => {
if (priority >= selectedPriority) {
selectedPriority = priority;
selectedHandler = handler;
const executeAction = async (handlerRegister: HandlerRegister | undefined) => {
try {
if (handlerRegister && handlerRegister.handler) {
const result = handlerRegister.handler(processHandlers);
if (result != null) {
await result;
}
}
});
} catch (e) {
console.error(e);
}
};

busy = true;
executeAction(selectedHandler).then(() => busy = false);
}
});
};
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;
}
});

const executeAction = async (handler: Handler | undefined) => {
try {
if (handler) {
const result = handler();
if (result != null) {
await result;
busy = true;
handlers = handlers.filter(handler => handler.id !== selectedHandler.id);
executeAction(selectedHandler).then(() => busy = false);
}
}
} catch (e) {
console.error(e);
}
};

processHandlers();
});
};

export const OVERLAY_BACK_BUTTON_PRIORITY = 100;
Expand Down
59 changes: 59 additions & 0 deletions core/src/utils/test/hardware-back-button.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
}
3 changes: 2 additions & 1 deletion packages/react-router/src/ReactRouter/NavManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ export class NavManager extends React.Component<NavManagerProps, NavContextState

if (document) {
document.addEventListener('ionBackButton', (e: any) => {
e.detail.register(0, () => {
e.detail.register(0, (processNextHandler: () => void) => {
this.props.history.goBack();
processNextHandler();
});
});
}
Expand Down
5 changes: 4 additions & 1 deletion vue/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ export default class Router extends VueRouter {

// Listen to Ionic's back button event
document.addEventListener('ionBackButton', (e: Event) => {
(e as BackButtonEvent).detail.register(0, () => this.back());
(e as BackButtonEvent).detail.register(0, processNextHandler => {
this.back();
processNextHandler();
});
});
}

Expand Down

0 comments on commit 3821c04

Please sign in to comment.