Skip to content
Merged
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
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,7 @@ module.exports = {
globals: {
nativeFabricUIManager: 'readonly',
RN$enableMicrotasksInReact: 'readonly',
RN$isNativeEventTargetEventDispatchingEnabled: 'readonly',
},
},
{
Expand Down
69 changes: 69 additions & 0 deletions packages/react-native-renderer/src/LegacySyntheticEvent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* 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
*/

/* globals Event$Init */

/**
* A bridge event class that extends the W3C Event interface and carries
* the native event payload. This is used as a compatibility layer during
* the migration from the legacy SyntheticEvent system to EventTarget-based
* dispatching.
*/
export default class LegacySyntheticEvent extends Event {
_nativeEvent: {[string]: mixed};
_propagationStopped: boolean;

constructor(
type: string,
options: Event$Init,
nativeEvent: {[string]: mixed},
) {
super(type, options);
this._nativeEvent = nativeEvent;
this._propagationStopped = false;
}

get nativeEvent(): {[string]: mixed} {
return this._nativeEvent;
}

stopPropagation(): void {
super.stopPropagation();
this._propagationStopped = true;
}

stopImmediatePropagation(): void {
super.stopImmediatePropagation();
this._propagationStopped = true;
}

/**
* No-op for backward compatibility. The legacy SyntheticEvent system
* used pooling which required calling persist() to keep the event.
* With EventTarget-based dispatching, events are never pooled.
*/
persist(): void {
// No-op
}

/**
* Backward-compatible wrapper for `defaultPrevented`.
*/
isDefaultPrevented(): boolean {
return this.defaultPrevented;
}

/**
* Backward-compatible wrapper. Returns true if stopPropagation()
* has been called.
*/
isPropagationStopped(): boolean {
return this._propagationStopped;
}
}
78 changes: 65 additions & 13 deletions packages/react-native-renderer/src/ReactFabricEventEmitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,23 @@ import accumulateInto from './legacy-events/accumulateInto';
import getListener from './ReactNativeGetListener';
import {runEventsInBatch} from './legacy-events/EventBatching';

import {RawEventEmitter} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
import {
RawEventEmitter,
ReactNativeViewConfigRegistry,
dispatchTrustedEvent,
setEventInitTimeStamp,
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
import {getPublicInstance} from './ReactFiberConfigFabric';
import LegacySyntheticEvent from './LegacySyntheticEvent';
import {topLevelTypeToEventName} from './ReactNativeEventTypeMapping';
import {processResponderEvent} from './ReactNativeResponder';
import {enableNativeEventTargetEventDispatching} from './ReactNativeFeatureFlags';

export {getListener, registrationNameModules as registrationNames};

const {customBubblingEventTypes, customDirectEventTypes} =
ReactNativeViewConfigRegistry;

/**
* Allows registered plugins an opportunity to extract events from top-level
* native browser events.
Expand All @@ -47,10 +59,12 @@ function extractPluginEvents(
nativeEventTarget: null | EventTarget,
): Array<ReactSyntheticEvent> | ReactSyntheticEvent | null {
let events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null = null;
const legacyPlugins = ((plugins: any): Array<LegacyPluginModule<Event>>);
const legacyPlugins = ((plugins: any): Array<
LegacyPluginModule<AnyNativeEvent>,
>);
for (let i = 0; i < legacyPlugins.length; i++) {
// Not every plugin in the ordering may be loaded at runtime.
const possiblePlugin: LegacyPluginModule<AnyNativeEvent> = legacyPlugins[i];
const possiblePlugin = legacyPlugins[i];
if (possiblePlugin) {
const extractedEvents = possiblePlugin.extractEvents(
topLevelType,
Expand Down Expand Up @@ -84,8 +98,12 @@ function runExtractedPluginEventsInBatch(
export function dispatchEvent(
target: null | Object,
topLevelType: RNTopLevelEventType,
nativeEvent: AnyNativeEvent,
nativeEventParam: mixed,
) {
const nativeEvent: AnyNativeEvent =
nativeEventParam != null && typeof nativeEventParam === 'object'
? (nativeEventParam: any)
: {};
const targetFiber = (target: null | Fiber);

let eventTarget = null;
Expand Down Expand Up @@ -121,18 +139,52 @@ export function dispatchEvent(
// Note that extracted events are *not* emitted,
// only events that have a 1:1 mapping with a native event, at least for now.
const event = {eventName: topLevelType, nativeEvent};
// $FlowFixMe[class-object-subtyping] found when upgrading Flow
RawEventEmitter.emit(topLevelType, event);
// $FlowFixMe[class-object-subtyping] found when upgrading Flow
RawEventEmitter.emit('*', event);

// Heritage plugin event system
runExtractedPluginEventsInBatch(
topLevelType,
targetFiber,
nativeEvent,
eventTarget,
);
if (enableNativeEventTargetEventDispatching()) {
// Process responder events before normal event dispatch.
// This handles touch negotiation (onStartShouldSetResponder, etc.)
processResponderEvent(topLevelType, targetFiber, nativeEvent);

// New EventTarget-based dispatch path
if (eventTarget != null) {
const bubbleDispatchConfig = customBubblingEventTypes[topLevelType];
const directDispatchConfig = customDirectEventTypes[topLevelType];
const bubbles = bubbleDispatchConfig != null;

// Skip events that are not registered in the view config
if (bubbles || directDispatchConfig != null) {
const eventName = topLevelTypeToEventName(topLevelType);
const options = {
bubbles,
cancelable: true,
};
// Preserve the native event timestamp for backwards compatibility.
// The legacy SyntheticEvent system used nativeEvent.timeStamp || nativeEvent.timestamp.
const nativeTimestamp =
nativeEvent.timeStamp ?? nativeEvent.timestamp;
if (typeof nativeTimestamp === 'number') {
setEventInitTimeStamp(options, nativeTimestamp);
}
const syntheticEvent = new LegacySyntheticEvent(
eventName,
options,
nativeEvent,
);
// $FlowFixMe[incompatible-call]
dispatchTrustedEvent(eventTarget, syntheticEvent);
}
}
} else {
// Heritage plugin event system
runExtractedPluginEventsInBatch(
topLevelType,
targetFiber,
nativeEvent,
eventTarget,
);
}
});
// React Native doesn't use ReactControlledComponent but if it did, here's
// where it would do it.
Expand Down
6 changes: 4 additions & 2 deletions packages/react-native-renderer/src/ReactNativeEventEmitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,12 @@ function extractPluginEvents(
nativeEventTarget: null | EventTarget,
): Array<ReactSyntheticEvent> | ReactSyntheticEvent | null {
let events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null = null;
const legacyPlugins = ((plugins: any): Array<LegacyPluginModule<Event>>);
const legacyPlugins = ((plugins: any): Array<
LegacyPluginModule<AnyNativeEvent>,
>);
for (let i = 0; i < legacyPlugins.length; i++) {
// Not every plugin in the ordering may be loaded at runtime.
const possiblePlugin: LegacyPluginModule<AnyNativeEvent> = legacyPlugins[i];
const possiblePlugin = legacyPlugins[i];
if (possiblePlugin) {
const extractedEvents = possiblePlugin.extractEvents(
topLevelType,
Expand Down
24 changes: 24 additions & 0 deletions packages/react-native-renderer/src/ReactNativeEventTypeMapping.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* 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
*/

/**
* Converts a topLevelType (e.g., "topPress") to a DOM event name (e.g., "press").
* Strips the "top" prefix and lowercases the result.
*/
export function topLevelTypeToEventName(topLevelType: string): string {
const fourthChar = topLevelType.charCodeAt(3);
if (
topLevelType.startsWith('top') &&
fourthChar >= 65 /* A */ &&
fourthChar <= 90 /* Z */
) {
return topLevelType.slice(3).toLowerCase();
}
return topLevelType;
}
23 changes: 23 additions & 0 deletions packages/react-native-renderer/src/ReactNativeFeatureFlags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* 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
*/

// These globals are set by React Native (e.g. in setUpDOM.js, setUpTimers.js)
// and provide access to RN's feature flags. We use global functions because we
// don't have another mechanism to pass feature flags from RN to React in OSS.
// Values are lazily evaluated and cached on first access.

let _enableNativeEventTargetEventDispatching: boolean | null = null;
export function enableNativeEventTargetEventDispatching(): boolean {
if (_enableNativeEventTargetEventDispatching == null) {
_enableNativeEventTargetEventDispatching =
typeof RN$isNativeEventTargetEventDispatchingEnabled === 'function' &&
RN$isNativeEventTargetEventDispatchingEnabled();
}
return _enableNativeEventTargetEventDispatching;
}
Loading
Loading