diff --git a/package.json b/package.json index fc1855bc3982..cd89bbb3b25c 100644 --- a/package.json +++ b/package.json @@ -107,8 +107,8 @@ "nullthrows": "^1.1.1", "prettier": "3.6.2", "prettier-plugin-hermes-parser": "0.36.0", - "react": "19.2.3", - "react-test-renderer": "19.2.3", + "react": "19.2.7", + "react-test-renderer": "19.2.7", "rimraf": "^3.0.2", "shelljs": "^0.8.5", "signedsource": "^2.0.0", @@ -120,7 +120,7 @@ "ws": "^7.5.10" }, "resolutions": { - "react-is": "19.2.3", + "react-is": "19.2.7", "on-headers": "1.1.0", "compression": "1.8.1", "@microsoft/api-extractor/minimatch": "3.1.4", diff --git a/packages/jest-preset/package.json b/packages/jest-preset/package.json index 987afe28dacf..29f2ec3e3f26 100644 --- a/packages/jest-preset/package.json +++ b/packages/jest-preset/package.json @@ -35,6 +35,6 @@ "regenerator-runtime": "^0.13.2" }, "peerDependencies": { - "react": "^19.2.3" + "react": "^19.2.7" } } diff --git a/packages/react-native/Libraries/Renderer/implementations/ReactFabric-dev.js b/packages/react-native/Libraries/Renderer/implementations/ReactFabric-dev.js index daab0e35a8e5..efe89e3067f5 100644 --- a/packages/react-native/Libraries/Renderer/implementations/ReactFabric-dev.js +++ b/packages/react-native/Libraries/Renderer/implementations/ReactFabric-dev.js @@ -7,7 +7,7 @@ * @noflow * @nolint * @preventMunge - * @generated SignedSource<> + * @generated SignedSource<<49a1a924bb8b9fa0ef032e4799b38449>> * * This file was sync'd from the facebook/react repository. */ @@ -18901,10 +18901,10 @@ __DEV__ && (function () { var internals = { bundleType: 1, - version: "19.2.3", + version: "19.2.7", rendererPackageName: "react-native-renderer", currentDispatcherRef: ReactSharedInternals, - reconcilerVersion: "19.2.3" + reconcilerVersion: "19.2.7" }; null !== extraDevToolsConfig && (internals.rendererConfig = extraDevToolsConfig); diff --git a/packages/react-native/Libraries/Renderer/implementations/ReactFabric-prod.js b/packages/react-native/Libraries/Renderer/implementations/ReactFabric-prod.js index 2625bf690ab0..f1317527757a 100644 --- a/packages/react-native/Libraries/Renderer/implementations/ReactFabric-prod.js +++ b/packages/react-native/Libraries/Renderer/implementations/ReactFabric-prod.js @@ -7,7 +7,7 @@ * @noflow * @nolint * @preventMunge - * @generated SignedSource<<8d29d23a1c540d7502dd188e691eb725>> + * @generated SignedSource<> * * This file was sync'd from the facebook/react repository. */ @@ -10495,10 +10495,10 @@ batchedUpdatesImpl = function (fn, a) { var roots = new Map(), internals$jscomp$inline_1245 = { bundleType: 0, - version: "19.2.3", + version: "19.2.7", rendererPackageName: "react-native-renderer", currentDispatcherRef: ReactSharedInternals, - reconcilerVersion: "19.2.3" + reconcilerVersion: "19.2.7" }; null !== extraDevToolsConfig && (internals$jscomp$inline_1245.rendererConfig = extraDevToolsConfig); diff --git a/packages/react-native/Libraries/Renderer/implementations/ReactFabric-profiling.js b/packages/react-native/Libraries/Renderer/implementations/ReactFabric-profiling.js index 544c75406ef2..c434b1423def 100644 --- a/packages/react-native/Libraries/Renderer/implementations/ReactFabric-profiling.js +++ b/packages/react-native/Libraries/Renderer/implementations/ReactFabric-profiling.js @@ -7,7 +7,7 @@ * @noflow * @nolint * @preventMunge - * @generated SignedSource<> + * @generated SignedSource<> * * This file was sync'd from the facebook/react repository. */ @@ -12298,10 +12298,10 @@ batchedUpdatesImpl = function (fn, a) { var roots = new Map(), internals$jscomp$inline_1537 = { bundleType: 0, - version: "19.2.3", + version: "19.2.7", rendererPackageName: "react-native-renderer", currentDispatcherRef: ReactSharedInternals, - reconcilerVersion: "19.2.3" + reconcilerVersion: "19.2.7" }; null !== extraDevToolsConfig && (internals$jscomp$inline_1537.rendererConfig = extraDevToolsConfig); diff --git a/packages/react-native/Libraries/Renderer/shims/ReactFabric.js b/packages/react-native/Libraries/Renderer/shims/ReactFabric.js index 15eeeb36bbaf..bc7baba8fa10 100644 --- a/packages/react-native/Libraries/Renderer/shims/ReactFabric.js +++ b/packages/react-native/Libraries/Renderer/shims/ReactFabric.js @@ -7,7 +7,9 @@ * @noformat * @nolint * @flow - * @generated SignedSource<> + * @generated SignedSource<<16b364e89f43b8a47832b0dfb98af11e>> + * + * This file was sync'd from the facebook/react repository. */ 'use strict'; diff --git a/packages/react-native/Libraries/Renderer/shims/ReactFeatureFlags.js b/packages/react-native/Libraries/Renderer/shims/ReactFeatureFlags.js index 1bf6b1b6cbaa..bf7cb70297c3 100644 --- a/packages/react-native/Libraries/Renderer/shims/ReactFeatureFlags.js +++ b/packages/react-native/Libraries/Renderer/shims/ReactFeatureFlags.js @@ -7,7 +7,9 @@ * @noformat * @nolint * @flow strict-local - * @generated SignedSource<<908f5fb85384725318e261f40e49d9a6>> + * @generated SignedSource<<1dd9e9c3f20e37ae14e485fc6ee3d9e9>> + * + * This file was sync'd from the facebook/react repository. */ 'use strict'; diff --git a/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js b/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js index 24cc37e31e76..0c23dc69b82f 100644 --- a/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js +++ b/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js @@ -7,7 +7,9 @@ * @noformat * @nolint * @flow strict - * @generated SignedSource<<4ab83fd2606d6a4d374ef914f231d9c1>> + * @generated SignedSource<<989e6e2e860dc2af7ba983849111bda8>> + * + * This file was sync'd from the facebook/react repository. */ import type { @@ -138,6 +140,36 @@ export type RenderRootOptions = { onDefaultTransitionIndicator?: () => void | (() => void), }; +/** + * 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. + */ +export type ReactNativeType = { + findHostInstance_DEPRECATED( + componentOrHandle: ?(React.ElementRef | number), + ): ?PublicInstance, + findNodeHandle( + componentOrHandle: ?(React.ElementRef | number), + ): ?number, + isChildPublicInstance(parent: PublicInstance, child: PublicInstance): boolean, + dispatchCommand( + handle: PublicInstance, + command: string, + args: Array, + ): void, + sendAccessibilityEvent(handle: PublicInstance, eventType: string): void, + render( + element: React.MixedElement, + containerTag: number, + callback: ?() => void, + options: ?RenderRootOptions, + ): ?React.ElementRef, + unmountComponentAtNode(containerTag: number): void, + unmountComponentAtNodeAndRemoveContainer(containerTag: number): void, + +unstable_batchedUpdates: (fn: (T) => void, bookkeeping: T) => void, + ... +}; + export opaque type Node = mixed; export opaque type InternalInstanceHandle = mixed; diff --git a/packages/react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry.js b/packages/react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry.js index 436b74da7674..ace3c4aac402 100644 --- a/packages/react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry.js +++ b/packages/react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry.js @@ -7,7 +7,9 @@ * @noformat * @nolint * @flow strict-local - * @generated SignedSource<<1f7876c0dc0b05685a730513dc410236>> + * @generated SignedSource<<67d18226984338ab9301147ce0a7d414>> + * + * This file was sync'd from the facebook/react repository. */ 'use strict'; diff --git a/packages/react-native/Libraries/Renderer/shims/createReactNativeComponentClass.js b/packages/react-native/Libraries/Renderer/shims/createReactNativeComponentClass.js index a069eb91e975..3be08df89565 100644 --- a/packages/react-native/Libraries/Renderer/shims/createReactNativeComponentClass.js +++ b/packages/react-native/Libraries/Renderer/shims/createReactNativeComponentClass.js @@ -7,7 +7,9 @@ * @noformat * @nolint * @flow strict-local - * @generated SignedSource<<52163887de05f1cff05388145cf85b3b>> + * @generated SignedSource<<556d1487de0b9e4a09cbc67dd130a884>> + * + * This file was sync'd from the facebook/react repository. */ 'use strict'; diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 47d4675facb7..81b662e103cf 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -150,7 +150,7 @@ "peerDependencies": { "@react-native/jest-preset": "0.87.0-main", "@types/react": "^19.1.1", - "react": "^19.2.3" + "react": "^19.2.7" }, "peerDependenciesMeta": { "@types/react": { diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 98dfa7b6eab0..940f56d10e73 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -1036,6 +1036,16 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + enableImperativeEvents: { + defaultValue: false, + metadata: { + description: + 'When enabled, ReactNativeElement and ReadOnlyText expose the public EventTarget API (addEventListener, removeEventListener, dispatchEvent). When disabled, those methods are removed from those final classes.', + expectedReleaseValue: true, + purpose: 'release', + }, + ossReleaseStage: 'none', + }, enableNativeEventTargetEventDispatching: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index cf32422016e7..fe98367b4bcf 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<77b178e216aa86a309f46cbf661d9122>> + * @generated SignedSource<<9ea39238fb9e7a4fd17f5c6a4f557e8c>> * @flow strict * @noformat */ @@ -33,6 +33,7 @@ export type ReactNativeFeatureFlagsJsOnly = $ReadOnly<{ animatedShouldSyncValueBeforeStartCallback: Getter, animatedShouldUseSingleOp: Getter, deferFlatListFocusChangeRenderUpdate: Getter, + enableImperativeEvents: Getter, enableNativeEventTargetEventDispatching: Getter, externalElementInspectionEnabled: Getter, fixVirtualizeListCollapseWindowSize: Getter, @@ -162,6 +163,11 @@ export const animatedShouldUseSingleOp: Getter = createJavaScriptFlagGe */ export const deferFlatListFocusChangeRenderUpdate: Getter = createJavaScriptFlagGetter('deferFlatListFocusChangeRenderUpdate', false); +/** + * When enabled, ReactNativeElement and ReadOnlyText expose the public EventTarget API (addEventListener, removeEventListener, dispatchEvent). When disabled, those methods are removed from those final classes. + */ +export const enableImperativeEvents: Getter = createJavaScriptFlagGetter('enableImperativeEvents', false); + /** * When enabled, the React Native renderer dispatches events through the W3C EventTarget API (addEventListener/dispatchEvent) instead of the legacy plugin-based system. */ diff --git a/packages/react-native/src/private/renderer/core/__tests__/EventTargetDispatching-itest.js b/packages/react-native/src/private/renderer/core/__tests__/EventTargetDispatching-itest.js index 10c73183b1fd..76de52d98711 100644 --- a/packages/react-native/src/private/renderer/core/__tests__/EventTargetDispatching-itest.js +++ b/packages/react-native/src/private/renderer/core/__tests__/EventTargetDispatching-itest.js @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. * * @fantom_flags enableNativeEventTargetEventDispatching:* + * @fantom_flags enableImperativeEvents:* * @flow strict-local * @format */ @@ -465,10 +466,13 @@ const {isOSS} = Fantom.getConstants(); }); // --- addEventListener / removeEventListener on refs --- - // These tests require EventTarget-based dispatching to be enabled, - // since addEventListener is only available when the flag is on. + // These tests require both `enableNativeEventTargetEventDispatching` and + // `enableImperativeEvents` to be enabled, since the public `addEventListener` + // API on element refs is only available when both flags are on. They are + // skipped for the other flag combinations. - (ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() + (ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() && + ReactNativeFeatureFlags.enableImperativeEvents() ? describe : describe.skip)('addEventListener / removeEventListener', () => { it('addEventListener on a ref receives dispatched events', () => { @@ -915,7 +919,8 @@ const {isOSS} = Fantom.getConstants(); }, ); - (ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() + (ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() && + ReactNativeFeatureFlags.enableImperativeEvents() ? it : it.skip)( 'direct (non-bubbling) events do not propagate via addEventListener', @@ -964,7 +969,8 @@ const {isOSS} = Fantom.getConstants(); }, ); - (ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() + (ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() && + ReactNativeFeatureFlags.enableImperativeEvents() ? describe : describe.skip)('bubbling to document element and document', () => { it('event bubbles from child up to the document element', () => { diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js index e1bc584eed4a..9fef664f4004 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js @@ -354,3 +354,24 @@ export const ReactNativeElement_public: typeof ReactNativeElement = // $FlowExpectedError[prop-missing] ReactNativeElement_public.prototype = ReactNativeElement.prototype; + +// The public imperative EventTarget API (`addEventListener`, +// `removeEventListener`, `dispatchEvent`) is only inherited by this final class +// when `enableNativeEventTargetEventDispatching` is enabled (which makes +// `ReadOnlyNode` extend `EventTarget`). Until that public API is finalized, it +// is gated behind `enableImperativeEvents`: when that flag is off we remove +// those methods from this final class. Native/internal event dispatch does not +// rely on these public methods, so removing them is safe. +if ( + ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() && + !ReactNativeFeatureFlags.enableImperativeEvents() +) { + const prototype: interface { + addEventListener?: unknown, + removeEventListener?: unknown, + dispatchEvent?: unknown, + } = ReactNativeElement.prototype; + prototype.addEventListener = undefined; + prototype.removeEventListener = undefined; + prototype.dispatchEvent = undefined; +} diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyText.js b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyText.js index 01dcbbae5955..278b227964f6 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyText.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyText.js @@ -10,6 +10,7 @@ // flowlint unsafe-getters-setters:off +import * as ReactNativeFeatureFlags from '../../../featureflags/ReactNativeFeatureFlags'; import ReadOnlyCharacterData from './ReadOnlyCharacterData'; import ReadOnlyNode from './ReadOnlyNode'; @@ -39,3 +40,24 @@ export const ReadOnlyText_public: typeof ReadOnlyText = // $FlowExpectedError[prop-missing] ReadOnlyText_public.prototype = ReadOnlyText.prototype; + +// The public imperative EventTarget API (`addEventListener`, +// `removeEventListener`, `dispatchEvent`) is only inherited by this final class +// when `enableNativeEventTargetEventDispatching` is enabled (which makes +// `ReadOnlyNode` extend `EventTarget`). Until that public API is finalized, it +// is gated behind `enableImperativeEvents`: when that flag is off we remove +// those methods from this final class. Native/internal event dispatch does not +// rely on these public methods, so removing them is safe. +if ( + ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() && + !ReactNativeFeatureFlags.enableImperativeEvents() +) { + const prototype: interface { + addEventListener?: unknown, + removeEventListener?: unknown, + dispatchEvent?: unknown, + } = ReadOnlyText.prototype; + prototype.addEventListener = undefined; + prototype.removeEventListener = undefined; + prototype.dispatchEvent = undefined; +} diff --git a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js index 6bd94995fda4..d0757fcdba75 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js +++ b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js @@ -5,6 +5,8 @@ * LICENSE file in the root directory of this source tree. * * @fantom_flags enableFabricCommitBranching:* + * @fantom_flags enableNativeEventTargetEventDispatching:true + * @fantom_flags enableImperativeEvents:* * @flow strict-local * @format */ @@ -23,6 +25,8 @@ import { NativeText, NativeVirtualText, } from 'react-native/Libraries/Text/TextNativeComponent'; +import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags'; +import Event from 'react-native/src/private/webapis/dom/events/Event'; import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement'; import ReadOnlyElement from 'react-native/src/private/webapis/dom/nodes/ReadOnlyElement'; import ReadOnlyNode from 'react-native/src/private/webapis/dom/nodes/ReadOnlyNode'; @@ -33,6 +37,20 @@ function ensureReactNativeElement(value: unknown): ReactNativeElement { return ensureInstance(value, ReactNativeElement); } +// The public imperative EventTarget API is not part of the static type of this +// final class (it is only present at runtime, gated by feature flags), so we +// cast to an interface with optional members to inspect/use it without Flow +// errors. Optional members make this a valid upcast and let us assert both +// presence (`'function'`) and absence (`'undefined'`). +type MaybeEventTarget = interface { + addEventListener?: (type: string, callback: (event: Event) => void) => void, + removeEventListener?: ( + type: string, + callback: (event: Event) => void, + ) => void, + dispatchEvent?: (event: Event) => boolean, +}; + /* eslint-disable no-bitwise */ describe('ReactNativeElement', () => { @@ -1632,6 +1650,108 @@ describe('ReactNativeElement', () => { }); }); + describe('imperative EventTarget API', () => { + // These tests run with `enableNativeEventTargetEventDispatching:true` and + // `enableImperativeEvents:*` (see the `@fantom_flags` pragmas). The public + // EventTarget API is gated behind `enableImperativeEvents`: when it is off + // the methods are removed from this final class, when it is on they are + // available. + const {isOSS} = Fantom.getConstants(); + + if (!ReactNativeFeatureFlags.enableImperativeEvents()) { + describe('when `enableImperativeEvents` is off (default)', () => { + it('removes the public EventTarget methods', () => { + const ref = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + const element = ensureReactNativeElement( + ref.current, + ) as MaybeEventTarget; + expect(typeof element.addEventListener).toBe('undefined'); + expect(typeof element.removeEventListener).toBe('undefined'); + expect(typeof element.dispatchEvent).toBe('undefined'); + }); + + // Removing the public API must not affect native/prop event delivery, + // which goes through the internal (symbol-keyed) dispatch path. + (isOSS ? it.skip : it)( + 'still delivers native events to prop handlers', + () => { + const ref = createRef(); + const onPointerUp = jest.fn(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(onPointerUp).toHaveBeenCalledTimes(0); + + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(onPointerUp).toHaveBeenCalledTimes(1); + }, + ); + }); + } + + if (ReactNativeFeatureFlags.enableImperativeEvents()) { + describe('when `enableImperativeEvents` is on', () => { + it('exposes the public EventTarget methods', () => { + const ref = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + const element = ensureReactNativeElement( + ref.current, + ) as MaybeEventTarget; + expect(typeof element.addEventListener).toBe('function'); + expect(typeof element.removeEventListener).toBe('function'); + expect(typeof element.dispatchEvent).toBe('function'); + }); + + it('round-trips a listener via `addEventListener` + `dispatchEvent`', () => { + const ref = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + const element = ensureReactNativeElement( + ref.current, + ) as MaybeEventTarget; + const listener = jest.fn(); + + element.addEventListener?.('custom', listener); + const result = element.dispatchEvent?.(new Event('custom')); + + expect(listener).toHaveBeenCalledTimes(1); + expect(result).toBe(true); + + element.removeEventListener?.('custom', listener); + element.dispatchEvent?.(new Event('custom')); + + expect(listener).toHaveBeenCalledTimes(1); + }); + }); + } + }); + describe('global constructors', () => { it('throws when constructing HTMLElement', () => { expect(() => new HTMLElement()).toThrow( diff --git a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReadOnlyText-itest.js b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReadOnlyText-itest.js index 8f7b258ad920..2e48cafc5511 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReadOnlyText-itest.js +++ b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReadOnlyText-itest.js @@ -4,6 +4,8 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @fantom_flags enableNativeEventTargetEventDispatching:true + * @fantom_flags enableImperativeEvents:* * @flow strict-local * @format */ @@ -18,6 +20,7 @@ import invariant from 'invariant'; import * as React from 'react'; import {createRef} from 'react'; import {NativeText} from 'react-native/Libraries/Text/TextNativeComponent'; +import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags'; import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement'; import ReadOnlyNode from 'react-native/src/private/webapis/dom/nodes/ReadOnlyNode'; import ReadOnlyText from 'react-native/src/private/webapis/dom/nodes/ReadOnlyText'; @@ -34,6 +37,17 @@ function ensureReactNativeElement(value: unknown): ReactNativeElement { return ensureInstance(value, ReactNativeElement); } +// The public imperative EventTarget API is not part of the static type of this +// final class (it is only present at runtime, gated by feature flags), so we +// cast to an interface with optional members to inspect it without Flow errors. +// Optional members make this a valid upcast and let us assert both presence +// (`'function'`) and absence (`'undefined'`). +type MaybeEventTarget = interface { + addEventListener?: unknown, + removeEventListener?: unknown, + dispatchEvent?: unknown, +}; + describe('ReadOnlyText', () => { it('should be used to create public text instances', () => { const parentNodeRef = createRef(); @@ -332,6 +346,55 @@ describe('ReadOnlyText', () => { }); }); + describe('imperative EventTarget API', () => { + // These tests run with `enableNativeEventTargetEventDispatching:true` and + // `enableImperativeEvents:*` (see the `@fantom_flags` pragmas). The public + // EventTarget API is gated behind `enableImperativeEvents`: when it is off + // the methods are removed from this final class, when it is on they are + // available. + if (!ReactNativeFeatureFlags.enableImperativeEvents()) { + it('removes the public EventTarget methods when `enableImperativeEvents` is off (default)', () => { + const parentNodeRef = createRef(); + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(Some text); + }); + + const parentNode = ensureReadOnlyNode(parentNodeRef.current); + const textNode = ensureReadOnlyText( + parentNode.childNodes[0], + ) as MaybeEventTarget; + + expect(typeof textNode.addEventListener).toBe('undefined'); + expect(typeof textNode.removeEventListener).toBe('undefined'); + expect(typeof textNode.dispatchEvent).toBe('undefined'); + }); + } + + if (ReactNativeFeatureFlags.enableImperativeEvents()) { + it('exposes the public EventTarget methods when `enableImperativeEvents` is on', () => { + const parentNodeRef = createRef(); + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(Some text); + }); + + const parentNode = ensureReadOnlyNode(parentNodeRef.current); + const textNode = ensureReadOnlyText( + parentNode.childNodes[0], + ) as MaybeEventTarget; + + expect(typeof textNode.addEventListener).toBe('function'); + expect(typeof textNode.removeEventListener).toBe('function'); + expect(typeof textNode.dispatchEvent).toBe('function'); + }); + } + }); + describe('global constructors', () => { it('throws when constructing Text', () => { expect(() => new Text()).toThrow( diff --git a/packages/rn-tester/package.json b/packages/rn-tester/package.json index 26b5b7bd97cc..631b750928e7 100644 --- a/packages/rn-tester/package.json +++ b/packages/rn-tester/package.json @@ -33,7 +33,7 @@ "nullthrows": "^1.1.1" }, "peerDependencies": { - "react": "19.2.3", + "react": "19.2.7", "react-native": "1000.0.0" }, "codegenConfig": { diff --git a/packages/virtualized-lists/package.json b/packages/virtualized-lists/package.json index d8c5de954063..5b25aaf81357 100644 --- a/packages/virtualized-lists/package.json +++ b/packages/virtualized-lists/package.json @@ -48,7 +48,7 @@ "nullthrows": "^1.1.1" }, "devDependencies": { - "react-test-renderer": "19.2.3" + "react-test-renderer": "19.2.7" }, "peerDependencies": { "@types/react": "^19.2.0", diff --git a/private/helloworld/package.json b/private/helloworld/package.json index 63cdc897f1ee..42f2864c68e5 100644 --- a/private/helloworld/package.json +++ b/private/helloworld/package.json @@ -12,7 +12,7 @@ "test": "jest" }, "dependencies": { - "react": "19.2.3", + "react": "19.2.7", "react-native": "1000.0.0" }, "devDependencies": { @@ -29,7 +29,7 @@ "eslint": "^8.19.0", "jest": "^29.7.0", "listr2": "^8.2.1", - "react-test-renderer": "19.2.3", + "react-test-renderer": "19.2.7", "rxjs": "^7.8.1" }, "engines": { diff --git a/yarn.lock b/yarn.lock index b2ce8ccc5a22..8d585205f09a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8022,28 +8022,28 @@ react-devtools-core@^6.1.5: shell-quote "^1.6.1" ws "^7" -react-is@19.2.3, react-is@^16.13.1, react-is@^18.0.0, react-is@^19.2.3: - version "19.2.3" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.3.tgz#eec2feb69c7fb31f77d0b5c08c10ae1c88886b29" - integrity sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA== +react-is@19.2.7, react-is@^16.13.1, react-is@^18.0.0, react-is@^19.2.7: + version "19.2.7" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.7.tgz#57668ee86a78574a542b0a539455212b2c086df2" + integrity sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A== react-refresh@^0.14.0: version "0.14.2" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA== -react-test-renderer@19.2.3: - version "19.2.3" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-19.2.3.tgz#d20f5193867c98b2df9e13b4e72bb83f311f6a43" - integrity sha512-TMR1LnSFiWZMJkCgNf5ATSvAheTT2NvKIwiVwdBPHxjBI7n/JbWd4gaZ16DVd9foAXdvDz+sB5yxZTwMjPRxpw== +react-test-renderer@19.2.7: + version "19.2.7" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-19.2.7.tgz#465cc2df3156ad997f94e93b12d6031d54828fde" + integrity sha512-U4TyPDJ9MsC8rFimXuJum8w40aPc9kbOZYO8Pc2/4A884i8hwJsMNA/JNyuOc/f2/37wHvk7HjpVl1V4re7Dig== dependencies: - react-is "^19.2.3" + react-is "^19.2.7" scheduler "^0.27.0" -react@19.2.3: - version "19.2.3" - resolved "https://registry.yarnpkg.com/react/-/react-19.2.3.tgz#d83e5e8e7a258cf6b4fe28640515f99b87cd19b8" - integrity sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA== +react@19.2.7: + version "19.2.7" + resolved "https://registry.yarnpkg.com/react/-/react-19.2.7.tgz#1f47a1bfc06f8ec885752c6f4af14369a9f8260b" + integrity sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ== read-pkg-up@^2.0.0: version "2.0.0"