Skip to content
Open
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
2 changes: 2 additions & 0 deletions packages/react-native/Libraries/AppState/AppState.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import {NativeEventSubscription} from '../EventEmitter/RCTNativeAppEventEmitter';
import {AddEventListenerOptions} from '../EventEmitter/EventTargetLike';

/**
* AppState can tell you if the app is in the foreground or background,
Expand Down Expand Up @@ -51,6 +52,7 @@ export interface AppStateStatic {
addEventListener(
type: AppStateEvent,
listener: (state: AppStateStatus) => void,
options?: AddEventListenerOptions,
): NativeEventSubscription;
}

Expand Down
16 changes: 15 additions & 1 deletion packages/react-native/Libraries/AppState/AppState.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* @format
*/

import {adaptToEventTarget} from '../EventEmitter/EventTargetLike';
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
import logError from '../Utilities/logError';
import Platform from '../Utilities/Platform';
Expand Down Expand Up @@ -104,13 +105,26 @@ class AppStateImpl {
}
}

addEventListener<K: AppStateEvent>(
type: K,
handler: (...AppStateEventDefinitions[K]) => void,
options?: ?{|once?: ?boolean, signal?: ?mixed|},
): EventSubscription {
return adaptToEventTarget(
(...args) => this._addEventListener(...args),
type,
handler,
options,
);
}

/**
* Add a handler to AppState changes by listening to the `change` event type
* and providing the handler.
*
* See https://reactnative.dev/docs/appstate#addeventlistener
*/
addEventListener<K: AppStateEvent>(
_addEventListener<K: AppStateEvent>(
type: K,
handler: (...AppStateEventDefinitions[K]) => void,
): EventSubscription {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import {HostInstance} from '../../../types/public/ReactNativeTypes';
import {EmitterSubscription} from '../../vendor/emitter/EventEmitter';
import {AddEventListenerOptions} from '../../EventEmitter/EventTargetLike';

type AccessibilityChangeEventName =
| 'change' // deprecated, maps to screenReaderChanged
Expand Down Expand Up @@ -129,10 +130,12 @@ export interface AccessibilityInfoStatic {
addEventListener(
eventName: AccessibilityChangeEventName,
handler: AccessibilityChangeEventHandler,
options?: AddEventListenerOptions,
): EmitterSubscription;
addEventListener(
eventName: AccessibilityAnnouncementEventName,
handler: AccessibilityAnnouncementFinishedEventHandler,
options?: AddEventListenerOptions,
): EmitterSubscription;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import type {HostInstance} from '../../../src/private/types/HostInstance';
import type {EventSubscription} from '../../vendor/emitter/EventEmitter';

import {adaptToEventTarget} from '../../EventEmitter/EventTargetLike';
import RCTDeviceEventEmitter from '../../EventEmitter/RCTDeviceEventEmitter';
import {sendAccessibilityEvent} from '../../ReactNative/RendererProxy';
import Platform from '../../Utilities/Platform';
Expand Down Expand Up @@ -428,12 +429,18 @@ const AccessibilityInfo = {
eventName: K,
// $FlowFixMe[incompatible-type] - Flow bug with unions and generics (T128099423)
handler: (...AccessibilityEventDefinitions[K]) => void,
options?: ?{|once?: ?boolean, signal?: ?mixed|},
): EventSubscription {
const deviceEventName = EventNames.get(eventName);
return deviceEventName == null
? {remove(): void {}}
: // $FlowFixMe[incompatible-type]
RCTDeviceEventEmitter.addListener(deviceEventName, handler);
adaptToEventTarget(
(...args) => RCTDeviceEventEmitter.addListener(...args),
eventName,
handler,
options,
);
},

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

export type AddEventListenerOptions = {
once?: boolean | undefined;
signal?: AbortSignal | undefined;
}
73 changes: 73 additions & 0 deletions packages/react-native/Libraries/EventEmitter/EventTargetLike.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* @flow strict-local
* @format
*/

import type {EventSubscription} from '../vendor/emitter/EventEmitter';

/**
* EventTarget adapter
*
* Options supported:
* - `once` (boolean) - If true, the listener would be automatically removed when invoked
* - `signal` (AbortSignal) - The listener will be removed when the abort() method of the AbortController which owns the AbortSignal is called
*
* see: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
*/
export const adaptToEventTarget = <
R: EventSubscription | {remove(): void, ...},
>(
// $FlowFixMe[unclear-type]
addEventListener: (...args: any[]) => R,
type: mixed,
listener: mixed,
options?: ?{|once?: ?boolean, signal?: ?mixed|},
): R => {
// Extract options to avoid mutation issues
// $FlowFixMe[incompatible-type]
const signal: ?AbortSignal = options?.signal;
const once = options?.once;

if (signal !== undefined && !(signal instanceof AbortSignal)) {
throw new TypeError(
"Failed to execute 'addEventListener': Failed to convert the 'signal' value to 'AbortSignal'.",
);
}

const subscription: R = addEventListener(type, (...args) => {
// $FlowFixMe[sketchy-null-bool]
if (once) {
subscription.remove();
signal?.removeEventListener('abort', onAbort);
}
// $FlowFixMe[not-a-function]
return listener(...args);
});

// If already aborted, remove subscription immediately
if (signal?.aborted) {
subscription.remove();
return subscription;
}

// Remove subscription if the abort signal is triggered
const onAbort = () => subscription.remove();
signal?.addEventListener('abort', onAbort, {once: true}); // Note: `once` option is supported by `event-target-shim` which is used by `abort-controller` polyfill

// $FlowFixMe[incompatible-type]
return Object.create(
// $FlowFixMe[not-an-object]
subscription,
{
remove: {
writable: true,
enumerable: true,
configurable: true,
value: () => {
subscription.remove();
signal?.removeEventListener('abort', onAbort);
},
},
},
);
};
2 changes: 2 additions & 0 deletions packages/react-native/Libraries/Linking/Linking.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import {NativeEventEmitter} from '../EventEmitter/NativeEventEmitter';
import {AddEventListenerOptions} from '../EventEmitter/EventTargetLike';
import {EmitterSubscription} from '../vendor/emitter/EventEmitter';

export interface LinkingImpl extends NativeEventEmitter {
Expand All @@ -18,6 +19,7 @@ export interface LinkingImpl extends NativeEventEmitter {
addEventListener(
type: 'url',
handler: (event: {url: string}) => void,
options?: AddEventListenerOptions,
): EmitterSubscription;

/**
Expand Down
9 changes: 8 additions & 1 deletion packages/react-native/Libraries/Linking/Linking.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import type {EventSubscription} from '../vendor/emitter/EventEmitter';

import {adaptToEventTarget} from '../EventEmitter/EventTargetLike';
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
import Platform from '../Utilities/Platform';
import NativeIntentAndroid from './NativeIntentAndroid';
Expand All @@ -35,8 +36,14 @@ class LinkingImpl extends NativeEventEmitter<LinkingEventDefinitions> {
addEventListener<K: keyof LinkingEventDefinitions>(
eventType: K,
listener: (...LinkingEventDefinitions[K]) => unknown,
options?: ?{|once?: ?boolean, signal?: ?mixed|},
): EventSubscription {
return this.addListener(eventType, listener);
return adaptToEventTarget(
(...args) => this.addListener(...args),
eventType,
listener,
options,
);
}

/**
Expand Down
33 changes: 22 additions & 11 deletions packages/react-native/Libraries/Utilities/BackHandler.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

import NativeDeviceEventManager from '../../Libraries/NativeModules/specs/NativeDeviceEventManager';
import {adaptToEventTarget} from '../EventEmitter/EventTargetLike';
import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter';

const DEVICE_BACK_EVENT = 'hardwareBackPress';
Expand Down Expand Up @@ -59,8 +60,27 @@ type TBackHandler = {
+addEventListener: (
eventName: BackPressEventName,
handler: BackPressHandler,
options?: ?{|once?: ?boolean, signal?: ?mixed|},
) => {remove: () => void, ...},
};

const addListener = (
eventName: BackPressEventName,
handler: BackPressHandler,
) => {
if (_backPressSubscriptions.indexOf(handler) === -1) {
_backPressSubscriptions.push(handler);
}
return {
remove: (): void => {
const index = _backPressSubscriptions.indexOf(handler);
if (index !== -1) {
_backPressSubscriptions.splice(index, 1);
}
},
};
};

const BackHandler: TBackHandler = {
exitApp: function (): void {
if (!NativeDeviceEventManager) {
Expand All @@ -78,18 +98,9 @@ const BackHandler: TBackHandler = {
addEventListener: function (
eventName: BackPressEventName,
handler: BackPressHandler,
options?: ?{|once?: ?boolean, signal?: ?mixed|},
): {remove: () => void, ...} {
if (_backPressSubscriptions.indexOf(handler) === -1) {
_backPressSubscriptions.push(handler);
}
return {
remove: (): void => {
const index = _backPressSubscriptions.indexOf(handler);
if (index !== -1) {
_backPressSubscriptions.splice(index, 1);
}
},
};
return adaptToEventTarget(addListener, eventName, handler, options);
},
};

Expand Down
2 changes: 2 additions & 0 deletions packages/react-native/Libraries/Utilities/BackHandler.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import {NativeEventSubscription} from '../EventEmitter/RCTNativeAppEventEmitter';
import {AddEventListenerOptions} from '../EventEmitter/EventTargetLike';

export type BackPressEventName = 'hardwareBackPress';

Expand All @@ -27,6 +28,7 @@ export interface BackHandlerStatic {
addEventListener(
eventName: BackPressEventName,
handler: () => boolean | null | undefined,
options?: AddEventListenerOptions,
): NativeEventSubscription;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/react-native/Libraries/Utilities/Dimensions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import {EmitterSubscription} from '../vendor/emitter/EventEmitter';
import {AddEventListenerOptions} from '../EventEmitter/EventTargetLike';

// Used by Dimensions below
export interface ScaledSize {
Expand Down Expand Up @@ -71,6 +72,7 @@ export interface Dimensions {
window: ScaledSize;
screen: ScaledSize;
}) => void,
options?: AddEventListenerOptions,
): EmitterSubscription;
}

Expand Down
9 changes: 8 additions & 1 deletion packages/react-native/Libraries/Utilities/Dimensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* @format
*/

import {adaptToEventTarget} from '../EventEmitter/EventTargetLike';
import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter';
import EventEmitter, {
type EventSubscription,
Expand Down Expand Up @@ -106,13 +107,19 @@ class Dimensions {
static addEventListener(
type: 'change',
handler: Function,
options?: ?{|once?: ?boolean, signal?: ?mixed|},
): EventSubscription {
invariant(
type === 'change',
'Trying to subscribe to unknown event: "%s"',
type,
);
return eventEmitter.addListener(type, handler);
return adaptToEventTarget(
(...args) => eventEmitter.addListener(...args),
type,
handler,
options,
);
}
}

Expand Down
Loading
Loading