diff --git a/packages/react-native-renderer/src/ReactFabricEventEmitter.js b/packages/react-native-renderer/src/ReactFabricEventEmitter.js index d8492156fdc7c..c3b89e91779a3 100644 --- a/packages/react-native-renderer/src/ReactFabricEventEmitter.js +++ b/packages/react-native-renderer/src/ReactFabricEventEmitter.js @@ -18,12 +18,12 @@ import {batchedUpdates} from './legacy-events/ReactGenericBatching'; import accumulateInto from './legacy-events/accumulateInto'; import {plugins} from './legacy-events/EventPluginRegistry'; -import getListener from './ReactNativeGetListener'; +import getListeners from './ReactNativeGetListeners'; import {runEventsInBatch} from './legacy-events/EventBatching'; import {RawEventEmitter} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; -export {getListener, registrationNameModules as registrationNames}; +export {getListeners, registrationNameModules as registrationNames}; /** * Allows registered plugins an opportunity to extract events from top-level diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index bec60bff22c99..af9b9926e083f 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -10,6 +10,7 @@ import type {ReactNodeList, OffscreenMode} from 'shared/ReactTypes'; import type {ElementRef} from 'react'; import type { + CustomEvent, HostComponent, MeasureInWindowOnSuccessCallback, MeasureLayoutOnSuccessCallback, @@ -95,6 +96,19 @@ export type RendererInspectionConfig = $ReadOnly<{| ) => void, |}>; +// TODO: find a better place for this type to live +export type EventListenerOptions = $ReadOnly<{| + capture?: boolean, + once?: boolean, + passive?: boolean, + signal: mixed, // not yet implemented +|}>; +export type EventListenerRemoveOptions = $ReadOnly<{| + capture?: boolean, +|}>; +// TODO: this will be changed in the future to be w3c-compatible and allow "EventListener" objects as well as functions. +export type EventListener = Function; + // TODO: Remove this conditional once all changes have propagated. if (registerEventHandler) { /** @@ -103,6 +117,14 @@ if (registerEventHandler) { registerEventHandler(dispatchEvent); } +type InternalEventListeners = { + [string]: {| + listener: EventListener, + options: EventListenerOptions, + invalidated: boolean, + |}[], +}; + /** * This is used for refs on host components. */ @@ -111,6 +133,7 @@ class ReactFabricHostComponent { viewConfig: ViewConfig; currentProps: Props; _internalInstanceHandle: Object; + _eventListeners: ?InternalEventListeners; constructor( tag: number, @@ -122,6 +145,7 @@ class ReactFabricHostComponent { this.viewConfig = viewConfig; this.currentProps = props; this._internalInstanceHandle = internalInstanceHandle; + this._eventListeners = null; } blur() { @@ -193,6 +217,101 @@ class ReactFabricHostComponent { return; } + + // This API (dispatchEvent, addEventListener, removeEventListener) attempts to adhere to the + // w3 Level2 Events spec as much as possible, treating HostComponent as a DOM node. + // + // Unless otherwise noted, these methods should "just work" and adhere to the W3 specs. + // If they deviate in a way that is not explicitly noted here, you've found a bug! + // + // See: + // * https://www.w3.org/TR/DOM-Level-2-Events/events.html + // * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent + // * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener + // * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener + dispatchEvent_unstable(event: CustomEvent) { + dispatchEvent(this._internalInstanceHandle, event.type, event); + } + + // Deviations from spec/TODOs: + // (1) listener must currently be a function, we do not support EventListener objects yet. + // (2) we do not support the `signal` option / AbortSignal yet + addEventListener_unstable( + eventType: string, + listener: EventListener, + options: EventListenerOptions | boolean, + ) { + if (typeof eventType !== 'string') { + throw new Error('addEventListener_unstable eventType must be a string'); + } + if (typeof listener !== 'function') { + throw new Error('addEventListener_unstable listener must be a function'); + } + + // The third argument is either boolean indicating "captures" or an object. + const optionsObj = + typeof options === 'object' && options !== null ? options : {}; + const capture = + (typeof options === 'boolean' ? options : optionsObj.capture) || false; + const once = optionsObj.once || false; + const passive = optionsObj.passive || false; + const signal = null; // TODO: implement signal/AbortSignal + + const eventListeners: InternalEventListeners = this._eventListeners || {}; + if (this._eventListeners === null) { + this._eventListeners = eventListeners; + } + + const namedEventListeners = eventListeners[eventType] || []; + if (eventListeners[eventType] == null) { + eventListeners[eventType] = namedEventListeners; + } + + namedEventListeners.push({ + listener: listener, + invalidated: false, + options: { + capture: capture, + once: once, + passive: passive, + signal: signal, + }, + }); + } + + // See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener + removeEventListener_unstable( + eventType: string, + listener: EventListener, + options: EventListenerRemoveOptions | boolean, + ) { + // eventType and listener must be referentially equal to be removed from the listeners + // data structure, but in "options" we only check the `capture` flag, according to spec. + // That means if you add the same function as a listener with capture set to true and false, + // you must also call removeEventListener twice with capture set to true/false. + const optionsObj = + typeof options === 'object' && options !== null ? options : {}; + const capture = + (typeof options === 'boolean' ? options : optionsObj.capture) || false; + + // If there are no event listeners or named event listeners, we can bail early - our + // job is already done. + const eventListeners = this._eventListeners; + if (!eventListeners) { + return; + } + const namedEventListeners = eventListeners[eventType]; + if (!namedEventListeners) { + return; + } + + eventListeners[eventType] = namedEventListeners.filter(listenerObj => { + return !( + listenerObj.listener === listener && + listenerObj.options.capture === capture + ); + }); + } } // eslint-disable-next-line no-unused-expressions diff --git a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js index 212b181c08294..7f7ee3b508013 100644 --- a/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js +++ b/packages/react-native-renderer/src/ReactNativeBridgeEventPlugin.js @@ -10,11 +10,15 @@ import type {AnyNativeEvent} from './legacy-events/PluginModuleType'; import type {TopLevelType} from './legacy-events/TopLevelEventTypes'; import SyntheticEvent from './legacy-events/SyntheticEvent'; +import type {PropagationPhases} from './legacy-events/PropagationPhases'; // Module provided by RN: -import {ReactNativeViewConfigRegistry} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; +import { + CustomEvent, + ReactNativeViewConfigRegistry, +} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; import accumulateInto from './legacy-events/accumulateInto'; -import getListener from './ReactNativeGetListener'; +import getListeners from './ReactNativeGetListeners'; import forEachAccumulated from './legacy-events/forEachAccumulated'; import {HostComponent} from 'react-reconciler/src/ReactWorkTags'; @@ -26,10 +30,15 @@ const { // Start of inline: the below functions were inlined from // EventPropagator.js, as they deviated from ReactDOM's newer // implementations. -function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) { +function listenersAtPhase( + inst, + event, + propagationPhase: PropagationPhases, + isCustomEvent: boolean, +) { const registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase]; - return getListener(inst, registrationName); + return getListeners(inst, registrationName, propagationPhase, isCustomEvent); } function accumulateDirectionalDispatches(inst, phase, event) { @@ -38,13 +47,21 @@ function accumulateDirectionalDispatches(inst, phase, event) { console.error('Dispatching inst must not be null'); } } - const listener = listenerAtPhase(inst, event, phase); - if (listener) { + const listeners = listenersAtPhase( + inst, + event, + phase, + event instanceof CustomEvent, + ); + if (listeners && listeners.length > 0) { event._dispatchListeners = accumulateInto( event._dispatchListeners, - listener, + listeners, ); - event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); + const insts = listeners.map(() => { + return inst; + }); + event._dispatchInstances = accumulateInto(event._dispatchInstances, insts); } } @@ -66,7 +83,12 @@ function getParent(inst) { /** * Simulates the traversal of a two-phase, capture/bubble event dispatch. */ -export function traverseTwoPhase(inst: Object, fn: Function, arg: Function) { +export function traverseTwoPhase( + inst: Object, + fn: Function, + arg: Function, + bubbles: boolean, +) { const path = []; while (inst) { path.push(inst); @@ -78,12 +100,28 @@ export function traverseTwoPhase(inst: Object, fn: Function, arg: Function) { } for (i = 0; i < path.length; i++) { fn(path[i], 'bubbled', arg); + // It's possible this is false for custom events. + if (!bubbles) { + break; + } } } function accumulateTwoPhaseDispatchesSingle(event) { if (event && event.dispatchConfig.phasedRegistrationNames) { - traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event); + // bubbles is only set on the dispatchConfig for custom events. + // The `event` param here at this point is a SyntheticEvent, not an Event or CustomEvent. + const bubbles = + event.dispatchConfig.isCustomEvent === true + ? !!event.dispatchConfig.bubbles + : true; + + traverseTwoPhase( + event._targetInst, + accumulateDirectionalDispatches, + event, + bubbles, + ); } } @@ -103,13 +141,27 @@ function accumulateDispatches( ): void { if (inst && event && event.dispatchConfig.registrationName) { const registrationName = event.dispatchConfig.registrationName; - const listener = getListener(inst, registrationName); - if (listener) { + // Since we "do not look for phased registration names", that + // should be the same as "bubbled" here, for all intents and purposes...? + const listeners = getListeners( + inst, + registrationName, + 'bubbled', + !!event.dispatchConfig.isCustomEvent, + ); + if (listeners) { event._dispatchListeners = accumulateInto( event._dispatchListeners, - listener, + listeners, + ); + // an inst for every listener + const insts = listeners.map(() => { + return inst; + }); + event._dispatchInstances = accumulateInto( + event._dispatchInstances, + insts, ); - event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); } } } @@ -130,7 +182,6 @@ function accumulateDirectDispatches(events: ?(Array | Object)) { } // End of inline -type PropagationPhases = 'bubbled' | 'captured'; const ReactNativeBridgeEventPlugin = { eventTypes: {}, @@ -147,8 +198,30 @@ const ReactNativeBridgeEventPlugin = { } const bubbleDispatchConfig = customBubblingEventTypes[topLevelType]; const directDispatchConfig = customDirectEventTypes[topLevelType]; + let customEventConfig = null; + if (nativeEvent instanceof CustomEvent) { + // $FlowFixMe + if (topLevelType.indexOf('on') !== 0) { + throw new Error('Custom event name must start with "on"'); + } + nativeEvent.isTrusted = false; + // For now, this custom event name should technically not be used - + // CustomEvents emitted in the system do not result in calling prop handlers. + customEventConfig = { + registrationName: topLevelType, + isCustomEvent: true, + bubbles: nativeEvent.bubbles, + phasedRegistrationNames: { + bubbled: topLevelType, + // Unlike with the props-based handlers, capture events are registered + // to the HostComponent event emitter with the same name but a flag indicating + // that the handler is for the capture phase. + captured: topLevelType, + }, + }; + } - if (!bubbleDispatchConfig && !directDispatchConfig) { + if (!bubbleDispatchConfig && !directDispatchConfig && !customEventConfig) { throw new Error( // $FlowFixMe - Flow doesn't like this string coercion because DOMTopLevelEventType is opaque `Unsupported top level event type "${topLevelType}" dispatched`, @@ -156,14 +229,22 @@ const ReactNativeBridgeEventPlugin = { } const event = SyntheticEvent.getPooled( - bubbleDispatchConfig || directDispatchConfig, + bubbleDispatchConfig || directDispatchConfig || customEventConfig, targetInst, nativeEvent, nativeEventTarget, ); - if (bubbleDispatchConfig) { + if (bubbleDispatchConfig || customEventConfig) { + // All CustomEvents go through two-phase dispatching, even if they + // are non-bubbling events, which is why we put the `bubbles` param + // in the config for CustomEvents only. + // CustomEvents are not emitted to prop handler functions ever. + // Native two-phase events will be emitted to prop handler functions + // and to HostComponent event listeners. accumulateTwoPhaseDispatches(event); } else if (directDispatchConfig) { + // Direct dispatched events do not go to HostComponent EventEmitters, + // they *only* go to the prop function handlers. accumulateDirectDispatches(event); } else { return null; diff --git a/packages/react-native-renderer/src/ReactNativeEventEmitter.js b/packages/react-native-renderer/src/ReactNativeEventEmitter.js index 2ba35aed39b9f..91816a53f82da 100644 --- a/packages/react-native-renderer/src/ReactNativeEventEmitter.js +++ b/packages/react-native-renderer/src/ReactNativeEventEmitter.js @@ -17,12 +17,12 @@ import {registrationNameModules} from './legacy-events/EventPluginRegistry'; import {batchedUpdates} from './legacy-events/ReactGenericBatching'; import {runEventsInBatch} from './legacy-events/EventBatching'; import {plugins} from './legacy-events/EventPluginRegistry'; -import getListener from './ReactNativeGetListener'; +import getListeners from './ReactNativeGetListeners'; import accumulateInto from './legacy-events/accumulateInto'; import {getInstanceFromNode} from './ReactNativeComponentTree'; -export {getListener, registrationNameModules as registrationNames}; +export {getListeners, registrationNameModules as registrationNames}; /** * Version of `ReactBrowserEventEmitter` that works on the receiving side of a diff --git a/packages/react-native-renderer/src/ReactNativeGetListener.js b/packages/react-native-renderer/src/ReactNativeGetListener.js deleted file mode 100644 index 4f76fddd29e7b..0000000000000 --- a/packages/react-native-renderer/src/ReactNativeGetListener.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * @flow - */ - -import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; - -import {getFiberCurrentPropsFromNode} from './legacy-events/EventPluginUtils'; - -export default function getListener( - inst: Fiber, - registrationName: string, -): Function | null { - const stateNode = inst.stateNode; - if (stateNode === null) { - // Work in progress (ex: onload events in incremental mode). - return null; - } - const props = getFiberCurrentPropsFromNode(stateNode); - if (props === null) { - // Work in progress. - return null; - } - const listener = props[registrationName]; - - if (listener && typeof listener !== 'function') { - throw new Error( - `Expected \`${registrationName}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`, - ); - } - - return listener; -} diff --git a/packages/react-native-renderer/src/ReactNativeGetListeners.js b/packages/react-native-renderer/src/ReactNativeGetListeners.js new file mode 100644 index 0000000000000..2fbfab9429d49 --- /dev/null +++ b/packages/react-native-renderer/src/ReactNativeGetListeners.js @@ -0,0 +1,126 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @flow + */ + +import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; +import type {PropagationPhases} from './legacy-events/PropagationPhases'; + +import {getFiberCurrentPropsFromNode} from './legacy-events/EventPluginUtils'; + +/** + * Get a list of listeners for a specific event, in-order. + * For React Native we treat the props-based function handlers + * as the first-class citizens, and they are always executed first + * for both capture and bubbling phase. + * + * We need "phase" propagated to this point to support the HostComponent + * EventEmitter API, which does not mutate the name of the handler based + * on phase (whereas prop handlers are registered as `onMyEvent` and `onMyEvent_Capture`). + * + * Additionally, we do NOT want CustomEvent events dispatched through + * the EventEmitter directly in JS to be emitted to prop handlers. This + * may change in the future. OTOH, native events emitted into React Native + * will be emitted both to the prop handler function and to imperative event + * listeners. + */ +export default function getListeners( + inst: Fiber, + registrationName: string, + phase: PropagationPhases, + isCustomEvent: boolean, +): Array | null { + // Previously, there was only one possible listener for an event: + // the onEventName property in props. + // Now, it is also possible to have N listeners + // for a specific event on a node. Thus, we accumulate all of the listeners, + // including the props listener, and return a function that calls them all in + // order, starting with the handler prop and then the listeners in order. + // We return either a non-empty array or null. + let listeners = null; + + const stateNode = inst.stateNode; + + if (stateNode === null) { + return null; + } + + // If null: Work in progress (ex: onload events in incremental mode). + if (!isCustomEvent) { + const props = getFiberCurrentPropsFromNode(stateNode); + if (props === null) { + // Work in progress. + return null; + } + const listener = props[registrationName]; + + if (listener && typeof listener !== 'function') { + throw new Error( + `Expected \`${registrationName}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`, + ); + } + + if (listener) { + if (listeners === null) { + listeners = []; + } + listeners.push(listener); + } + } + + // Get imperative event listeners for this event + if ( + stateNode.canonical && + stateNode.canonical._eventListeners && + stateNode.canonical._eventListeners[registrationName] && + stateNode.canonical._eventListeners[registrationName].length > 0 + ) { + const eventListeners = + stateNode.canonical._eventListeners[registrationName]; + const requestedPhaseIsCapture = phase === 'captured'; + + eventListeners.forEach(listenerObj => { + // Make sure phase of listener matches requested phase + const isCaptureEvent = + listenerObj.options.capture != null && listenerObj.options.capture; + if (isCaptureEvent !== requestedPhaseIsCapture) { + return; + } + + // Only call once? + // If so, we ensure that it's only called once by setting a flag + // and by removing it from eventListeners once it is called (but only + // when it's actually been executed). + if (listeners === null) { + listeners = []; + } + if (listenerObj.options.once) { + listeners.push(function() { + const args = Array.prototype.slice.call(arguments); + + // Guard against function being called more than once in + // case there are somehow multiple in-flight references to + // it being processed + if (!listenerObj.invalidated) { + listenerObj.listener.apply(null, args); + listenerObj.invalidated = true; + } + + // Remove from the event listener once it's been called + stateNode.canonical.removeEventListener_unstable( + registrationName, + listenerObj.listener, + listenerObj.capture, + ); + }); + } else { + listeners.push(listenerObj.listener); + } + }); + } + + return listeners; +} diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js index 55150e83517e6..857a40a2ccc4d 100644 --- a/packages/react-native-renderer/src/ReactNativeTypes.js +++ b/packages/react-native-renderer/src/ReactNativeTypes.js @@ -158,6 +158,11 @@ export type TouchedViewDataAtPoint = $ReadOnly<{| ...InspectorData, |}>; +export interface CustomEvent extends Event { + constructor(type: string, eventInitDict?: CustomEvent$Init): void; + detail: any; +} + /** * Flat ReactNative renderer bundles are too big for Flow to parse efficiently. * Provide minimal Flow typing for the high-level RN API and call it a day. diff --git a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/CustomEvent.js b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/CustomEvent.js new file mode 100644 index 0000000000000..0b0b79c5ddddf --- /dev/null +++ b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/CustomEvent.js @@ -0,0 +1,14 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +// See the react-native repository for a full implementation. +// This is just a stub, currently to pass `instanceof` checks. +const CustomEvent = jest.fn(); + +module.exports = {default: CustomEvent}; diff --git a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js index 3eaf3a5a38057..dbfb9910c943e 100644 --- a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js +++ b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js @@ -44,4 +44,7 @@ module.exports = { get RawEventEmitter() { return require('./RawEventEmitter').default; }, + get CustomEvent() { + return require('./CustomEvent').default; + }, }; diff --git a/packages/react-native-renderer/src/legacy-events/PropagationPhases.js b/packages/react-native-renderer/src/legacy-events/PropagationPhases.js new file mode 100644 index 0000000000000..7d05d30be8c47 --- /dev/null +++ b/packages/react-native-renderer/src/legacy-events/PropagationPhases.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export type PropagationPhases = 'bubbled' | 'captured'; diff --git a/packages/react-native-renderer/src/legacy-events/SyntheticEvent.js b/packages/react-native-renderer/src/legacy-events/SyntheticEvent.js index 84d39bbd603d2..1d1d7a3b93bb7 100644 --- a/packages/react-native-renderer/src/legacy-events/SyntheticEvent.js +++ b/packages/react-native-renderer/src/legacy-events/SyntheticEvent.js @@ -77,6 +77,11 @@ function SyntheticEvent( this._dispatchListeners = null; this._dispatchInstances = null; + // React Native Event polyfill - enable Event proxying calls to SyntheticEvent + if (nativeEvent.setSyntheticEvent) { + nativeEvent.setSyntheticEvent(this); + } + const Interface = this.constructor.Interface; for (const propName in Interface) { if (!Interface.hasOwnProperty(propName)) { @@ -118,12 +123,22 @@ Object.assign(SyntheticEvent.prototype, { return; } + // React Native Event polyfill - disable recursion + if (event.setSyntheticEvent) { + event.setSyntheticEvent(null); + } + if (event.preventDefault) { event.preventDefault(); } else if (typeof event.returnValue !== 'unknown') { event.returnValue = false; } this.isDefaultPrevented = functionThatReturnsTrue; + + // React Native Event polyfill - reenable Event proxying calls to SyntheticEvent + if (event.setSyntheticEvent) { + event.setSyntheticEvent(this); + } }, stopPropagation: function() { @@ -132,6 +147,11 @@ Object.assign(SyntheticEvent.prototype, { return; } + // React Native Event polyfill - disable recursion + if (event.setSyntheticEvent) { + event.setSyntheticEvent(null); + } + if (event.stopPropagation) { event.stopPropagation(); } else if (typeof event.cancelBubble !== 'unknown') { @@ -144,6 +164,11 @@ Object.assign(SyntheticEvent.prototype, { } this.isPropagationStopped = functionThatReturnsTrue; + + // React Native Event polyfill - reenable Event proxying calls to SyntheticEvent + if (event.setSyntheticEvent) { + event.setSyntheticEvent(this); + } }, /** diff --git a/packages/shared/ReactVersion.js b/packages/shared/ReactVersion.js index 8d357322ca3ac..fbc65449faf8f 100644 --- a/packages/shared/ReactVersion.js +++ b/packages/shared/ReactVersion.js @@ -1,16 +1 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -// TODO: this is special because it gets imported during build. -// -// TODO: 17.0.3 has not been released to NPM; -// It exists as a placeholder so that DevTools can support work tag changes between releases. -// When we next publish a release (either 17.0.3 or 17.1.0), update the matching TODO in backend/renderer.js -// TODO: This module is used both by the release scripts and to expose a version -// at runtime. We should instead inject the version number as part of the build -// process, and use the ReactVersions.js module as the single source of truth. -export default '17.0.3'; +export default '18.0.0-rc.0-experimental-049a50b73-20220214'; diff --git a/scripts/flow/react-native-host-hooks.js b/scripts/flow/react-native-host-hooks.js index c7a2fc8dc3212..896964f063643 100644 --- a/scripts/flow/react-native-host-hooks.js +++ b/scripts/flow/react-native-host-hooks.js @@ -138,6 +138,7 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface' emit: (channel: string, event: RawEventEmitterEvent) => string, ... }; + declare export var CustomEvent: CustomEvent; } declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInitializeCore' {