Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions packages/react-native/Libraries/Utilities/BackHandler.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,26 @@
*/

import NativeDeviceEventManager from '../../Libraries/NativeModules/specs/NativeDeviceEventManager';
import {setEventInitTimeStamp} from '../../src/private/webapis/dom/events/internals/EventInternals';
import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter';
import {HardwareBackPressEvent} from './HardwareBackPressEvent';

const DEVICE_BACK_EVENT = 'hardwareBackPress';

type BackPressEventName = 'backPress' | 'hardwareBackPress';
type BackPressHandler = () => ?boolean;
type BackPressHandler = (event: HardwareBackPressEvent) => ?boolean;

const _backPressSubscriptions: Array<BackPressHandler> = [];

RCTDeviceEventEmitter.addListener(DEVICE_BACK_EVENT, function () {
RCTDeviceEventEmitter.addListener(DEVICE_BACK_EVENT, function (nativeEvent) {
const options = {};
const nativeTimestamp = nativeEvent?.timeStamp;
if (nativeTimestamp != null) {
setEventInitTimeStamp(options, nativeTimestamp);
}
const event = new HardwareBackPressEvent(options);
for (let i = _backPressSubscriptions.length - 1; i >= 0; i--) {
if (_backPressSubscriptions[i]?.()) {
if (_backPressSubscriptions[i]?.(event)) {
return;
}
}
Expand Down
12 changes: 11 additions & 1 deletion packages/react-native/Libraries/Utilities/BackHandler.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ import {NativeEventSubscription} from '../EventEmitter/RCTNativeAppEventEmitter'

export type BackPressEventName = 'hardwareBackPress';

/**
* Event dispatched when the hardware back button is pressed.
* The `timeStamp` property reflects the native timestamp captured
* when the back press was emitted.
*/
export interface HardwareBackPressEvent {
readonly type: string;
readonly timeStamp: number;
}

/**
* Detect hardware back button presses, and programmatically invoke the
* default back button functionality to exit the app if there are no
Expand All @@ -26,7 +36,7 @@ export interface BackHandlerStatic {
exitApp(): void;
addEventListener(
eventName: BackPressEventName,
handler: () => boolean | null | undefined,
handler: (event: HardwareBackPressEvent) => boolean | null | undefined,
): NativeEventSubscription;
}

Expand Down
4 changes: 3 additions & 1 deletion packages/react-native/Libraries/Utilities/BackHandler.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
* @format
*/

import type {HardwareBackPressEvent} from './HardwareBackPressEvent';

type BackPressEventName = 'backPress' | 'hardwareBackPress';
type BackPressHandler = () => ?boolean;
type BackPressHandler = (event: HardwareBackPressEvent) => ?boolean;

function emptyFunction(): void {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@

export type BackPressEventName = 'backPress' | 'hardwareBackPress';

export interface HardwareBackPressEvent {
+type: string;
+timeStamp: number;
}

type TBackHandler = {
exitApp(): void,
addEventListener(
eventName: BackPressEventName,
handler: () => ?boolean,
handler: (event: HardwareBackPressEvent) => ?boolean,
): {remove: () => void, ...},
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

import type {EventInit} from '../../src/private/webapis/dom/events/Event';

import Event from '../../src/private/webapis/dom/events/Event';

/**
* Event dispatched when the hardware back button is pressed on Android.
*/
export class HardwareBackPressEvent extends Event {
constructor(options?: ?EventInit) {
super('hardwareBackPress', options);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,20 @@
*/

import type {BackPressEventName} from '../BackHandler';
import type {HardwareBackPressEvent} from '../HardwareBackPressEvent';

const _backPressSubscriptions = new Set<() => ?boolean>();
import {HardwareBackPressEvent as HardwareBackPressEventClass} from '../HardwareBackPressEvent';

const _backPressSubscriptions = new Set<
(event: HardwareBackPressEvent) => ?boolean,
>();

const BackHandler = {
exitApp: jest.fn() as () => void,

addEventListener: function (
eventName: BackPressEventName,
handler: () => ?boolean,
handler: (event: HardwareBackPressEvent) => ?boolean,
): {remove: () => void, ...} {
_backPressSubscriptions.add(handler);
return {
Expand All @@ -28,10 +33,11 @@ const BackHandler = {
},

mockPressBack: function () {
const event = new HardwareBackPressEventClass();
let invokeDefault = true;
const subscriptions = [..._backPressSubscriptions].reverse();
for (let i = 0; i < subscriptions.length; ++i) {
if (subscriptions[i]()) {
if (subscriptions[i](event)) {
invokeDefault = false;
break;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';

import type {HardwareBackPressEvent} from 'react-native/Libraries/Utilities/BackHandler';

import RCTDeviceEventEmitter from 'react-native/Libraries/EventEmitter/RCTDeviceEventEmitter';
import BackHandler from 'react-native/Libraries/Utilities/BackHandler';
import {HardwareBackPressEvent as HardwareBackPressEventClass} from 'react-native/Libraries/Utilities/HardwareBackPressEvent';

describe('BackHandler', () => {
const subscriptions: Array<{remove: () => void, ...}> = [];

afterEach(() => {
for (const sub of subscriptions) {
sub.remove();
}
subscriptions.length = 0;
});

it('calls handlers in reverse order (LIFO)', () => {
const callOrder: Array<string> = [];
const handler1 = (_event: HardwareBackPressEvent) => {
callOrder.push('first');
return false;
};
const handler2 = (_event: HardwareBackPressEvent) => {
callOrder.push('second');
return true;
};

subscriptions.push(
BackHandler.addEventListener('hardwareBackPress', handler1),
);
subscriptions.push(
BackHandler.addEventListener('hardwareBackPress', handler2),
);

RCTDeviceEventEmitter.emit('hardwareBackPress', {timeStamp: 100});

expect(callOrder).toEqual(['second']);
});

it('calls all handlers when none return true', () => {
const callOrder: Array<string> = [];
const handler1 = (_event: HardwareBackPressEvent) => {
callOrder.push('first');
return false;
};
const handler2 = (_event: HardwareBackPressEvent) => {
callOrder.push('second');
return false;
};

subscriptions.push(
BackHandler.addEventListener('hardwareBackPress', handler1),
);
subscriptions.push(
BackHandler.addEventListener('hardwareBackPress', handler2),
);

RCTDeviceEventEmitter.emit('hardwareBackPress', {timeStamp: 100});

expect(callOrder).toEqual(['second', 'first']);
});

it('passes HardwareBackPressEvent to handlers', () => {
let receivedEvent: ?HardwareBackPressEvent = null;
const handler = (event: HardwareBackPressEvent) => {
receivedEvent = event;
return true;
};

subscriptions.push(
BackHandler.addEventListener('hardwareBackPress', handler),
);

RCTDeviceEventEmitter.emit('hardwareBackPress', {timeStamp: 42});

expect(receivedEvent).toBeInstanceOf(HardwareBackPressEventClass);
});

it('event has native timestamp as timeStamp', () => {
let receivedEvent: ?HardwareBackPressEvent = null;
const handler = (event: HardwareBackPressEvent) => {
receivedEvent = event;
return true;
};

subscriptions.push(
BackHandler.addEventListener('hardwareBackPress', handler),
);

RCTDeviceEventEmitter.emit('hardwareBackPress', {timeStamp: 42});

expect(receivedEvent?.timeStamp).toBe(42);
});

it('event falls back to performance.now() when no native timestamp', () => {
let receivedEvent: ?HardwareBackPressEvent = null;
const handler = (event: HardwareBackPressEvent) => {
receivedEvent = event;
return true;
};

subscriptions.push(
BackHandler.addEventListener('hardwareBackPress', handler),
);

const before = performance.now();
RCTDeviceEventEmitter.emit('hardwareBackPress', null);
const after = performance.now();

const timeStamp = receivedEvent?.timeStamp;
expect(timeStamp).not.toBeNull();
if (timeStamp != null) {
expect(timeStamp).toBeGreaterThanOrEqual(before);
expect(timeStamp).toBeLessThanOrEqual(after);
}
});

it('removes handler on subscription.remove()', () => {
let called = false;
const handler = (_event: HardwareBackPressEvent) => {
called = true;
return true;
};

const sub = BackHandler.addEventListener('hardwareBackPress', handler);
sub.remove();

RCTDeviceEventEmitter.emit('hardwareBackPress', {timeStamp: 100});

expect(called).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';

import {HardwareBackPressEvent} from 'react-native/Libraries/Utilities/HardwareBackPressEvent';
import Event from 'react-native/src/private/webapis/dom/events/Event';
import {setEventInitTimeStamp} from 'react-native/src/private/webapis/dom/events/internals/EventInternals';

describe('HardwareBackPressEvent', () => {
it('extends Event', () => {
const event = new HardwareBackPressEvent();

expect(event).toBeInstanceOf(Event);
expect(event.type).toBe('hardwareBackPress');
expect(event.bubbles).toBe(false);
expect(event.cancelable).toBe(false);
expect(event.composed).toBe(false);
});

it('uses native timestamp as timeStamp when provided via setEventInitTimeStamp', () => {
const options = {};
setEventInitTimeStamp(options, 12345);
const event = new HardwareBackPressEvent(options);

expect(event.timeStamp).toBe(12345);
});

it('falls back to performance.now() when no timestamp is provided', () => {
const before = performance.now();
const event = new HardwareBackPressEvent();
const after = performance.now();

expect(event.timeStamp).toBeGreaterThanOrEqual(before);
expect(event.timeStamp).toBeLessThanOrEqual(after);
});

it('falls back to performance.now() when options is undefined', () => {
const before = performance.now();
const event = new HardwareBackPressEvent(undefined);
const after = performance.now();

expect(event.timeStamp).toBeGreaterThanOrEqual(before);
expect(event.timeStamp).toBeLessThanOrEqual(after);
});

it('does NOT allow changing the timeStamp value after construction', () => {
const options = {};
setEventInitTimeStamp(options, 12345);
const event = new HardwareBackPressEvent(options);

expect(() => {
'use strict';
// $FlowExpectedError[cannot-write]
event.timeStamp = 999;
}).toThrow();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public open class DeviceEventManagerModule(
public open fun emitHardwareBackPressed() {
val reactApplicationContext: ReactApplicationContext? =
getReactApplicationContextIfActiveOrWarn()
reactApplicationContext?.emitDeviceEvent("hardwareBackPress", null)
val map = buildReadableMap { put("timeStamp", System.nanoTime() / 1_000_000.0) }
reactApplicationContext?.emitDeviceEvent("hardwareBackPress", map)
}

/** Sends an event to the JS instance that a new intent was received. */
Expand Down
Loading
Loading