From 944c9a88d36fbb780a1d26d6ed04f098bc9bcbda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 5 Jun 2026 10:22:28 -0700 Subject: [PATCH 1/3] Gate the public EventTarget API on ReactNativeElement and ReadOnlyText behind enableImperativeEvents (#57088) Summary: ## Context We are migrating React Native event dispatching to the W3C `EventTarget` interface. That refactor is backwards compatible, but turning it on also incidentally exposes a brand new public API on component refs (`addEventListener`, `removeEventListener`, `dispatchEvent`). We want to decouple the event-dispatching refactor from shipping that new public API so we can roll out the refactor faster, without committing to the public surface before it is finalized. ## Changes Adds a new JS-only feature flag, `enableImperativeEvents` (default off), that controls whether the final DOM node classes `ReactNativeElement` and `ReadOnlyText` expose the public `EventTarget` API. - When `EventTarget`-based dispatching is enabled but `enableImperativeEvents` is off, `addEventListener`, `removeEventListener` and `dispatchEvent` are removed from those two final classes. Native and internal event dispatch do not rely on these public methods, so event delivery is unaffected. - When `enableImperativeEvents` is on, the classes keep the full `EventTarget` API. - Adds integration test coverage for both flag states, and pins the flag on in the existing `EventTarget` dispatching tests that exercise imperative listeners. Changelog: [Internal] Reviewed By: javache Differential Revision: D107656477 --- .../ReactNativeFeatureFlags.config.js | 10 ++ .../featureflags/ReactNativeFeatureFlags.js | 8 +- .../__tests__/EventTargetDispatching-itest.js | 16 ++- .../webapis/dom/nodes/ReactNativeElement.js | 21 +++ .../private/webapis/dom/nodes/ReadOnlyText.js | 22 ++++ .../__tests__/ReactNativeElement-itest.js | 120 ++++++++++++++++++ .../dom/nodes/__tests__/ReadOnlyText-itest.js | 63 +++++++++ 7 files changed, 254 insertions(+), 6 deletions(-) diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 98dfa7b6eab..940f56d10e7 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 cf32422016e..fe98367b4bc 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 10c73183b1f..76de52d9871 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 e1bc584eed4..9fef664f400 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 01dcbbae595..278b227964f 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 6bd94995fda..d0757fcdba7 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 8f7b258ad92..2e48cafc551 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( From c409bfd09faf6f0f2c138fae016b07b29b01f236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 5 Jun 2026 10:22:28 -0700 Subject: [PATCH 2/3] Upgrade React and renderers from 19.2.3 to 19.2.7 (#57089) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: This sync includes the following changes: - **[6117d7cca4](https://github.com/facebook/react/commit/6117d7cca4 )**: Version 19.2.7 ([#36591](https://github.com/facebook/react/pull/36591)) //// - **[02c444c3f0](https://github.com/facebook/react/commit/02c444c3f0 )**: [19.2.x][FlightReply] Don't drop FormData entries in `decodeReplyFromBusboy` ([#36566](https://github.com/facebook/react/pull/36566)) //// - **[e1cfa7b6de](https://github.com/facebook/react/commit/e1cfa7b6de )**: [19.2.x] Enable CI ([#36552](https://github.com/facebook/react/pull/36552)) //// - **[eaf3e95ca9](https://github.com/facebook/react/commit/eaf3e95ca9 )**: Version 19.2.6 //// - **[795203e75e](https://github.com/facebook/react/commit/795203e75e )**: [FlightReply] Type hardening and performance improvements //// - **[23f4f9f30d](https://github.com/facebook/react/commit/23f4f9f30d )**: 19.2.5 //// - **[672b24221f](https://github.com/facebook/react/commit/672b24221f )**: [Flight] Avoid consuming cyclic models multiple times //// - **[90ab3f89f4](https://github.com/facebook/react/commit/90ab3f89f4 )**: Version 19.2.4 //// - **[63d61c7557](https://github.com/facebook/react/commit/63d61c7557 )**: Add more DoS mitigations to React Flight Reply, and harden React Flight //// - **[612e371fb2](https://github.com/facebook/react/commit/612e371fb2 )**: Version 19.2.3 //// - **[fc5c53c396](https://github.com/facebook/react/commit/fc5c53c396 )**: Add extra protection //// - **[b910fc15e3](https://github.com/facebook/react/commit/b910fc15e3 )**: Version 19.2.2 //// - **[dd0519822a](https://github.com/facebook/react/commit/dd0519822a )**: Patch Promise cycles and toString on Server Functions //// - **[a4d338a6c6](https://github.com/facebook/react/commit/a4d338a6c6 )**: [test] Cleanup stack assertions in tests mixing React Server and Client ([#35316](https://github.com/facebook/react/pull/35316)) //// - **[053df4e856](https://github.com/facebook/react/commit/053df4e856 )**: Version 19.2.1 //// - **[b180cb2b25](https://github.com/facebook/react/commit/b180cb2b25 )**: Bring ReactFlightClient fixes to FlightReplyServer //// - **[581fdc550a](https://github.com/facebook/react/commit/581fdc550a )**: [release] Allow building single release channel with processed versions ([#35270](https://github.com/facebook/react/pull/35270)) //// - **[f0dfee38f8](https://github.com/facebook/react/commit/f0dfee38f8 )**: [Flight] Avoid main-thread stalls from large debug strings ([#36570](https://github.com/facebook/react/pull/36570)) //// - **[c014813413](https://github.com/facebook/react/commit/c014813413 )**: [Flight] Fix stranded row content under Node stream backpressure ([#36516](https://github.com/facebook/react/pull/36516)) //// - **[c0cd4d5d30](https://github.com/facebook/react/commit/c0cd4d5d30 )**: Throw special error if rejected Promises are incorrectly instrumented ([#36328](https://github.com/facebook/react/pull/36328)) //// - **[cafd63bcf7](https://github.com/facebook/react/commit/cafd63bcf7 )**: Stop transpiling computed property names ([#36542](https://github.com/facebook/react/pull/36542)) //// - **[37fa36ced3](https://github.com/facebook/react/commit/37fa36ced3 )**: [Fizz] Fix crash when capturing the callsite of a stalled use() of a Flight chunk that was rejected in the meantime ([#36544](https://github.com/facebook/react/pull/36544)) //// - **[b91823e214](https://github.com/facebook/react/commit/b91823e214 )**: [FlightReply] Don't drop FormData entries in `decodeReplyFromBusboy` ([#36468](https://github.com/facebook/react/pull/36468)) //// - **[8fc5763b8a](https://github.com/facebook/react/commit/8fc5763b8a )**: fix[describeClassComponentFrame]: invoke constructor with new keyword ([#36455](https://github.com/facebook/react/pull/36455)) //// - **[d5736f098e](https://github.com/facebook/react/commit/d5736f098e )**: [Fiber] track stylesheet preloads when explicitly preloaded ([#36386](https://github.com/facebook/react/pull/36386)) //// - **[dd453071d9](https://github.com/facebook/react/commit/dd453071d9 )**: [FlightReply] Type hardening and performance improvements ([#36425](https://github.com/facebook/react/pull/36425)) //// - **[f4e0d4ed0c](https://github.com/facebook/react/commit/f4e0d4ed0c )**: [enableInfiniteRenderLoopDetection] Add a flag to force throwing ([#36357](https://github.com/facebook/react/pull/36357)) //// - **[ad5dfc82b7](https://github.com/facebook/react/commit/ad5dfc82b7 )**: Add react-flight-server-fb package for Meta's internal bundler ([#36309](https://github.com/facebook/react/pull/36309)) //// - **[142cfde89e](https://github.com/facebook/react/commit/142cfde89e )**: Fix FragmentInstance listener leak: normalize boolean vs object capture options per DOM spec ([#36047](https://github.com/facebook/react/pull/36047)) //// - **[94643c3b85](https://github.com/facebook/react/commit/94643c3b85 )**: Suggest correct casing for misspelled credentialless iframe attribute ([#36322](https://github.com/facebook/react/pull/36322)) //// - **[306a01b4e0](https://github.com/facebook/react/commit/306a01b4e0 )**: Add credentialless as a recognized boolean attribute for iframes ([#36148](https://github.com/facebook/react/pull/36148)) //// - **[3ee1fe4a8e](https://github.com/facebook/react/commit/3ee1fe4a8e )**: Fix contributor attribution for ESLint v10 support //// - **[1ddff43c41](https://github.com/facebook/react/commit/1ddff43c41 )**: Add null check before calling fabricSuspendOnActiveViewTransition ([#36310](https://github.com/facebook/react/pull/36310)) //// - **[d1727fbf98](https://github.com/facebook/react/commit/d1727fbf98 )**: [eprh] Update changelog for 7.1.1 ([#36308](https://github.com/facebook/react/pull/36308)) //// - **[bc249804d3](https://github.com/facebook/react/commit/bc249804d3 )**: [eprh] Add back a no-op for removed component-hook-factories rule ([#36307](https://github.com/facebook/react/pull/36307)) //// - **[da9325b519](https://github.com/facebook/react/commit/da9325b519 )**: [Fiber] Double invoke Effects in StrictMode after Fast Refresh ([#35962](https://github.com/facebook/react/pull/35962)) //// - **[67e47593b6](https://github.com/facebook/react/commit/67e47593b6 )**: [Fiber] Double invoke Effects in Strict Mode during Hydration ([#35961](https://github.com/facebook/react/pull/35961)) //// - **[23fcd7cea1](https://github.com/facebook/react/commit/23fcd7cea1 )**: Minify prod error messages for all browser bundles ([#36277](https://github.com/facebook/react/pull/36277)) //// - **[77319e2af0](https://github.com/facebook/react/commit/77319e2af0 )**: [eprh] Update changelog for 7.1.0 ([#36292](https://github.com/facebook/react/pull/36292)) //// - **[4b073f4887](https://github.com/facebook/react/commit/4b073f4887 )**: [Fizz] add additional task reentrancy protections ([#36291](https://github.com/facebook/react/pull/36291)) //// - **[f6fe4275c7](https://github.com/facebook/react/commit/f6fe4275c7 )**: Wire up createViewTransitionInstance and suspendOnActiveViewTransition in Fabric ([#36196](https://github.com/facebook/react/pull/36196)) //// - **[fe5160140d](https://github.com/facebook/react/commit/fe5160140d )**: Wire up startViewTransitionReadyFinished in Fabric ([#36246](https://github.com/facebook/react/pull/36246)) //// - **[ea6792026f](https://github.com/facebook/react/commit/ea6792026f )**: [Fizz] prevent reentrant finishedTask from calling completeAll multiple times ([#36287](https://github.com/facebook/react/pull/36287)) //// - **[56922cf751](https://github.com/facebook/react/commit/56922cf751 )**: [react-native-renderer] Delete Paper (legacy) renderer ([#36285](https://github.com/facebook/react/pull/36285)) //// - **[00f063c31d](https://github.com/facebook/react/commit/00f063c31d )**: [test] Make `enableSuspenseyImages` dynamic ([#36274](https://github.com/facebook/react/pull/36274)) //// - **[0418c8a8b6](https://github.com/facebook/react/commit/0418c8a8b6 )**: [RN] Move new event dispatching pipeline to RN ([#36266](https://github.com/facebook/react/pull/36266)) //// - **[568244232e](https://github.com/facebook/react/commit/568244232e )**: [react-native-renderer] EventTarget-based event dispatching ([#36253](https://github.com/facebook/react/pull/36253)) //// - **[fef12a01c8](https://github.com/facebook/react/commit/fef12a01c8 )**: fix: explicitly warn for infinite loops discovered only via enableInfiniteRenderLoopDetection ([#36195](https://github.com/facebook/react/pull/36195)) //// - **[705268dcd1](https://github.com/facebook/react/commit/705268dcd1 )**: Fix require('ReactFeatureFlags') in eslint-plugin-react-hooks www build ([#36243](https://github.com/facebook/react/pull/36243)) //// - **[733d3aaf99](https://github.com/facebook/react/commit/733d3aaf99 )**: Fix FB_WWW eprh bundle dev guard ([#36238](https://github.com/facebook/react/pull/36238)) //// - **[404b38c764](https://github.com/facebook/react/commit/404b38c764 )**: [Flight] Add more cycle protections ([#36236](https://github.com/facebook/react/pull/36236)) //// - **[74568e8627](https://github.com/facebook/react/commit/74568e8627 )**: [Flight] Transport `AggregateErrors.errors` ([#36156](https://github.com/facebook/react/pull/36156)) //// - **[9627b5a1ca](https://github.com/facebook/react/commit/9627b5a1ca )**: [Fiber] Fix context propagation into Suspense fallbacks ([#36160](https://github.com/facebook/react/pull/36160)) //// - **[f944b4c535](https://github.com/facebook/react/commit/f944b4c535 )**: Fix typos in comments ([#35701](https://github.com/facebook/react/pull/35701)) //// - **[677818e4a2](https://github.com/facebook/react/commit/677818e4a2 )**: Fix typos in tests and comments ([#35627](https://github.com/facebook/react/pull/35627)) //// - **[3cb2c42013](https://github.com/facebook/react/commit/3cb2c42013 )**: Add ReactFeatureFlags support to eprh ([#35951](https://github.com/facebook/react/pull/35951)) //// - **[c0d218f0f3](https://github.com/facebook/react/commit/c0d218f0f3 )**: Fix useDeferredValue getting stuck ([#36134](https://github.com/facebook/react/pull/36134)) //// - **[6a04c369f1](https://github.com/facebook/react/commit/6a04c369f1 )**: Enables Basic View Transition support for React Native Fabric renderer ([#35764](https://github.com/facebook/react/pull/35764)) //// - **[d594643e5e](https://github.com/facebook/react/commit/d594643e5e )**: Turn on enableViewTransition for RN FB build ([#36106](https://github.com/facebook/react/pull/36106)) //// - **[b4546cd0d4](https://github.com/facebook/react/commit/b4546cd0d4 )**: [enableInfiniteRenderLoopDetection] Warn about potential infinite loop, instead of interrupting ([#35999](https://github.com/facebook/react/pull/35999)) //// - **[3f0b9e61c4](https://github.com/facebook/react/commit/3f0b9e61c4 )**: Update CSS shorthand property list ([#35636](https://github.com/facebook/react/pull/35636)) //// - **[12ba7d8129](https://github.com/facebook/react/commit/12ba7d8129 )**: [Flight Reply] Early bailout if backing entry for Blob deserialization is not a Blob ([#36055](https://github.com/facebook/react/pull/36055)) //// - **[c80a075095](https://github.com/facebook/react/commit/c80a075095 )**: Fix focus set for delegated and already focused elements ([#36010](https://github.com/facebook/react/pull/36010)) //// - **[5e9eedb578](https://github.com/facebook/react/commit/5e9eedb578 )**: [Flight] Clear chunk reason after successful module initialization ([#36024](https://github.com/facebook/react/pull/36024)) //// - **[1e3152365d](https://github.com/facebook/react/commit/1e3152365d )**: Enable Fragment Ref flags across builds ([#36026](https://github.com/facebook/react/pull/36026)) //// - **[014138df87](https://github.com/facebook/react/commit/014138df87 )**: [DevTools] Fix a crash when rendering a new class Component when simulating errored state ([#35985](https://github.com/facebook/react/pull/35985)) //// - **[93882bd40e](https://github.com/facebook/react/commit/93882bd40e )**: [errors] s/form state/action state ([#35790](https://github.com/facebook/react/pull/35790)) //// - **[3bc2d41428](https://github.com/facebook/react/commit/3bc2d41428 )**: [noop] Fix `createContainer` argument order in the Fiber implementation ([#35945](https://github.com/facebook/react/pull/35945)) //// - **[5e4279134d](https://github.com/facebook/react/commit/5e4279134d )**: [noop] Typecheck `react-noop-renderer` against host config and renderer API ([#35944](https://github.com/facebook/react/pull/35944)) //// - **[ee4699f5a1](https://github.com/facebook/react/commit/ee4699f5a1 )**: [noop] Fail tests on unasserted recoverable errors ([#35948](https://github.com/facebook/react/pull/35948)) //// - **[9c0323e2cf](https://github.com/facebook/react/commit/9c0323e2cf )**: Stabilize reactFragments host node handle ([#35642](https://github.com/facebook/react/pull/35642)) //// - **[e6f1c33acf](https://github.com/facebook/react/commit/e6f1c33acf )**: [Fiber] Don't warn when rendering data block scripts ([#35953](https://github.com/facebook/react/pull/35953)) //// - **[4cc5b7a90b](https://github.com/facebook/react/commit/4cc5b7a90b )**: Add support for event information in React scheduler tracks in React Native ([#35947](https://github.com/facebook/react/pull/35947)) //// - **[e0cc7202e1](https://github.com/facebook/react/commit/e0cc7202e1 )**: [flags] Clean up `enableHiddenSubtreeInsertionEffectCleanup` ([#35918](https://github.com/facebook/react/pull/35918)) //// - **[843d69f077](https://github.com/facebook/react/commit/843d69f077 )**: [react-dom] Support `maskType` SVG prop ([#35921](https://github.com/facebook/react/pull/35921)) //// - **[b4a8d29845](https://github.com/facebook/react/commit/b4a8d29845 )**: fix: remove unused variable to fix linter ([#35919](https://github.com/facebook/react/pull/35919)) //// - **[98ce535fdb](https://github.com/facebook/react/commit/98ce535fdb )**: [RN] Expose event as a global variable during dispatch ([#35913](https://github.com/facebook/react/pull/35913)) //// - **[a48e9e3f10](https://github.com/facebook/react/commit/a48e9e3f10 )**: [RN] Fix timeStamp property of SyntheticEvent in React Native ([#35912](https://github.com/facebook/react/pull/35912)) //// - **[074d96b9dd](https://github.com/facebook/react/commit/074d96b9dd )**: [flags] land `enableTrustedTypesIntegration` ([#35816](https://github.com/facebook/react/pull/35816)) //// - **[ab18f33d46](https://github.com/facebook/react/commit/ab18f33d46 )**: Fix context propagation through suspended Suspense boundaries ([#35839](https://github.com/facebook/react/pull/35839)) //// - **[2ba3065527](https://github.com/facebook/react/commit/2ba3065527 )**: [Flight] Add support for transporting `Error.cause` ([#35810](https://github.com/facebook/react/pull/35810)) //// - **[38cd020c1f](https://github.com/facebook/react/commit/38cd020c1f )**: Don't outline Suspense boundaries with suspensey CSS during shell flush ([#35824](https://github.com/facebook/react/pull/35824)) //// - **[f247ebaf44](https://github.com/facebook/react/commit/f247ebaf44 )**: [Flight] Walk parsed JSON instead of using reviver for parsing RSC payload ([#35776](https://github.com/facebook/react/pull/35776)) //// - **[4842fbea02](https://github.com/facebook/react/commit/4842fbea02 )**: [react-dom] Add support for `onFullscreenChange` and `onFullscreenError` events ([#34621](https://github.com/facebook/react/pull/34621)) //// - **[61db53c179](https://github.com/facebook/react/commit/61db53c179 )**: [Native] Add RCTSelectableText as a recognized Text component ([#35780](https://github.com/facebook/react/pull/35780)) //// - **[47d1ad1454](https://github.com/facebook/react/commit/47d1ad1454 )**: [Flight] Skip `transferReferencedDebugInfo` during debug info resolution ([#35795](https://github.com/facebook/react/pull/35795)) //// - **[e8c6362678](https://github.com/facebook/react/commit/e8c6362678 )**: [eslint-plugin-react-hooks] Add ESLint v10 support ([#35720](https://github.com/facebook/react/pull/35720)) //// - **[892c68605c](https://github.com/facebook/react/commit/892c68605c )**: [fiber] bugfix - don't show in error message. ([#35763](https://github.com/facebook/react/pull/35763)) //// - **[cd515d7e22](https://github.com/facebook/react/commit/cd515d7e22 )**: Minor DOM FragmentInstance refactors ([#35641](https://github.com/facebook/react/pull/35641)) //// - **[78f5c504b7](https://github.com/facebook/react/commit/78f5c504b7 )**: Notify FragmentInstance of added/removed text ([#35637](https://github.com/facebook/react/pull/35637)) //// - **[eab523e2a9](https://github.com/facebook/react/commit/eab523e2a9 )**: [Fiber] Avoid duplicate debug info for array children ([#35733](https://github.com/facebook/react/pull/35733)) //// - **[272441a9ad](https://github.com/facebook/react/commit/272441a9ad )**: [Flight] Add `unstable_allowPartialStream` option to Flight Client ([#35731](https://github.com/facebook/react/pull/35731)) //// - **[b07aa7d643](https://github.com/facebook/react/commit/b07aa7d643 )**: [Flight] Fix `encodeReply` for JSX with temporary references ([#35730](https://github.com/facebook/react/pull/35730)) //// - **[2dd9b7cf76](https://github.com/facebook/react/commit/2dd9b7cf76 )**: [Flight] Fix debug channel flag in Node.js server renderToPipeableStream ([#35724](https://github.com/facebook/react/pull/35724)) //// - **[9a5996a6c1](https://github.com/facebook/react/commit/9a5996a6c1 )**: [flags] Cleanup `enableHalt` ([#35708](https://github.com/facebook/react/pull/35708)) //// - **[95ffd6cd9c](https://github.com/facebook/react/commit/95ffd6cd9c )**: Disable parallel transitions in canary ([#35709](https://github.com/facebook/react/pull/35709)) //// - **[3aaab92a26](https://github.com/facebook/react/commit/3aaab92a26 )**: [flags] add enableEffectEventMutationPhase ([#35548](https://github.com/facebook/react/pull/35548)) //// - **[087a34696f](https://github.com/facebook/react/commit/087a34696f )**: [test] add activity test with gSBU and enableViewTransition bugfix ([#35555](https://github.com/facebook/react/pull/35555)) //// - **[6913ea4d28](https://github.com/facebook/react/commit/6913ea4d28 )**: [flags] Add `enableParallelTransitions` ([#35392](https://github.com/facebook/react/pull/35392)) //// - **[cf993fb457](https://github.com/facebook/react/commit/cf993fb457 )**: [Flight] Fix stack overflow in `visitAsyncNode` with deep async chains ([#35612](https://github.com/facebook/react/pull/35612)) //// - **[c137dd6f54](https://github.com/facebook/react/commit/c137dd6f54 )**: Fix exhaustive deps bug with flow type casting. ([#35691](https://github.com/facebook/react/pull/35691)) //// - **[f84ce5a45c](https://github.com/facebook/react/commit/f84ce5a45c )**: [flags] Turn on `enableViewTransition` in OSS `react-test-renderer` ([#35684](https://github.com/facebook/react/pull/35684)) //// - **[6853d7ab2f](https://github.com/facebook/react/commit/6853d7ab2f )**: [Perf Tracks] Prevent crash when accessing `$$typeof` ([#35679](https://github.com/facebook/react/pull/35679)) //// - **[e32c126121](https://github.com/facebook/react/commit/e32c126121 )**: [flags] Turn on `enableAsyncDebugInfo` everywhere ([#35683](https://github.com/facebook/react/pull/35683)) //// - **[3e00319b35](https://github.com/facebook/react/commit/3e00319b35 )**: [Flight] allow context providers from client modules ([#35675](https://github.com/facebook/react/pull/35675)) //// - **[3419420e8b](https://github.com/facebook/react/commit/3419420e8b )**: [flags] Cleanup `enableActivity` ([#35681](https://github.com/facebook/react/pull/35681)) //// - **[b1533b034e](https://github.com/facebook/react/commit/b1533b034e )**: [Flight] Allow overriding `request.timeOrigin` via `options.startTime` ([#35598](https://github.com/facebook/react/pull/35598)) //// - **[7b023d7073](https://github.com/facebook/react/commit/7b023d7073 )**: [react-dom] Include `submitter` in `submit` events ([#35590](https://github.com/facebook/react/pull/35590)) //// - **[dcab44d757](https://github.com/facebook/react/commit/dcab44d757 )**: [react-dom] Fire onReset when automatically resetting forms ([#35176](https://github.com/facebook/react/pull/35176)) //// - **[ed4bd540ca](https://github.com/facebook/react/commit/ed4bd540ca )**: [Flight] Warn once if `eval` is disabled in dev environment ([#35661](https://github.com/facebook/react/pull/35661)) //// - **[da64117876](https://github.com/facebook/react/commit/da64117876 )**: [Perf Tracks] Handle function names that aren't strings ([#35659](https://github.com/facebook/react/pull/35659)) //// - **[230772f99d](https://github.com/facebook/react/commit/230772f99d )**: [tests] Fix ReactDOMAttribute-test ([#35654](https://github.com/facebook/react/pull/35654)) //// - **[875b06489f](https://github.com/facebook/react/commit/875b06489f )**: Add text node support to FragmentInstance operations ([#35630](https://github.com/facebook/react/pull/35630)) //// - **[d4d099f05b](https://github.com/facebook/react/commit/d4d099f05b )**: [flags] make `enableTrustedTypesIntegration` dynamic ([#35646](https://github.com/facebook/react/pull/35646)) //// - **[c0c37063e2](https://github.com/facebook/react/commit/c0c37063e2 )**: [Flight] Restore original function name in dev, server callstacks served with `unsafe-eval` ([#35650](https://github.com/facebook/react/pull/35650)) //// - **[87ae75b33f](https://github.com/facebook/react/commit/87ae75b33f )**: [Perf Tracks] Use minus (`-`) instead of en dash for removed props ([#35649](https://github.com/facebook/react/pull/35649)) //// - **[ff191f24b5](https://github.com/facebook/react/commit/ff191f24b5 )**: [Perf Tracks] Handle arrays with bigints in deep objects ([#35648](https://github.com/facebook/react/pull/35648)) //// - **[e66ef6480e](https://github.com/facebook/react/commit/e66ef6480e )**: [tests] remove withoutStack from assertConsole helpers ([#35498](https://github.com/facebook/react/pull/35498)) //// - **[8c34556ca8](https://github.com/facebook/react/commit/8c34556ca8 )**: [Flight] Fix react-markup types for server references ([#35634](https://github.com/facebook/react/pull/35634)) //// - **[10680271fa](https://github.com/facebook/react/commit/10680271fa )**: [Flight] Add more DoS mitigations to Flight Reply, and harden Flight ([#35632](https://github.com/facebook/react/pull/35632)) //// - **[699abc89ce](https://github.com/facebook/react/commit/699abc89ce )**: [flags] make enableComponentPerformanceTrack static everywhere ([#35629](https://github.com/facebook/react/pull/35629)) //// - **[94913cbffe](https://github.com/facebook/react/commit/94913cbffe )**: [flags] cleanup renameElementSymbol ([#35600](https://github.com/facebook/react/pull/35600)) //// - **[2d8e7f1ce3](https://github.com/facebook/react/commit/2d8e7f1ce3 )**: [flags] Remove enableHydrationLaneScheduling ([#35549](https://github.com/facebook/react/pull/35549)) //// - **[03ee29da2f](https://github.com/facebook/react/commit/03ee29da2f )**: [eslint-plugin-react-hooks] Skip compilation for non-React files ([#35589](https://github.com/facebook/react/pull/35589)) //// - **[b546603bcb](https://github.com/facebook/react/commit/b546603bcb )**: [Fiber] getNearestMountedFiber should consider fibers with alternates as mounted ([#35578](https://github.com/facebook/react/pull/35578)) //// - **[d29087523a](https://github.com/facebook/react/commit/d29087523a )**: Cancel animation when a custom Timeline is used ([#35567](https://github.com/facebook/react/pull/35567)) //// - **[d343c39cce](https://github.com/facebook/react/commit/d343c39cce )**: Remove Gesture warning when cloning the root ([#35566](https://github.com/facebook/react/pull/35566)) //// - **[1ecd99c774](https://github.com/facebook/react/commit/1ecd99c774 )**: Temporarily Mount useInsertionEffect while a Gesture snapshot is being computed ([#35565](https://github.com/facebook/react/pull/35565)) //// - **[c55ffb5ca3](https://github.com/facebook/react/commit/c55ffb5ca3 )**: Add Clean Up Callbacks to View Transition and Gesture Transition Events ([#35564](https://github.com/facebook/react/pull/35564)) //// - **[a49952b303](https://github.com/facebook/react/commit/a49952b303 )**: Properly clean up gesture Animations ([#35559](https://github.com/facebook/react/pull/35559)) //// - **[4bcf67e746](https://github.com/facebook/react/commit/4bcf67e746 )**: Support onGestureEnter/Exit/Share/Update events ([#35556](https://github.com/facebook/react/pull/35556)) //// - **[41b3e9a670](https://github.com/facebook/react/commit/41b3e9a670 )**: [Fizz] Push a stalled use() to the ownerStack/debugTask ([#35226](https://github.com/facebook/react/pull/35226)) //// - **[195fd2286b](https://github.com/facebook/react/commit/195fd2286b )**: [tests] Fix flaky flight tests ([#35513](https://github.com/facebook/react/pull/35513)) //// - **[be3fb29904](https://github.com/facebook/react/commit/be3fb29904 )**: [internal] revert change merged accidentally ([#35546](https://github.com/facebook/react/pull/35546)) //// - **[23e5edd05c](https://github.com/facebook/react/commit/23e5edd05c )**: [flags] clean up enableUseEffectEventHook ([#35541](https://github.com/facebook/react/pull/35541)) //// - **[3926e2438f](https://github.com/facebook/react/commit/3926e2438f )**: Fix ViewTransition null stateNode with SuspenseList ([#35520](https://github.com/facebook/react/pull/35520)) //// - **[6baff7ac76](https://github.com/facebook/react/commit/6baff7ac76 )**: [Flight] Allow cyclic references to be serialized when unwrapping lazy elements ([#35471](https://github.com/facebook/react/pull/35471)) //// - **[db71391c5c](https://github.com/facebook/react/commit/db71391c5c )**: [Fiber] Instrument the lazy initializer thenable in all cases ([#35521](https://github.com/facebook/react/pull/35521)) //// - **[4cf906380d](https://github.com/facebook/react/commit/4cf906380d )**: Optimize gesture by allowing the original work in progress tree to be a suspended commit ([#35510](https://github.com/facebook/react/pull/35510)) //// - **[eac3c95537](https://github.com/facebook/react/commit/eac3c95537 )**: Defer useDeferredValue updates in Gestures ([#35511](https://github.com/facebook/react/pull/35511)) //// - **[35a81cecf7](https://github.com/facebook/react/commit/35a81cecf7 )**: Entangle Gesture revert commit with the corresponding Action commit ([#35487](https://github.com/facebook/react/pull/35487)) //// - **[4028aaa50c](https://github.com/facebook/react/commit/4028aaa50c )**: Commit the Gesture lane if a gesture ends closer to the target state ([#35486](https://github.com/facebook/react/pull/35486)) //// - **[f0fbb0d199](https://github.com/facebook/react/commit/f0fbb0d199 )**: [Fiber] fix useId tracking on replay ([#35518](https://github.com/facebook/react/pull/35518)) //// - **[53daaf5aba](https://github.com/facebook/react/commit/53daaf5aba )**: Improve the detection of changed hooks ([#35123](https://github.com/facebook/react/pull/35123)) //// - **[4a3d993e52](https://github.com/facebook/react/commit/4a3d993e52 )**: Add the suffix to cancelled view transition names ([#35485](https://github.com/facebook/react/pull/35485)) //// - **[3e1abcc8d7](https://github.com/facebook/react/commit/3e1abcc8d7 )**: [tests] Require exact error messages in assertConsole helpers ([#35497](https://github.com/facebook/react/pull/35497)) //// - **[c18662405c](https://github.com/facebook/react/commit/c18662405c )**: [Fiber] Correctly handle replaying when hydrating ([#35494](https://github.com/facebook/react/pull/35494)) //// - **[65eec428c4](https://github.com/facebook/react/commit/65eec428c4 )**: Use `FormData` `submitter` parameter ([#29028](https://github.com/facebook/react/pull/29028)) //// - **[454fc41fc7](https://github.com/facebook/react/commit/454fc41fc7 )**: [test] Add tests for cyclic arrays in Flight and Flight Reply ([#35347](https://github.com/facebook/react/pull/35347)) //// - **[f93b9fd44b](https://github.com/facebook/react/commit/f93b9fd44b )**: Skip hydration errors when a view transition has been applied ([#35380](https://github.com/facebook/react/pull/35380)) //// - **[b731fe28cc](https://github.com/facebook/react/commit/b731fe28cc )**: Improve cyclic thenable detection in ReactFlightReplyServer ([#35369](https://github.com/facebook/react/pull/35369)) //// - **[88ee1f5955](https://github.com/facebook/react/commit/88ee1f5955 )**: Add reporting modes for react-hooks/exhaustive-effect-dependencies and temporarily enable ([#35365](https://github.com/facebook/react/pull/35365)) //// - **[ba5b843692](https://github.com/facebook/react/commit/ba5b843692 )**: [test] Exclude repository root from assertions ([#35361](https://github.com/facebook/react/pull/35361)) //// - **[b45bb335db](https://github.com/facebook/react/commit/b45bb335db )**: [Flight] Add extra loop protection ([#35351](https://github.com/facebook/react/pull/35351)) //// - **[894bc73cb4](https://github.com/facebook/react/commit/894bc73cb4 )**: [Flight] Patch Promise cycles and toString on Server Functions ([#35345](https://github.com/facebook/react/pull/35345)) //// - **[eade0d0fb7](https://github.com/facebook/react/commit/eade0d0fb7 )**: Attach instance handle to DOM in DEV for enableInternalInstanceMap ([#35341](https://github.com/facebook/react/pull/35341)) //// - **[734f1bf1ac](https://github.com/facebook/react/commit/734f1bf1ac )**: [eprh] Enable enableUseKeyedState and enableVerboseNoSetStateInEffect ([#35338](https://github.com/facebook/react/pull/35338)) //// - **[61331f3c9e](https://github.com/facebook/react/commit/61331f3c9e )**: Fix ViewTransition crash in Mobile Safari ([#35337](https://github.com/facebook/react/pull/35337)) //// - **[380778d296](https://github.com/facebook/react/commit/380778d296 )**: [test] Cleanup stack assertions in tests mixing React Server and Client ([#35316](https://github.com/facebook/react/pull/35316)) //// - **[378973b387](https://github.com/facebook/react/commit/378973b387 )**: [Flight] Move `react-server-dom-webpack/*.unbundled` to private `react-server-dom-unbundled` ([#35290](https://github.com/facebook/react/pull/35290)) //// - **[3016ff87d8](https://github.com/facebook/react/commit/3016ff87d8 )**: [Flight] Never parse "then" functions ([#35289](https://github.com/facebook/react/pull/35289)) //// - **[66ae640b36](https://github.com/facebook/react/commit/66ae640b36 )**: [eprh] fix react-compiler rules missing `meta.docs.url` property ([#35258](https://github.com/facebook/react/pull/35258)) //// - **[7dc903cd29](https://github.com/facebook/react/commit/7dc903cd29 )**: Patch FlightReplyServer with fixes from ReactFlightClient ([#35277](https://github.com/facebook/react/pull/35277)) //// - **[36df5e8b42](https://github.com/facebook/react/commit/36df5e8b42 )**: [release] Allow building single release channel with processed versions ([#35270](https://github.com/facebook/react/pull/35270)) //// - **[50e7ec8a69](https://github.com/facebook/react/commit/50e7ec8a69 )**: [compiler] Deprecate noEmit, add outputMode ([#35112](https://github.com/facebook/react/pull/35112)) //// - **[8ac5f4eb36](https://github.com/facebook/react/commit/8ac5f4eb36 )**: Fix form status reset when component state is updated ([#34075](https://github.com/facebook/react/pull/34075)) //// - **[eb89912ee5](https://github.com/facebook/react/commit/eb89912ee5 )**: Add expertimental `optimisticKey` behind a flag ([#35162](https://github.com/facebook/react/pull/35162)) //// - **[7df96b0c1a](https://github.com/facebook/react/commit/7df96b0c1a )**: [Flight] Complete list of Node.js' internal Promise awaits ([#35161](https://github.com/facebook/react/pull/35161)) //// - **[45bc3c9f04](https://github.com/facebook/react/commit/45bc3c9f04 )**: [Flight] Reduce risk of maximum call stack exceeded when emitting async sequence ([#35159](https://github.com/facebook/react/pull/35159)) //// - **[fb2177c153](https://github.com/facebook/react/commit/fb2177c153 )**: [Flight] Fix pending chunks count for streams & async iterables in DEV ([#35143](https://github.com/facebook/react/pull/35143)) //// - **[93fc57400b](https://github.com/facebook/react/commit/93fc57400b )**: [Flight] Fix broken byte stream parsing caused by buffer detachment ([#35127](https://github.com/facebook/react/pull/35127)) //// - **[093b3246e1](https://github.com/facebook/react/commit/093b3246e1 )**: [react-dom] Batch updates from `resize` until next frame ([#35117](https://github.com/facebook/react/pull/35117)) //// - **[bbe3f4d322](https://github.com/facebook/react/commit/bbe3f4d322 )**: [flags] disableLegacyMode in native-fb ([#35120](https://github.com/facebook/react/pull/35120)) //// - **[be48396dbd](https://github.com/facebook/react/commit/be48396dbd )**: Remove Dead Code in WWW JS //// - **[5268492536](https://github.com/facebook/react/commit/5268492536 )**: Fix: Activity should hide portal contents ([#35091](https://github.com/facebook/react/pull/35091)) //// - **[c83be7da9f](https://github.com/facebook/react/commit/c83be7da9f )**: [Fizz] Simplify createSuspenseBoundary path ([#35087](https://github.com/facebook/react/pull/35087)) //// - **[717e70843e](https://github.com/facebook/react/commit/717e70843e )**: Fix: Errors should not escape a hidden Activity ([#35074](https://github.com/facebook/react/pull/35074)) //// - **[fa50caf5f8](https://github.com/facebook/react/commit/fa50caf5f8 )**: [Fizz] Unify preamble only fields to save a field ([#35068](https://github.com/facebook/react/pull/35068)) //// - **[1e986f514f](https://github.com/facebook/react/commit/1e986f514f )**: [Fizz] Unify prerender only fields to save a field ([#35067](https://github.com/facebook/react/pull/35067)) //// - **[38bdda1ca6](https://github.com/facebook/react/commit/38bdda1ca6 )**: Don't skip content in visible offscreen trees for Gesture View Transitions ([#35066](https://github.com/facebook/react/pull/35066)) //// - **[a44e750e87](https://github.com/facebook/react/commit/a44e750e87 )**: Store instance handles in an internal map behind flag ([#35053](https://github.com/facebook/react/pull/35053)) //// - **[37b089a59c](https://github.com/facebook/react/commit/37b089a59c )**: Don't skip content in visible offscreen trees for View Transitions ([#35063](https://github.com/facebook/react/pull/35063)) //// - **[1a31a814f1](https://github.com/facebook/react/commit/1a31a814f1 )**: Escape View Transition Name Strings as base64 ([#35060](https://github.com/facebook/react/pull/35060)) //// - **[fa767dade6](https://github.com/facebook/react/commit/fa767dade6 )**: Remove unstable_expectedLoadTime option ([#35051](https://github.com/facebook/react/pull/35051)) //// - **[0ba2f01f74](https://github.com/facebook/react/commit/0ba2f01f74 )**: Rename to and implement in SSR ([#35022](https://github.com/facebook/react/pull/35022)) //// - **[dd048c3b2d](https://github.com/facebook/react/commit/dd048c3b2d )**: Clean up enablePostpone Experiment ([#35048](https://github.com/facebook/react/pull/35048)) //// - **[c308cb5905](https://github.com/facebook/react/commit/c308cb5905 )**: Disable enablePostpone flag in experimental ([#31042](https://github.com/facebook/react/pull/31042)) //// - **[986323f8c6](https://github.com/facebook/react/commit/986323f8c6 )**: [Fiber] SuspenseList with "hidden" tail row should "catch" suspense ([#35042](https://github.com/facebook/react/pull/35042)) //// - **[8f8b336734](https://github.com/facebook/react/commit/8f8b336734 )**: [eslint] Fix useEffectEvent checks in component syntax ([#35041](https://github.com/facebook/react/pull/35041)) //// - **[d000261eef](https://github.com/facebook/react/commit/d000261eef )**: [Tracks] Annotate devtools.performanceIssue for Cascading Updates in DEV ([#34961](https://github.com/facebook/react/pull/34961)) //// - **[f646e8ffd8](https://github.com/facebook/react/commit/f646e8ffd8 )**: [Flight] Fix `hasReadable` flag in Node.js clients' debug channel ([#35039](https://github.com/facebook/react/pull/35039)) //// - **[edd05f181b](https://github.com/facebook/react/commit/edd05f181b )**: Add fragment handles to children of FragmentInstances ([#34935](https://github.com/facebook/react/pull/34935)) //// - **[67f7d47a9b](https://github.com/facebook/react/commit/67f7d47a9b )**: [Flight] Fix debug info filtering to include later resolved I/O ([#35036](https://github.com/facebook/react/pull/35036)) //// - **[561ee24d4a](https://github.com/facebook/react/commit/561ee24d4a )**: [Fizz] Push halted await to the owner stack for late-arriving I/O info ([#35019](https://github.com/facebook/react/pull/35019)) //// - **[488d88b018](https://github.com/facebook/react/commit/488d88b018 )**: Render children passed to "backwards" SuspenseList in reverse mount order ([#35021](https://github.com/facebook/react/pull/35021)) //// - **[26cf280480](https://github.com/facebook/react/commit/26cf280480 )**: Switch the default revealOrder to "forwards" and tail "hidden" on SuspenseList ([#35018](https://github.com/facebook/react/pull/35018)) //// - **[4f93170066](https://github.com/facebook/react/commit/4f93170066 )**: [Flight] Cache the value if we visit the same I/O or Promise multiple times along different paths ([#35005](https://github.com/facebook/react/pull/35005)) //// - **[0fa32506da](https://github.com/facebook/react/commit/0fa32506da )**: [Flight] Clone subsequent I/O nodes if it's resolved more than once ([#35003](https://github.com/facebook/react/pull/35003)) //// - **[fb0d96073c](https://github.com/facebook/react/commit/fb0d96073c )**: [tests] disableLegacyMode in test-renderer ([#35002](https://github.com/facebook/react/pull/35002)) //// - **[b4455a6ee6](https://github.com/facebook/react/commit/b4455a6ee6 )**: [react-dom] Include all Node.js APIs in Bun entrypoint for `/server` ([#34193](https://github.com/facebook/react/pull/34193)) //// - **[dd53a946ec](https://github.com/facebook/react/commit/dd53a946ec )**: [rn] enabled disableLegacyMode everywhere ([#34947](https://github.com/facebook/react/pull/34947)) //// - **[90817f8810](https://github.com/facebook/react/commit/90817f8810 )**: [rn] delete the legacy renderers from the sync ([#34946](https://github.com/facebook/react/pull/34946)) //// - **[0d721b60c2](https://github.com/facebook/react/commit/0d721b60c2 )**: [Flight] Don't hang after resolving cyclic references ([#34988](https://github.com/facebook/react/pull/34988)) //// - **[e0654becf7](https://github.com/facebook/react/commit/e0654becf7 )**: [eprh] Update changelog for 7.0.1 ([#34964](https://github.com/facebook/react/pull/34964)) //// - **[eb2f784e75](https://github.com/facebook/react/commit/eb2f784e75 )**: Add hint for Node.js cjs-module-lexer for eslint-plugin-react-hook types ([#34953](https://github.com/facebook/react/pull/34953)) //// - **[723b25c644](https://github.com/facebook/react/commit/723b25c644 )**: Add hint for Node.js cjs-module-lexer for eslint-plugin-react-hook types ([#34951](https://github.com/facebook/react/pull/34951)) //// - **[bbb7a1fdf7](https://github.com/facebook/react/commit/bbb7a1fdf7 )**: [eprh] Type `configs.flat` more strictly ([#34950](https://github.com/facebook/react/pull/34950)) //// - **[6b344c7c53](https://github.com/facebook/react/commit/6b344c7c53 )**: Switch to `export =` to fix eslint-plugin-react-hooks types ([#34949](https://github.com/facebook/react/pull/34949)) //// - **[21272a680f](https://github.com/facebook/react/commit/21272a680f )**: Lower case "rsc stream" debug info ([#34921](https://github.com/facebook/react/pull/34921)) //// - **[2cfb221937](https://github.com/facebook/react/commit/2cfb221937 )**: [Flight] Allow passing DEV only startTime as an option ([#34912](https://github.com/facebook/react/pull/34912)) //// - **[58bdc0bb96](https://github.com/facebook/react/commit/58bdc0bb96 )**: [Flight] Ignore bound-anonymous-fn resources as they're not considered I/O ([#34911](https://github.com/facebook/react/pull/34911)) //// - **[bf11d2fb2f](https://github.com/facebook/react/commit/bf11d2fb2f )**: [DevTools] Infer name from stack if it's the generic "lazy" name ([#34907](https://github.com/facebook/react/pull/34907)) //// - **[ec7d9a7249](https://github.com/facebook/react/commit/ec7d9a7249 )**: Resolve the .default export of a React.lazy as the canonical value ([#34906](https://github.com/facebook/react/pull/34906)) //// - **[dc485c7303](https://github.com/facebook/react/commit/dc485c7303 )**: [Flight] Fix detached `ArrayBuffer` error when streaming typed arrays ([#34849](https://github.com/facebook/react/pull/34849)) //// - **[2381ecc290](https://github.com/facebook/react/commit/2381ecc290 )**: [ESLint] Disallow passing effect event down when inlined as a prop ([#34820](https://github.com/facebook/react/pull/34820)) //// - **[ed1351c4fb](https://github.com/facebook/react/commit/ed1351c4fb )**: [compiler] improve zod v3 backwards compat ([#34877](https://github.com/facebook/react/pull/34877)) //// - **[d8aa94b0f4](https://github.com/facebook/react/commit/d8aa94b0f4 )**: Only capture stacks for up to 10 frames for Owner Stacks ([#34864](https://github.com/facebook/react/pull/34864)) //// - **[56e846921d](https://github.com/facebook/react/commit/56e846921d )**: [Flight] Exclude RSC Stream if the stream resolves in a task ([#34838](https://github.com/facebook/react/pull/34838)) //// - **[19b71673b1](https://github.com/facebook/react/commit/19b71673b1 )**: [Flight] Forward the current environment when forwarding I/O entries ([#34836](https://github.com/facebook/react/pull/34836)) //// - **[03a62b20fd](https://github.com/facebook/react/commit/03a62b20fd )**: [Flight] Look for moved debugInfo when logging component performance track ([#34839](https://github.com/facebook/react/pull/34839)) //// - **[b9ec735de2](https://github.com/facebook/react/commit/b9ec735de2 )**: [Perf Tracks]: Clear potentially large measures ([#34803](https://github.com/facebook/react/pull/34803)) //// - **[47905a7950](https://github.com/facebook/react/commit/47905a7950 )**: Fix/add missing else branch for renders with no props change ([#34837](https://github.com/facebook/react/pull/34837)) //// - **[7b971c0a55](https://github.com/facebook/react/commit/7b971c0a55 )**: Current behavior for excluding Component render with unchanged props from Components track ([#34822](https://github.com/facebook/react/pull/34822)) //// - **[026abeaa5f](https://github.com/facebook/react/commit/026abeaa5f )**: [Flight] Respect displayName of Promise instances on the server ([#34825](https://github.com/facebook/react/pull/34825)) //// - **[93d4458fdc](https://github.com/facebook/react/commit/93d4458fdc )**: [Fiber] Ensure `useEffectEvent` reads latest values in `forwardRef` and `memo()` Components ([#34831](https://github.com/facebook/react/pull/34831)) //// - **[1d68bce19c](https://github.com/facebook/react/commit/1d68bce19c )**: [Fiber] Don't unhide a node if a direct parent offscreen is still hidden ([#34821](https://github.com/facebook/react/pull/34821)) //// - **[ead92181bd](https://github.com/facebook/react/commit/ead92181bd )**: [Flight] Avoid unnecessary indirection when serializing debug info ([#34797](https://github.com/facebook/react/pull/34797)) //// - **[d44659744f](https://github.com/facebook/react/commit/d44659744f )**: [Flight] Fix preload `as` attribute for stylesheets ([#34760](https://github.com/facebook/react/pull/34760)) //// - **[3e1b34dc51](https://github.com/facebook/react/commit/3e1b34dc51 )**: [compiler] Setup RecommendedLatest preset ([#34782](https://github.com/facebook/react/pull/34782)) //// - **[7568e71854](https://github.com/facebook/react/commit/7568e71854 )**: [eprh] Prepare for 7.0.0 ([#34757](https://github.com/facebook/react/pull/34757)) //// - **[848e0e3a4f](https://github.com/facebook/react/commit/848e0e3a4f )**: [eprh] Update plugin config to be compatible with flat and legacy ([#34762](https://github.com/facebook/react/pull/34762)) //// - **[3025aa3964](https://github.com/facebook/react/commit/3025aa3964 )**: [Flight] Don't serialize toJSON in Debug path and omit wide arrays ([#34759](https://github.com/facebook/react/pull/34759)) //// - **[a4eb2dfa6f](https://github.com/facebook/react/commit/a4eb2dfa6f )**: Release Fragment refs to Canary ([#34720](https://github.com/facebook/react/pull/34720)) //// - **[6a8c7fb6f1](https://github.com/facebook/react/commit/6a8c7fb6f1 )**: Release `` to Canary ([#34712](https://github.com/facebook/react/pull/34712)) //// - **[b65e6fc58b](https://github.com/facebook/react/commit/b65e6fc58b )**: Revert [eprh] Remove hermes-parser ([#34747](https://github.com/facebook/react/pull/34747)) //// - **[c786258422](https://github.com/facebook/react/commit/c786258422 )**: [eprh] Fix config type not being exported correctly ([#34746](https://github.com/facebook/react/pull/34746)) //// - **[1be3ce9996](https://github.com/facebook/react/commit/1be3ce9996 )**: [Fiber] Bail out of diffing wide objects and arrays ([#34742](https://github.com/facebook/react/pull/34742)) //// - **[a2329c10ff](https://github.com/facebook/react/commit/a2329c10ff )**: [eprh] 6.1.1 changelog ([#34726](https://github.com/facebook/react/pull/34726)) //// - **[d6eb735938](https://github.com/facebook/react/commit/d6eb735938 )**: [compiler] Update for Zod v3/v4 compatibility ([#34717](https://github.com/facebook/react/pull/34717)) //// - **[71753ac90a](https://github.com/facebook/react/commit/71753ac90a )**: [eprh] Remove hermes-parser ([#34719](https://github.com/facebook/react/pull/34719)) //// - **[f24d3bbc70](https://github.com/facebook/react/commit/f24d3bbc70 )**: Update readme for eprh ([#34714](https://github.com/facebook/react/pull/34714)) //// - **[85c427d822](https://github.com/facebook/react/commit/85c427d822 )**: [compiler] Remove babel/plugin-proposal-private-methods ([#34715](https://github.com/facebook/react/pull/34715)) //// - **[74dee8ef64](https://github.com/facebook/react/commit/74dee8ef64 )**: Add getClientRects to fabric fragment instance ([#34545](https://github.com/facebook/react/pull/34545)) //// - **[e866b1d1e9](https://github.com/facebook/react/commit/e866b1d1e9 )**: Add getRootNode to fabric fragment instance ([#34544](https://github.com/facebook/react/pull/34544)) //// - **[19f65ff179](https://github.com/facebook/react/commit/19f65ff179 )**: [eprh] Remove NoUnusedOptOutDirectives ([#34703](https://github.com/facebook/react/pull/34703)) //// - **[26b177bc5e](https://github.com/facebook/react/commit/26b177bc5e )**: [eprh] Fix `recommended` config for flat config compatibility ([#34700](https://github.com/facebook/react/pull/34700)) //// - **[5cc3d49f72](https://github.com/facebook/react/commit/5cc3d49f72 )**: [eprh] Add compiler rules to recommended preset ([#34675](https://github.com/facebook/react/pull/34675)) //// - **[6a8a8ef326](https://github.com/facebook/react/commit/6a8a8ef326 )**: [Flight] Add `` ([#34697](https://github.com/facebook/react/pull/34697)) //// - **[7d9f876cbc](https://github.com/facebook/react/commit/7d9f876cbc )**: [Fizz] Detatch boundary after flushing segment with boundary ([#34694](https://github.com/facebook/react/pull/34694)) //// - **[d74f061b69](https://github.com/facebook/react/commit/d74f061b69 )**: [Fiber] Clean up ViewTransition when it fails to start ([#34676](https://github.com/facebook/react/pull/34676)) //// - **[79ca5ae855](https://github.com/facebook/react/commit/79ca5ae855 )**: Bump next prerelease version numbers ([#34674](https://github.com/facebook/react/pull/34674)) //// Changelog: [General][Changed] - React Native sync for revisions f0dfee3...6117d7c jest_e2e[run_all_tests] Differential Revision: D107673776 --- package.json | 6 ++-- packages/jest-preset/package.json | 2 +- .../implementations/ReactFabric-dev.js | 6 ++-- .../implementations/ReactFabric-prod.js | 6 ++-- .../implementations/ReactFabric-profiling.js | 6 ++-- .../Libraries/Renderer/shims/ReactFabric.js | 4 ++- .../Renderer/shims/ReactFeatureFlags.js | 4 ++- .../Renderer/shims/ReactNativeTypes.js | 34 ++++++++++++++++++- .../shims/ReactNativeViewConfigRegistry.js | 4 ++- .../shims/createReactNativeComponentClass.js | 4 ++- packages/react-native/package.json | 2 +- packages/rn-tester/package.json | 2 +- packages/virtualized-lists/package.json | 2 +- private/helloworld/package.json | 4 +-- yarn.lock | 26 +++++++------- 15 files changed, 76 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index fc1855bc398..cd89bbb3b25 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 987afe28dac..29f2ec3e3f2 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 daab0e35a8e..efe89e3067f 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 2625bf690ab..f1317527757 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 544c75406ef..c434b1423de 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 15eeeb36bba..bc7baba8fa1 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 1bf6b1b6cba..bf7cb70297c 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 24cc37e31e7..0c23dc69b82 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 436b74da767..ace3c4aac40 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 a069eb91e97..3be08df8956 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 47d4675facb..81b662e103c 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/rn-tester/package.json b/packages/rn-tester/package.json index 26b5b7bd97c..631b750928e 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 d8c5de95406..5b25aaf8135 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 63cdc897f1e..42f2864c68e 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 b2ce8ccc5a2..8d585205f09 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" From dde0c97f8243d03e43f82f6c65452c6933461392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Fri, 5 Jun 2026 10:22:28 -0700 Subject: [PATCH 3/3] Remove isOSS guards from Fantom renderer tests Summary: Remove the isOSS conditionals that were skipping Fantom renderer tests in OSS environments. Now that the React Native renderers have been upgraded and the code is compatible in OSS, these tests can run in both internal and OSS environments. Changes: - ResponderEventTarget-itest.js: Removed isOSS check that skipped the entire Responder System test suite - EventTargetDispatching-itest.js: Removed isOSS check that skipped the entire EventTarget-based Event Dispatching test suite - EventDispatching-benchmark-itest.js: Removed if (isOSS) block that provided a dummy test instead of the benchmark suite - EventDispatching-itest.js: Removed if (!isOSS) condition guarding three tests for native event timeStamp and global event dispatching Changelog: [Internal] Differential Revision: D107675643 --- .../EventDispatching-benchmark-itest.js | 496 ++-- .../core/__tests__/EventDispatching-itest.js | 156 +- .../__tests__/EventTargetDispatching-itest.js | 2420 ++++++++--------- .../__tests__/ResponderEventTarget-itest.js | 4 +- 4 files changed, 1526 insertions(+), 1550 deletions(-) diff --git a/packages/react-native/src/private/renderer/core/__tests__/EventDispatching-benchmark-itest.js b/packages/react-native/src/private/renderer/core/__tests__/EventDispatching-benchmark-itest.js index 4c6b1309f5f..97a676b9af6 100644 --- a/packages/react-native/src/private/renderer/core/__tests__/EventDispatching-benchmark-itest.js +++ b/packages/react-native/src/private/renderer/core/__tests__/EventDispatching-benchmark-itest.js @@ -39,188 +39,206 @@ function createNestedViews( ); } -const {isOSS} = Fantom.getConstants(); - -if (isOSS) { - it('is not supported in OSS yet', () => { - expect(true).toBe(true); - }); -} else { - Fantom.unstable_benchmark - .suite('Event Dispatching') - .test( - 'dispatch event, flat (1 handler)', - () => { - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); - }, - { - beforeAll: () => { - ref = React.createRef(); - }, - beforeEach: () => { - root = Fantom.createRoot(); - Fantom.runTask(() => { - root.render( - {}} - style={{width: 10, height: 10}} - />, - ); - }); - }, - afterEach: () => { - root.destroy(); +Fantom.unstable_benchmark + .suite('Event Dispatching') + .test( + 'dispatch event, flat (1 handler)', + () => { + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, }, + ); + }, + { + beforeAll: () => { + ref = React.createRef(); }, - ) - .test( - 'dispatch event, nested 10 deep (bubbling)', - () => { - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); + beforeEach: () => { + root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + {}} + style={{width: 10, height: 10}} + />, + ); + }); }, - { - beforeAll: () => { - ref = React.createRef(); - }, - beforeEach: () => { - root = Fantom.createRoot(); - Fantom.runTask(() => { - root.render(createNestedViews(10, ref)); - }); - }, - afterEach: () => { - root.destroy(); + afterEach: () => { + root.destroy(); + }, + }, + ) + .test( + 'dispatch event, nested 10 deep (bubbling)', + () => { + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, }, + ); + }, + { + beforeAll: () => { + ref = React.createRef(); }, - ) - .test( - 'dispatch event, nested 50 deep (bubbling)', - () => { - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); + beforeEach: () => { + root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(createNestedViews(10, ref)); + }); }, - { - beforeAll: () => { - ref = React.createRef(); - }, - beforeEach: () => { - root = Fantom.createRoot(); - Fantom.runTask(() => { - root.render(createNestedViews(50, ref)); - }); - }, - afterEach: () => { - root.destroy(); + afterEach: () => { + root.destroy(); + }, + }, + ) + .test( + 'dispatch event, nested 50 deep (bubbling)', + () => { + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, }, + ); + }, + { + beforeAll: () => { + ref = React.createRef(); }, - ) - .test( - 'dispatch event, nested 10 deep (no handlers on ancestors)', - () => { - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); + beforeEach: () => { + root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(createNestedViews(50, ref)); + }); }, - { - beforeAll: () => { - ref = React.createRef(); - }, - beforeEach: () => { - root = Fantom.createRoot(); - Fantom.runTask(() => { - let views: React.MixedElement = ( - {}} - style={{width: 10, height: 10}} - /> - ); - for (let i = 0; i < 10; i++) { - views = {views}; - } - root.render(views); - }); - }, - afterEach: () => { - root.destroy(); + afterEach: () => { + root.destroy(); + }, + }, + ) + .test( + 'dispatch event, nested 10 deep (no handlers on ancestors)', + () => { + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, }, + ); + }, + { + beforeAll: () => { + ref = React.createRef(); }, - ) - .test( - 'dispatch event with stopPropagation, nested 10 deep', - () => { - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); + beforeEach: () => { + root = Fantom.createRoot(); + Fantom.runTask(() => { + let views: React.MixedElement = ( + {}} + style={{width: 10, height: 10}} + /> + ); + for (let i = 0; i < 10; i++) { + views = {views}; + } + root.render(views); + }); + }, + afterEach: () => { + root.destroy(); }, - { - beforeAll: () => { - ref = React.createRef(); + }, + ) + .test( + 'dispatch event with stopPropagation, nested 10 deep', + () => { + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, }, - beforeEach: () => { - root = Fantom.createRoot(); - Fantom.runTask(() => { - let views: React.MixedElement = ( - { - e.stopPropagation(); - }} - style={{width: 10, height: 10}} - /> + ); + }, + { + beforeAll: () => { + ref = React.createRef(); + }, + beforeEach: () => { + root = Fantom.createRoot(); + Fantom.runTask(() => { + let views: React.MixedElement = ( + { + e.stopPropagation(); + }} + style={{width: 10, height: 10}} + /> + ); + for (let i = 0; i < 10; i++) { + views = ( + {}}> + {views} + ); - for (let i = 0; i < 10; i++) { - views = ( - {}}> - {views} - - ); - } - root.render(views); - }); - }, - afterEach: () => { - root.destroy(); + } + root.render(views); + }); + }, + afterEach: () => { + root.destroy(); + }, + }, + ) + .test( + 'render + dispatch, flat (handler update cost)', + () => { + Fantom.runTask(() => { + root.render( + {}} + style={{width: 10, height: 10}} + />, + ); + }); + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, }, + ); + }, + { + beforeAll: () => { + ref = React.createRef(); }, - ) - .test( - 'render + dispatch, flat (handler update cost)', - () => { + beforeEach: () => { + root = Fantom.createRoot(); Fantom.runTask(() => { root.render( , ); }); - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); - }, - { - beforeAll: () => { - ref = React.createRef(); - }, - beforeEach: () => { - root = Fantom.createRoot(); - Fantom.runTask(() => { - root.render( - {}} - style={{width: 10, height: 10}} - />, - ); - }); - }, - afterEach: () => { - root.destroy(); - }, }, - ) - .test( - 'dispatch event, nested 50 deep (bubbling), stable tree', - () => { - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); + afterEach: () => { + root.destroy(); }, - { - beforeAll: () => { - ref = React.createRef(); - root = Fantom.createRoot(); - Fantom.runTask(() => { - root.render(createNestedViews(50, ref)); - }); - }, - afterAll: () => { - root.destroy(); + }, + ) + .test( + 'dispatch event, nested 50 deep (bubbling), stable tree', + () => { + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, }, + ); + }, + { + beforeAll: () => { + ref = React.createRef(); + root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(createNestedViews(50, ref)); + }); }, - ) - .test( - 'dispatch event, nested 50 deep (no handlers on ancestors), stable tree', - () => { - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); + afterAll: () => { + root.destroy(); }, - { - beforeAll: () => { - ref = React.createRef(); - root = Fantom.createRoot(); - Fantom.runTask(() => { - let views: React.MixedElement = ( - {}} - style={{width: 10, height: 10}} - /> - ); - for (let i = 0; i < 50; i++) { - views = {views}; - } - root.render(views); - }); - }, - afterAll: () => { - root.destroy(); + }, + ) + .test( + 'dispatch event, nested 50 deep (no handlers on ancestors), stable tree', + () => { + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, }, + ); + }, + { + beforeAll: () => { + ref = React.createRef(); + root = Fantom.createRoot(); + Fantom.runTask(() => { + let views: React.MixedElement = ( + {}} + style={{width: 10, height: 10}} + /> + ); + for (let i = 0; i < 50; i++) { + views = {views}; + } + root.render(views); + }); }, - ); -} + afterAll: () => { + root.destroy(); + }, + }, + ); diff --git a/packages/react-native/src/private/renderer/core/__tests__/EventDispatching-itest.js b/packages/react-native/src/private/renderer/core/__tests__/EventDispatching-itest.js index eec975e6217..bbb830ac5af 100644 --- a/packages/react-native/src/private/renderer/core/__tests__/EventDispatching-itest.js +++ b/packages/react-native/src/private/renderer/core/__tests__/EventDispatching-itest.js @@ -20,112 +20,106 @@ import * as FabricUIManager from 'react-native/Libraries/ReactNative/FabricUIMan const UIManager = nullthrows(FabricUIManager.getFabricUIManager()); describe('Event Dispatching', () => { - // The OSS renderer hasn't been synced yet to have these changes - // TODO(next-major) Remove this condition - if (!Fantom.getConstants().isOSS) { - it('provides the native event timeStamp (camel case) when available', () => { - const root = Fantom.createRoot(); + it('provides the native event timeStamp (camel case) when available', () => { + const root = Fantom.createRoot(); - const ref = React.createRef>(); + const ref = React.createRef>(); - const onPointerUp = jest.fn((e: PointerEvent) => { - e.persist(); - }); + const onPointerUp = jest.fn((e: PointerEvent) => { + e.persist(); + }); - Fantom.runTask(() => { - root.render(); - }); + Fantom.runTask(() => { + root.render(); + }); - expect(onPointerUp).toHaveBeenCalledTimes(0); + expect(onPointerUp).toHaveBeenCalledTimes(0); - const NATIVE_EVENT_TIMESTAMP = 1234; + const NATIVE_EVENT_TIMESTAMP = 1234; - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0, timeStamp: NATIVE_EVENT_TIMESTAMP}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0, timeStamp: NATIVE_EVENT_TIMESTAMP}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); - expect(onPointerUp).toHaveBeenCalledTimes(1); - expect(onPointerUp.mock.calls[0][0].timeStamp).toBe( - NATIVE_EVENT_TIMESTAMP, - ); - }); + expect(onPointerUp).toHaveBeenCalledTimes(1); + expect(onPointerUp.mock.calls[0][0].timeStamp).toBe(NATIVE_EVENT_TIMESTAMP); + }); - it('provides a default timeStamp when the native event timeStamp is NOT available', () => { - const root = Fantom.createRoot(); + it('provides a default timeStamp when the native event timeStamp is NOT available', () => { + const root = Fantom.createRoot(); - const ref = React.createRef>(); + const ref = React.createRef>(); - const onPointerUp = jest.fn((e: PointerEvent) => { - e.persist(); - }); + const onPointerUp = jest.fn((e: PointerEvent) => { + e.persist(); + }); - Fantom.runTask(() => { - root.render(); - }); + Fantom.runTask(() => { + root.render(); + }); - expect(onPointerUp).toHaveBeenCalledTimes(0); + expect(onPointerUp).toHaveBeenCalledTimes(0); - const lowerBound = performance.now(); + const lowerBound = performance.now(); - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); - const upperBound = performance.now(); + const upperBound = performance.now(); - expect(onPointerUp).toHaveBeenCalledTimes(1); - expect(onPointerUp.mock.calls[0][0].timeStamp).toBeGreaterThanOrEqual( - lowerBound, - ); - expect(onPointerUp.mock.calls[0][0].timeStamp).toBeLessThanOrEqual( - upperBound, - ); - }); + expect(onPointerUp).toHaveBeenCalledTimes(1); + expect(onPointerUp.mock.calls[0][0].timeStamp).toBeGreaterThanOrEqual( + lowerBound, + ); + expect(onPointerUp.mock.calls[0][0].timeStamp).toBeLessThanOrEqual( + upperBound, + ); + }); - it('exposes dispatched events in the global scope', () => { - const root = Fantom.createRoot(); + it('exposes dispatched events in the global scope', () => { + const root = Fantom.createRoot(); - const ref = React.createRef>(); + const ref = React.createRef>(); - let globalEventIsDispatchedEvent: boolean = false; + let globalEventIsDispatchedEvent: boolean = false; - const onPointerUp = jest.fn((e: PointerEvent) => { - globalEventIsDispatchedEvent = global.event === e; - }); + const onPointerUp = jest.fn((e: PointerEvent) => { + globalEventIsDispatchedEvent = global.event === e; + }); - Fantom.runTask(() => { - root.render(); - }); + Fantom.runTask(() => { + root.render(); + }); - const globalEventValueBeforeDispatch = {type: 'some-event'}; - global.event = globalEventValueBeforeDispatch; + const globalEventValueBeforeDispatch = {type: 'some-event'}; + global.event = globalEventValueBeforeDispatch; - expect(onPointerUp).toHaveBeenCalledTimes(0); + expect(onPointerUp).toHaveBeenCalledTimes(0); - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); - expect(onPointerUp).toHaveBeenCalledTimes(1); - expect(globalEventIsDispatchedEvent).toBe(true); - expect(global.event).toBe(globalEventValueBeforeDispatch); - }); - } + expect(onPointerUp).toHaveBeenCalledTimes(1); + expect(globalEventIsDispatchedEvent).toBe(true); + expect(global.event).toBe(globalEventValueBeforeDispatch); + }); it('dispatches events with discrete priority', () => { const root = Fantom.createRoot(); 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 76de52d9871..480fd956087 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 @@ -33,51 +33,482 @@ function asEventTarget(node: ?interface {}): ReadOnlyNodeWithEventTarget { return node; } -const {isOSS} = Fantom.getConstants(); +describe('EventTarget-based Event Dispatching', () => { + it('dispatches basic press event to handler', () => { + const root = Fantom.createRoot(); + const ref = React.createRef>(); + const onPointerUp = jest.fn(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(onPointerUp).toHaveBeenCalledTimes(0); + + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 10, y: 20}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(onPointerUp).toHaveBeenCalledTimes(1); + }); + + it('event bubbles from child to parent', () => { + const root = Fantom.createRoot(); + const childRef = React.createRef>(); + const parentHandler = jest.fn(); + + Fantom.runTask(() => { + root.render( + + + , + ); + }); + + expect(parentHandler).toHaveBeenCalledTimes(0); + + Fantom.dispatchNativeEvent( + childRef, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(parentHandler).toHaveBeenCalledTimes(1); + }); + + it('capture phase fires before bubble phase', () => { + const root = Fantom.createRoot(); + const childRef = React.createRef>(); + const order: Array = []; + + Fantom.runTask(() => { + root.render( + { + order.push('parent-capture'); + }} + onPointerUp={() => { + order.push('parent-bubble'); + }}> + { + order.push('child-capture'); + }} + onPointerUp={() => { + order.push('child-bubble'); + }} + /> + , + ); + }); + + Fantom.dispatchNativeEvent( + childRef, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(order).toEqual([ + 'parent-capture', + 'child-capture', + 'child-bubble', + 'parent-bubble', + ]); + }); + + it('stopPropagation prevents parent handler from firing', () => { + const root = Fantom.createRoot(); + const childRef = React.createRef>(); + const parentHandler = jest.fn(); + const childHandler = jest.fn((e: PointerEvent) => { + e.stopPropagation(); + }); + + Fantom.runTask(() => { + root.render( + + + , + ); + }); + + Fantom.dispatchNativeEvent( + childRef, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(childHandler).toHaveBeenCalledTimes(1); + expect(parentHandler).toHaveBeenCalledTimes(0); + }); + + it('event object has correct nativeEvent property', () => { + const root = Fantom.createRoot(); + const ref = React.createRef>(); + let capturedNativeEvent: NativePointerEvent | null = null; + + const onPointerUp = jest.fn((e: PointerEvent) => { + // Capture nativeEvent inside the handler because legacy SyntheticEvent + // nullifies properties after dispatch. + capturedNativeEvent = e.nativeEvent; + }); + + Fantom.runTask(() => { + root.render(); + }); + + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 42, y: 99}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(onPointerUp).toHaveBeenCalledTimes(1); + expect(capturedNativeEvent?.x).toBe(42); + expect(capturedNativeEvent?.y).toBe(99); + }); + + it('handler updates correctly when prop changes', () => { + const root = Fantom.createRoot(); + const ref = React.createRef>(); + const firstHandler = jest.fn(); + const secondHandler = jest.fn(); + + Fantom.runTask(() => { + root.render(); + }); + + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(firstHandler).toHaveBeenCalledTimes(1); + expect(secondHandler).toHaveBeenCalledTimes(0); + + // Re-render with a new handler + Fantom.runTask(() => { + root.render(); + }); + + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(firstHandler).toHaveBeenCalledTimes(1); + expect(secondHandler).toHaveBeenCalledTimes(1); + }); + + it('handler removal stops event dispatch', () => { + const root = Fantom.createRoot(); + const ref = React.createRef>(); + const handler = jest.fn(); + + Fantom.runTask(() => { + root.render(); + }); + + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(handler).toHaveBeenCalledTimes(1); + + // Re-render without the handler + Fantom.runTask(() => { + root.render(); + }); + + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(handler).toHaveBeenCalledTimes(1); + }); + + it('multiple event types on the same element dispatch correctly', () => { + const root = Fantom.createRoot(); + const ref = React.createRef>(); + const onPointerUp = jest.fn(); + const onPointerMove = jest.fn(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(onPointerUp).toHaveBeenCalledTimes(1); + expect(onPointerMove).toHaveBeenCalledTimes(0); + + Fantom.dispatchNativeEvent( + ref, + 'onPointerMove', + {x: 1, y: 1}, + { + category: Fantom.NativeEventCategory.Continuous, + }, + ); + + expect(onPointerUp).toHaveBeenCalledTimes(1); + expect(onPointerMove).toHaveBeenCalledTimes(1); + }); + + it('preventDefault sets defaultPrevented to true', () => { + const root = Fantom.createRoot(); + const ref = React.createRef>(); + let defaultPrevented: ?boolean = false; + + const handler = jest.fn((e: PointerEvent) => { + e.preventDefault(); + defaultPrevented = e.defaultPrevented; + }); + + Fantom.runTask(() => { + root.render(); + }); + + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(handler).toHaveBeenCalledTimes(1); + expect(defaultPrevented).toBe(true); + }); + + it('isDefaultPrevented() returns true after preventDefault()', () => { + const root = Fantom.createRoot(); + const ref = React.createRef>(); + let result = false; + + const handler = jest.fn((e: PointerEvent) => { + e.preventDefault(); + result = e.isDefaultPrevented(); + }); + + Fantom.runTask(() => { + root.render(); + }); + + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(handler).toHaveBeenCalledTimes(1); + expect(result).toBe(true); + }); + + it('isDefaultPrevented() returns false when preventDefault() was not called', () => { + const root = Fantom.createRoot(); + const ref = React.createRef>(); + let result = true; + + const handler = jest.fn((e: PointerEvent) => { + result = e.isDefaultPrevented(); + }); + + Fantom.runTask(() => { + root.render(); + }); + + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(handler).toHaveBeenCalledTimes(1); + expect(result).toBe(false); + }); + + it('isPropagationStopped() returns true after stopPropagation()', () => { + const root = Fantom.createRoot(); + const ref = React.createRef>(); + let result = false; + + const handler = jest.fn((e: PointerEvent) => { + e.stopPropagation(); + result = e.isPropagationStopped(); + }); + + Fantom.runTask(() => { + root.render(); + }); + + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(handler).toHaveBeenCalledTimes(1); + expect(result).toBe(true); + }); + + it('isPropagationStopped() returns false when stopPropagation() was not called', () => { + const root = Fantom.createRoot(); + const ref = React.createRef>(); + let result = true; + + const handler = jest.fn((e: PointerEvent) => { + result = e.isPropagationStopped(); + }); + + Fantom.runTask(() => { + root.render(); + }); + + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(handler).toHaveBeenCalledTimes(1); + expect(result).toBe(false); + }); + + it('persist() is callable and does not throw', () => { + const root = Fantom.createRoot(); + const ref = React.createRef>(); -(isOSS ? describe.skip : describe)( - 'EventTarget-based Event Dispatching', - () => { - it('dispatches basic press event to handler', () => { + const handler = jest.fn((e: PointerEvent) => { + e.persist(); + }); + + Fantom.runTask(() => { + root.render(); + }); + + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(handler).toHaveBeenCalledTimes(1); + }); + + // --- addEventListener / removeEventListener on refs --- + // 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.enableImperativeEvents() + ? describe + : describe.skip)('addEventListener / removeEventListener', () => { + it('addEventListener on a ref receives dispatched events', () => { const root = Fantom.createRoot(); const ref = React.createRef>(); - const onPointerUp = jest.fn(); + const handler = jest.fn(); Fantom.runTask(() => { - root.render(); + root.render(); }); - expect(onPointerUp).toHaveBeenCalledTimes(0); + // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag + asEventTarget(ref.current).addEventListener('pointerup', handler); Fantom.dispatchNativeEvent( ref, 'onPointerUp', - {x: 10, y: 20}, + {x: 0, y: 0}, { category: Fantom.NativeEventCategory.Discrete, }, ); - expect(onPointerUp).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledTimes(1); }); - it('event bubbles from child to parent', () => { + it('removeEventListener stops receiving events', () => { const root = Fantom.createRoot(); - const childRef = React.createRef>(); - const parentHandler = jest.fn(); + const ref = React.createRef>(); + const handler = jest.fn(); Fantom.runTask(() => { - root.render( - - - , - ); + root.render(); }); - expect(parentHandler).toHaveBeenCalledTimes(0); + // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag + asEventTarget(ref.current).addEventListener('pointerup', handler); Fantom.dispatchNativeEvent( - childRef, + ref, 'onPointerUp', {x: 0, y: 0}, { @@ -85,28 +516,33 @@ const {isOSS} = Fantom.getConstants(); }, ); - expect(parentHandler).toHaveBeenCalledTimes(1); + expect(handler).toHaveBeenCalledTimes(1); + + asEventTarget(ref.current).removeEventListener('pointerup', handler); + + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(handler).toHaveBeenCalledTimes(1); }); - it('capture phase fires before bubble phase', () => { + it('addEventListener with capture option fires during capture phase', () => { const root = Fantom.createRoot(); + const parentRef = React.createRef>(); const childRef = React.createRef>(); const order: Array = []; Fantom.runTask(() => { root.render( - { - order.push('parent-capture'); - }} - onPointerUp={() => { - order.push('parent-bubble'); - }}> + { - order.push('child-capture'); - }} onPointerUp={() => { order.push('child-bubble'); }} @@ -115,6 +551,15 @@ const {isOSS} = Fantom.getConstants(); ); }); + // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag + asEventTarget(parentRef.current).addEventListener( + 'pointerup', + () => { + order.push('parent-capture'); + }, + {capture: true}, + ); + Fantom.dispatchNativeEvent( childRef, 'onPointerUp', @@ -124,30 +569,26 @@ const {isOSS} = Fantom.getConstants(); }, ); - expect(order).toEqual([ - 'parent-capture', - 'child-capture', - 'child-bubble', - 'parent-bubble', - ]); + expect(order).toEqual(['parent-capture', 'child-bubble']); }); - it('stopPropagation prevents parent handler from firing', () => { + it('addEventListener receives events that bubble from children', () => { const root = Fantom.createRoot(); + const parentRef = React.createRef>(); const childRef = React.createRef>(); - const parentHandler = jest.fn(); - const childHandler = jest.fn((e: PointerEvent) => { - e.stopPropagation(); - }); + const handler = jest.fn(); Fantom.runTask(() => { root.render( - - + + , ); }); + // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag + asEventTarget(parentRef.current).addEventListener('pointerup', handler); + Fantom.dispatchNativeEvent( childRef, 'onPointerUp', @@ -157,68 +598,73 @@ const {isOSS} = Fantom.getConstants(); }, ); - expect(childHandler).toHaveBeenCalledTimes(1); - expect(parentHandler).toHaveBeenCalledTimes(0); + expect(handler).toHaveBeenCalledTimes(1); }); - it('event object has correct nativeEvent property', () => { + // --- Declarative (prop) vs imperative (addEventListener) ordering --- + + it('declarative prop handler fires before imperative addEventListener listener', () => { const root = Fantom.createRoot(); const ref = React.createRef>(); - let capturedNativeEvent: NativePointerEvent | null = null; + const order: Array = []; - const onPointerUp = jest.fn((e: PointerEvent) => { - // Capture nativeEvent inside the handler because legacy SyntheticEvent - // nullifies properties after dispatch. - capturedNativeEvent = e.nativeEvent; + Fantom.runTask(() => { + root.render( + { + order.push('prop'); + }} + />, + ); }); - Fantom.runTask(() => { - root.render(); + // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag + asEventTarget(ref.current).addEventListener('pointerup', () => { + order.push('addEventListener'); }); Fantom.dispatchNativeEvent( ref, 'onPointerUp', - {x: 42, y: 99}, + {x: 0, y: 0}, { category: Fantom.NativeEventCategory.Discrete, }, ); - expect(onPointerUp).toHaveBeenCalledTimes(1); - expect(capturedNativeEvent?.x).toBe(42); - expect(capturedNativeEvent?.y).toBe(99); + expect(order).toEqual(['prop', 'addEventListener']); }); - it('handler updates correctly when prop changes', () => { + it('declarative capture prop fires before imperative capture addEventListener', () => { const root = Fantom.createRoot(); - const ref = React.createRef>(); - const firstHandler = jest.fn(); - const secondHandler = jest.fn(); + const parentRef = React.createRef>(); + const childRef = React.createRef>(); + const order: Array = []; Fantom.runTask(() => { - root.render(); + root.render( + { + order.push('parent-prop-capture'); + }}> + + , + ); }); - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, + // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag + asEventTarget(parentRef.current).addEventListener( + 'pointerup', + () => { + order.push('parent-imperative-capture'); }, + {capture: true}, ); - expect(firstHandler).toHaveBeenCalledTimes(1); - expect(secondHandler).toHaveBeenCalledTimes(0); - - // Re-render with a new handler - Fantom.runTask(() => { - root.render(); - }); - Fantom.dispatchNativeEvent( - ref, + childRef, 'onPointerUp', {x: 0, y: 0}, { @@ -226,35 +672,34 @@ const {isOSS} = Fantom.getConstants(); }, ); - expect(firstHandler).toHaveBeenCalledTimes(1); - expect(secondHandler).toHaveBeenCalledTimes(1); + expect(order).toEqual([ + 'parent-prop-capture', + 'parent-imperative-capture', + ]); }); - it('handler removal stops event dispatch', () => { + it('stopImmediatePropagation in prop handler prevents addEventListener listeners', () => { const root = Fantom.createRoot(); const ref = React.createRef>(); - const handler = jest.fn(); + const imperativeHandler = jest.fn(); Fantom.runTask(() => { - root.render(); + root.render( + { + e.stopImmediatePropagation(); + }} + />, + ); }); - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, + // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag + asEventTarget(ref.current).addEventListener( + 'pointerup', + imperativeHandler, ); - expect(handler).toHaveBeenCalledTimes(1); - - // Re-render without the handler - Fantom.runTask(() => { - root.render(); - }); - Fantom.dispatchNativeEvent( ref, 'onPointerUp', @@ -264,25 +709,26 @@ const {isOSS} = Fantom.getConstants(); }, ); - expect(handler).toHaveBeenCalledTimes(1); + expect(imperativeHandler).toHaveBeenCalledTimes(0); }); - it('multiple event types on the same element dispatch correctly', () => { + it('stopImmediatePropagation in addEventListener does not affect prop handler', () => { const root = Fantom.createRoot(); const ref = React.createRef>(); - const onPointerUp = jest.fn(); - const onPointerMove = jest.fn(); + const propHandler = jest.fn(); Fantom.runTask(() => { - root.render( - , - ); + root.render(); }); + // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag + asEventTarget(ref.current).addEventListener( + 'pointerup', + (e: $FlowFixMe) => { + e.stopImmediatePropagation(); + }, + ); + Fantom.dispatchNativeEvent( ref, 'onPointerUp', @@ -292,38 +738,253 @@ const {isOSS} = Fantom.getConstants(); }, ); - expect(onPointerUp).toHaveBeenCalledTimes(1); - expect(onPointerMove).toHaveBeenCalledTimes(0); + // Prop handler fires first, so it is not affected + expect(propHandler).toHaveBeenCalledTimes(1); + }); - Fantom.dispatchNativeEvent( - ref, - 'onPointerMove', - {x: 1, y: 1}, + it('full dispatch order: capture props, capture imperative, bubble props, bubble imperative', () => { + const root = Fantom.createRoot(); + const parentRef = React.createRef>(); + const childRef = React.createRef>(); + const order: Array = []; + + Fantom.runTask(() => { + root.render( + { + order.push('parent-prop-capture'); + }} + onPointerUp={() => { + order.push('parent-prop-bubble'); + }}> + { + order.push('child-prop-capture'); + }} + onPointerUp={() => { + order.push('child-prop-bubble'); + }} + /> + , + ); + }); + + // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag + asEventTarget(parentRef.current).addEventListener( + 'pointerup', + () => { + order.push('parent-imperative-capture'); + }, + {capture: true}, + ); + // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag + asEventTarget(parentRef.current).addEventListener('pointerup', () => { + order.push('parent-imperative-bubble'); + }); + // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag + asEventTarget(childRef.current).addEventListener( + 'pointerup', + () => { + order.push('child-imperative-capture'); + }, + {capture: true}, + ); + // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag + asEventTarget(childRef.current).addEventListener('pointerup', () => { + order.push('child-imperative-bubble'); + }); + + Fantom.dispatchNativeEvent( + childRef, + 'onPointerUp', + {x: 0, y: 0}, { - category: Fantom.NativeEventCategory.Continuous, + category: Fantom.NativeEventCategory.Discrete, }, ); - expect(onPointerUp).toHaveBeenCalledTimes(1); - expect(onPointerMove).toHaveBeenCalledTimes(1); + expect(order).toEqual([ + 'parent-prop-capture', + 'parent-imperative-capture', + 'child-prop-capture', + 'child-imperative-capture', + 'child-prop-bubble', + 'child-imperative-bubble', + 'parent-prop-bubble', + 'parent-imperative-bubble', + ]); + }); + }); + + it('event has type and bubbles properties when using EventTarget dispatching', () => { + const root = Fantom.createRoot(); + const ref = React.createRef>(); + let eventType: unknown = null; + let eventBubbles: unknown = null; + + const handler = jest.fn((e: PointerEvent) => { + eventType = e.type; + eventBubbles = e.bubbles; }); - it('preventDefault sets defaultPrevented to true', () => { + Fantom.runTask(() => { + root.render(); + }); + + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(handler).toHaveBeenCalledTimes(1); + // The legacy SyntheticEvent does not set type/bubbles as standard + // DOM Event properties. The new EventTarget-based path does. + if (eventType != null) { + expect(eventType).toBe('pointerup'); + expect(eventBubbles).toBe(true); + } + }); + + (ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() + ? it + : it.skip)( + 'event.target points to the original target and event.currentTarget changes at each step', + () => { const root = Fantom.createRoot(); - const ref = React.createRef>(); - let defaultPrevented: ?boolean = false; + const parentRef = React.createRef>(); + const childRef = React.createRef>(); + const targets: Array<{target: unknown, currentTarget: unknown}> = []; + + Fantom.runTask(() => { + root.render( + { + targets.push({ + target: e.target, + currentTarget: e.currentTarget, + }); + }} + onPointerUp={(e: $FlowFixMe) => { + targets.push({ + target: e.target, + currentTarget: e.currentTarget, + }); + }}> + { + targets.push({ + target: e.target, + currentTarget: e.currentTarget, + }); + }} + /> + , + ); + }); + + Fantom.dispatchNativeEvent( + childRef, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(targets).toHaveLength(3); + + // event.target is always the original target element + expect(targets[0].target).toBe(childRef.current); + expect(targets[1].target).toBe(childRef.current); + expect(targets[2].target).toBe(childRef.current); + + // event.currentTarget changes at each propagation step + // Capture: parent + expect(targets[0].currentTarget).toBe(parentRef.current); + // Bubble: child, then parent + expect(targets[1].currentTarget).toBe(childRef.current); + expect(targets[2].currentTarget).toBe(parentRef.current); + }, + ); + + (ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() && + ReactNativeFeatureFlags.enableImperativeEvents() + ? it + : it.skip)( + 'direct (non-bubbling) events do not propagate via addEventListener', + () => { + const root = Fantom.createRoot(); + const parentRef = React.createRef>(); + const childRef = React.createRef>(); + const childHandler = jest.fn(); + const parentImperativeHandler = jest.fn(); - const handler = jest.fn((e: PointerEvent) => { - e.preventDefault(); - defaultPrevented = e.defaultPrevented; + Fantom.runTask(() => { + root.render( + + + , + ); }); + // Add an imperative listener on the parent for the 'layout' event. + // Since 'layout' is a direct (non-bubbling) event, this should NOT + // fire when we dispatch onLayout on the child. + // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag + asEventTarget(parentRef.current).addEventListener( + 'layout', + parentImperativeHandler, + ); + + const childCallsBefore = childHandler.mock.calls.length; + + Fantom.dispatchNativeEvent( + childRef, + 'onLayout', + {layout: {x: 0, y: 0, width: 100, height: 50}}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + // Child handler fires + expect(childHandler.mock.calls.length - childCallsBefore).toBeGreaterThan( + 0, + ); + // Parent's addEventListener listener does NOT fire because layout + // is a non-bubbling (direct) event + expect(parentImperativeHandler).toHaveBeenCalledTimes(0); + }, + ); + + (ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() && + ReactNativeFeatureFlags.enableImperativeEvents() + ? describe + : describe.skip)('bubbling to document element and document', () => { + it('event bubbles from child up to the document element', () => { + const root = Fantom.createRoot(); + const childRef = React.createRef>(); + const documentElementHandler = jest.fn(); + Fantom.runTask(() => { - root.render(); + root.render(); }); + asEventTarget(root.document.documentElement).addEventListener( + 'pointerup', + documentElementHandler, + ); + Fantom.dispatchNativeEvent( - ref, + childRef, 'onPointerUp', {x: 0, y: 0}, { @@ -331,26 +992,64 @@ const {isOSS} = Fantom.getConstants(); }, ); - expect(handler).toHaveBeenCalledTimes(1); - expect(defaultPrevented).toBe(true); + expect(documentElementHandler).toHaveBeenCalledTimes(1); }); - it('isDefaultPrevented() returns true after preventDefault()', () => { + it('event bubbles from child up to the document', () => { const root = Fantom.createRoot(); - const ref = React.createRef>(); - let result = false; + const childRef = React.createRef>(); + const documentHandler = jest.fn(); - const handler = jest.fn((e: PointerEvent) => { - e.preventDefault(); - result = e.isDefaultPrevented(); + Fantom.runTask(() => { + root.render(); }); + asEventTarget(root.document).addEventListener( + 'pointerup', + documentHandler, + ); + + Fantom.dispatchNativeEvent( + childRef, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(documentHandler).toHaveBeenCalledTimes(1); + }); + + it('event bubbles from a deeply nested child up to document element and document', () => { + const root = Fantom.createRoot(); + const childRef = React.createRef>(); + const documentElementHandler = jest.fn(); + const documentHandler = jest.fn(); + Fantom.runTask(() => { - root.render(); + root.render( + + + + + + + , + ); }); + asEventTarget(root.document.documentElement).addEventListener( + 'pointerup', + documentElementHandler, + ); + asEventTarget(root.document).addEventListener( + 'pointerup', + documentHandler, + ); + Fantom.dispatchNativeEvent( - ref, + childRef, 'onPointerUp', {x: 0, y: 0}, { @@ -358,25 +1057,55 @@ const {isOSS} = Fantom.getConstants(); }, ); - expect(handler).toHaveBeenCalledTimes(1); - expect(result).toBe(true); + expect(documentElementHandler).toHaveBeenCalledTimes(1); + expect(documentHandler).toHaveBeenCalledTimes(1); }); - it('isDefaultPrevented() returns false when preventDefault() was not called', () => { + it('capture phase on document fires before capture phase on document element', () => { const root = Fantom.createRoot(); - const ref = React.createRef>(); - let result = true; + const childRef = React.createRef>(); + const order: Array = []; - const handler = jest.fn((e: PointerEvent) => { - result = e.isDefaultPrevented(); + Fantom.runTask(() => { + root.render( + { + order.push('child-capture'); + }} + onPointerUp={() => { + order.push('child-bubble'); + }} + />, + ); }); - Fantom.runTask(() => { - root.render(); + asEventTarget(root.document).addEventListener( + 'pointerup', + () => { + order.push('document-capture'); + }, + {capture: true}, + ); + asEventTarget(root.document.documentElement).addEventListener( + 'pointerup', + () => { + order.push('documentElement-capture'); + }, + {capture: true}, + ); + asEventTarget(root.document.documentElement).addEventListener( + 'pointerup', + () => { + order.push('documentElement-bubble'); + }, + ); + asEventTarget(root.document).addEventListener('pointerup', () => { + order.push('document-bubble'); }); Fantom.dispatchNativeEvent( - ref, + childRef, 'onPointerUp', {x: 0, y: 0}, { @@ -384,26 +1113,40 @@ const {isOSS} = Fantom.getConstants(); }, ); - expect(handler).toHaveBeenCalledTimes(1); - expect(result).toBe(false); + expect(order).toEqual([ + 'document-capture', + 'documentElement-capture', + 'child-capture', + 'child-bubble', + 'documentElement-bubble', + 'document-bubble', + ]); }); - it('isPropagationStopped() returns true after stopPropagation()', () => { + it('event.target points to the original child and event.currentTarget transitions through document element and document', () => { const root = Fantom.createRoot(); - const ref = React.createRef>(); - let result = false; - - const handler = jest.fn((e: PointerEvent) => { - e.stopPropagation(); - result = e.isPropagationStopped(); - }); + const childRef = React.createRef>(); + const targets: Array<{target: unknown, currentTarget: unknown}> = []; Fantom.runTask(() => { - root.render(); + root.render(); }); + asEventTarget(root.document.documentElement).addEventListener( + 'pointerup', + (e: $FlowFixMe) => { + targets.push({target: e.target, currentTarget: e.currentTarget}); + }, + ); + asEventTarget(root.document).addEventListener( + 'pointerup', + (e: $FlowFixMe) => { + targets.push({target: e.target, currentTarget: e.currentTarget}); + }, + ); + Fantom.dispatchNativeEvent( - ref, + childRef, 'onPointerUp', {x: 0, y: 0}, { @@ -411,25 +1154,40 @@ const {isOSS} = Fantom.getConstants(); }, ); - expect(handler).toHaveBeenCalledTimes(1); - expect(result).toBe(true); + expect(targets).toHaveLength(2); + + // event.target is always the original target element + expect(targets[0].target).toBe(childRef.current); + expect(targets[1].target).toBe(childRef.current); + + // event.currentTarget changes at each propagation step + expect(targets[0].currentTarget).toBe(root.document.documentElement); + expect(targets[1].currentTarget).toBe(root.document); }); - it('isPropagationStopped() returns false when stopPropagation() was not called', () => { + it('stopPropagation on document element prevents document handler from firing', () => { const root = Fantom.createRoot(); - const ref = React.createRef>(); - let result = true; - - const handler = jest.fn((e: PointerEvent) => { - result = e.isPropagationStopped(); + const childRef = React.createRef>(); + const documentHandler = jest.fn(); + const documentElementHandler = jest.fn((e: $FlowFixMe) => { + e.stopPropagation(); }); Fantom.runTask(() => { - root.render(); + root.render(); }); + asEventTarget(root.document.documentElement).addEventListener( + 'pointerup', + documentElementHandler, + ); + asEventTarget(root.document).addEventListener( + 'pointerup', + documentHandler, + ); + Fantom.dispatchNativeEvent( - ref, + childRef, 'onPointerUp', {x: 0, y: 0}, { @@ -437,24 +1195,26 @@ const {isOSS} = Fantom.getConstants(); }, ); - expect(handler).toHaveBeenCalledTimes(1); - expect(result).toBe(false); + expect(documentElementHandler).toHaveBeenCalledTimes(1); + expect(documentHandler).toHaveBeenCalledTimes(0); }); - it('persist() is callable and does not throw', () => { + it('removeEventListener on document element stops events from being received', () => { const root = Fantom.createRoot(); - const ref = React.createRef>(); - - const handler = jest.fn((e: PointerEvent) => { - e.persist(); - }); + const childRef = React.createRef>(); + const documentElementHandler = jest.fn(); Fantom.runTask(() => { - root.render(); + root.render(); }); + asEventTarget(root.document.documentElement).addEventListener( + 'pointerup', + documentElementHandler, + ); + Fantom.dispatchNativeEvent( - ref, + childRef, 'onPointerUp', {x: 0, y: 0}, { @@ -462,57 +1222,134 @@ const {isOSS} = Fantom.getConstants(); }, ); - expect(handler).toHaveBeenCalledTimes(1); + expect(documentElementHandler).toHaveBeenCalledTimes(1); + + asEventTarget(root.document.documentElement).removeEventListener( + 'pointerup', + documentElementHandler, + ); + + Fantom.dispatchNativeEvent( + childRef, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + expect(documentElementHandler).toHaveBeenCalledTimes(1); }); - // --- addEventListener / removeEventListener on refs --- - // 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. + it('direct (non-bubbling) events do not reach the document element or document', () => { + const root = Fantom.createRoot(); + const childRef = React.createRef>(); + const childHandler = jest.fn(); + const documentElementHandler = jest.fn(); + const documentHandler = jest.fn(); - (ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() && - ReactNativeFeatureFlags.enableImperativeEvents() - ? describe - : describe.skip)('addEventListener / removeEventListener', () => { - it('addEventListener on a ref receives dispatched events', () => { - const root = Fantom.createRoot(); - const ref = React.createRef>(); - const handler = jest.fn(); + Fantom.runTask(() => { + root.render(); + }); - Fantom.runTask(() => { - root.render(); - }); + asEventTarget(root.document.documentElement).addEventListener( + 'layout', + documentElementHandler, + ); + asEventTarget(root.document).addEventListener('layout', documentHandler); - // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag - asEventTarget(ref.current).addEventListener('pointerup', handler); + const childCallsBefore = childHandler.mock.calls.length; - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); + Fantom.dispatchNativeEvent( + childRef, + 'onLayout', + {layout: {x: 0, y: 0, width: 100, height: 50}}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); - expect(handler).toHaveBeenCalledTimes(1); - }); + // Child handler fires + expect(childHandler.mock.calls.length - childCallsBefore).toBeGreaterThan( + 0, + ); + // Non-bubbling events don't reach the document element or the document + expect(documentElementHandler).toHaveBeenCalledTimes(0); + expect(documentHandler).toHaveBeenCalledTimes(0); + }); + }); - it('removeEventListener stops receiving events', () => { - const root = Fantom.createRoot(); - const ref = React.createRef>(); - const handler = jest.fn(); + it('stopPropagation in capture phase prevents all bubble-phase handlers', () => { + const root = Fantom.createRoot(); + const childRef = React.createRef>(); + const order: Array = []; - Fantom.runTask(() => { - root.render(); - }); + Fantom.runTask(() => { + root.render( + { + order.push('parent-capture'); + e.stopPropagation(); + }} + onPointerUp={() => { + order.push('parent-bubble'); + }}> + { + order.push('child-capture'); + }} + onPointerUp={() => { + order.push('child-bubble'); + }} + /> + , + ); + }); - // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag - asEventTarget(ref.current).addEventListener('pointerup', handler); + Fantom.dispatchNativeEvent( + childRef, + 'onPointerUp', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); + + // Only the parent capture handler should fire; everything else is stopped + expect(order).toEqual(['parent-capture']); + }); + + // When enableNativeEventTargetEventDispatching is true, EventTarget.js + // defers handler errors via setTimeout(0) in reportListenerError. This + // leaves a pending callback that Fantom's validateEmptyMessageQueue + // catches, and the error leaks into subsequent tests. Skip in that + // configuration until the error propagation mechanism is made + // synchronous (matching the legacy rethrowCaughtError pattern). + (ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() + ? describe.skip + : describe)('error handling', () => { + it('error in event handler does not break dispatch to subsequent listeners', () => { + const root = Fantom.createRoot(); + const childRef = React.createRef>(); + const parentHandler = jest.fn(); + + Fantom.runTask(() => { + root.render( + + { + throw new Error('handler error'); + }} + /> + , + ); + }); + const dispatch = () => Fantom.dispatchNativeEvent( - ref, + childRef, 'onPointerUp', {x: 0, y: 0}, { @@ -520,1075 +1357,230 @@ const {isOSS} = Fantom.getConstants(); }, ); - expect(handler).toHaveBeenCalledTimes(1); + expect(dispatch).toThrow('handler error'); - asEventTarget(ref.current).removeEventListener('pointerup', handler); + // The parent bubble handler should still fire despite child's error + expect(parentHandler).toHaveBeenCalledTimes(1); + }); + }); - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); - - expect(handler).toHaveBeenCalledTimes(1); - }); - - it('addEventListener with capture option fires during capture phase', () => { - const root = Fantom.createRoot(); - const parentRef = React.createRef>(); - const childRef = React.createRef>(); - const order: Array = []; - - Fantom.runTask(() => { - root.render( - - { - order.push('child-bubble'); - }} - /> - , - ); - }); - - // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag - asEventTarget(parentRef.current).addEventListener( - 'pointerup', - () => { - order.push('parent-capture'); - }, - {capture: true}, - ); - - Fantom.dispatchNativeEvent( - childRef, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); - - expect(order).toEqual(['parent-capture', 'child-bubble']); - }); - - it('addEventListener receives events that bubble from children', () => { - const root = Fantom.createRoot(); - const parentRef = React.createRef>(); - const childRef = React.createRef>(); - const handler = jest.fn(); - - Fantom.runTask(() => { - root.render( - - - , - ); - }); - - // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag - asEventTarget(parentRef.current).addEventListener('pointerup', handler); - - Fantom.dispatchNativeEvent( - childRef, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); - - expect(handler).toHaveBeenCalledTimes(1); - }); - - // --- Declarative (prop) vs imperative (addEventListener) ordering --- - - it('declarative prop handler fires before imperative addEventListener listener', () => { - const root = Fantom.createRoot(); - const ref = React.createRef>(); - const order: Array = []; - - Fantom.runTask(() => { - root.render( - { - order.push('prop'); - }} - />, - ); - }); - - // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag - asEventTarget(ref.current).addEventListener('pointerup', () => { - order.push('addEventListener'); - }); - - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); - - expect(order).toEqual(['prop', 'addEventListener']); - }); - - it('declarative capture prop fires before imperative capture addEventListener', () => { - const root = Fantom.createRoot(); - const parentRef = React.createRef>(); - const childRef = React.createRef>(); - const order: Array = []; - - Fantom.runTask(() => { - root.render( - { - order.push('parent-prop-capture'); - }}> - - , - ); - }); - - // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag - asEventTarget(parentRef.current).addEventListener( - 'pointerup', - () => { - order.push('parent-imperative-capture'); - }, - {capture: true}, - ); - - Fantom.dispatchNativeEvent( - childRef, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); - - expect(order).toEqual([ - 'parent-prop-capture', - 'parent-imperative-capture', - ]); - }); - - it('stopImmediatePropagation in prop handler prevents addEventListener listeners', () => { - const root = Fantom.createRoot(); - const ref = React.createRef>(); - const imperativeHandler = jest.fn(); - - Fantom.runTask(() => { - root.render( - { - e.stopImmediatePropagation(); - }} - />, - ); - }); - - // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag - asEventTarget(ref.current).addEventListener( - 'pointerup', - imperativeHandler, - ); - - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); - - expect(imperativeHandler).toHaveBeenCalledTimes(0); - }); - - it('stopImmediatePropagation in addEventListener does not affect prop handler', () => { - const root = Fantom.createRoot(); - const ref = React.createRef>(); - const propHandler = jest.fn(); - - Fantom.runTask(() => { - root.render(); - }); - - // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag - asEventTarget(ref.current).addEventListener( - 'pointerup', - (e: $FlowFixMe) => { - e.stopImmediatePropagation(); - }, - ); + (ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() + ? describe + : describe.skip)('event timestamps', () => { + it('event preserves native timestamp from nativeEvent.timeStamp', () => { + const root = Fantom.createRoot(); + const ref = React.createRef>(); + let eventTimeStamp: unknown = null; - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, + Fantom.runTask(() => { + root.render( + { + eventTimeStamp = e.timeStamp; + }} + />, ); - - // Prop handler fires first, so it is not affected - expect(propHandler).toHaveBeenCalledTimes(1); }); - it('full dispatch order: capture props, capture imperative, bubble props, bubble imperative', () => { - const root = Fantom.createRoot(); - const parentRef = React.createRef>(); - const childRef = React.createRef>(); - const order: Array = []; - - Fantom.runTask(() => { - root.render( - { - order.push('parent-prop-capture'); - }} - onPointerUp={() => { - order.push('parent-prop-bubble'); - }}> - { - order.push('child-prop-capture'); - }} - onPointerUp={() => { - order.push('child-prop-bubble'); - }} - /> - , - ); - }); - - // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag - asEventTarget(parentRef.current).addEventListener( - 'pointerup', - () => { - order.push('parent-imperative-capture'); - }, - {capture: true}, - ); - // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag - asEventTarget(parentRef.current).addEventListener('pointerup', () => { - order.push('parent-imperative-bubble'); - }); - // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag - asEventTarget(childRef.current).addEventListener( - 'pointerup', - () => { - order.push('child-imperative-capture'); - }, - {capture: true}, - ); - // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag - asEventTarget(childRef.current).addEventListener('pointerup', () => { - order.push('child-imperative-bubble'); - }); - - Fantom.dispatchNativeEvent( - childRef, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); + const nativeTimestamp = 12345.678; + Fantom.dispatchNativeEvent( + ref, + 'onPointerUp', + {x: 0, y: 0, timeStamp: nativeTimestamp}, + {category: Fantom.NativeEventCategory.Discrete}, + ); - expect(order).toEqual([ - 'parent-prop-capture', - 'parent-imperative-capture', - 'child-prop-capture', - 'child-imperative-capture', - 'child-prop-bubble', - 'child-imperative-bubble', - 'parent-prop-bubble', - 'parent-imperative-bubble', - ]); - }); + expect(eventTimeStamp).toBe(nativeTimestamp); }); + }); + + // --- dispatchConfig --- - it('event has type and bubbles properties when using EventTarget dispatching', () => { + describe('dispatchConfig', () => { + it('includes phasedRegistrationNames on bubbling events', () => { const root = Fantom.createRoot(); const ref = React.createRef>(); - let eventType: unknown = null; - let eventBubbles: unknown = null; - - const handler = jest.fn((e: PointerEvent) => { - eventType = e.type; - eventBubbles = e.bubbles; - }); + let capturedDispatchConfig: $FlowFixMe = null; Fantom.runTask(() => { - root.render(); + root.render( + { + capturedDispatchConfig = e.dispatchConfig; + }} + />, + ); }); Fantom.dispatchNativeEvent( ref, 'onPointerUp', {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, + {category: Fantom.NativeEventCategory.Discrete}, ); - expect(handler).toHaveBeenCalledTimes(1); - // The legacy SyntheticEvent does not set type/bubbles as standard - // DOM Event properties. The new EventTarget-based path does. - if (eventType != null) { - expect(eventType).toBe('pointerup'); - expect(eventBubbles).toBe(true); - } + expect(capturedDispatchConfig).not.toBeNull(); + expect(capturedDispatchConfig.phasedRegistrationNames.bubbled).toBe( + 'onPointerUp', + ); + expect(capturedDispatchConfig.phasedRegistrationNames.captured).toBe( + 'onPointerUpCapture', + ); }); - (ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() - ? it - : it.skip)( - 'event.target points to the original target and event.currentTarget changes at each step', - () => { - const root = Fantom.createRoot(); - const parentRef = React.createRef>(); - const childRef = React.createRef>(); - const targets: Array<{target: unknown, currentTarget: unknown}> = []; - - Fantom.runTask(() => { - root.render( - { - targets.push({ - target: e.target, - currentTarget: e.currentTarget, - }); - }} - onPointerUp={(e: $FlowFixMe) => { - targets.push({ - target: e.target, - currentTarget: e.currentTarget, - }); - }}> - { - targets.push({ - target: e.target, - currentTarget: e.currentTarget, - }); - }} - /> - , - ); - }); - - Fantom.dispatchNativeEvent( - childRef, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); - - expect(targets).toHaveLength(3); - - // event.target is always the original target element - expect(targets[0].target).toBe(childRef.current); - expect(targets[1].target).toBe(childRef.current); - expect(targets[2].target).toBe(childRef.current); - - // event.currentTarget changes at each propagation step - // Capture: parent - expect(targets[0].currentTarget).toBe(parentRef.current); - // Bubble: child, then parent - expect(targets[1].currentTarget).toBe(childRef.current); - expect(targets[2].currentTarget).toBe(parentRef.current); - }, - ); - - (ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() && - ReactNativeFeatureFlags.enableImperativeEvents() - ? it - : it.skip)( - 'direct (non-bubbling) events do not propagate via addEventListener', - () => { - const root = Fantom.createRoot(); - const parentRef = React.createRef>(); - const childRef = React.createRef>(); - const childHandler = jest.fn(); - const parentImperativeHandler = jest.fn(); - - Fantom.runTask(() => { - root.render( - - - , - ); - }); - - // Add an imperative listener on the parent for the 'layout' event. - // Since 'layout' is a direct (non-bubbling) event, this should NOT - // fire when we dispatch onLayout on the child. - // Temporary: ReadOnlyNode extends EventTarget at runtime behind feature flag - asEventTarget(parentRef.current).addEventListener( - 'layout', - parentImperativeHandler, - ); - - const childCallsBefore = childHandler.mock.calls.length; - - Fantom.dispatchNativeEvent( - childRef, - 'onLayout', - {layout: {x: 0, y: 0, width: 100, height: 50}}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); - - // Child handler fires - expect( - childHandler.mock.calls.length - childCallsBefore, - ).toBeGreaterThan(0); - // Parent's addEventListener listener does NOT fire because layout - // is a non-bubbling (direct) event - expect(parentImperativeHandler).toHaveBeenCalledTimes(0); - }, - ); - - (ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() && - ReactNativeFeatureFlags.enableImperativeEvents() - ? describe - : describe.skip)('bubbling to document element and document', () => { - it('event bubbles from child up to the document element', () => { - const root = Fantom.createRoot(); - const childRef = React.createRef>(); - const documentElementHandler = jest.fn(); - - Fantom.runTask(() => { - root.render(); - }); - - asEventTarget(root.document.documentElement).addEventListener( - 'pointerup', - documentElementHandler, - ); - - Fantom.dispatchNativeEvent( - childRef, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); - - expect(documentElementHandler).toHaveBeenCalledTimes(1); - }); - - it('event bubbles from child up to the document', () => { - const root = Fantom.createRoot(); - const childRef = React.createRef>(); - const documentHandler = jest.fn(); - - Fantom.runTask(() => { - root.render(); - }); - - asEventTarget(root.document).addEventListener( - 'pointerup', - documentHandler, - ); - - Fantom.dispatchNativeEvent( - childRef, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); - - expect(documentHandler).toHaveBeenCalledTimes(1); - }); - - it('event bubbles from a deeply nested child up to document element and document', () => { - const root = Fantom.createRoot(); - const childRef = React.createRef>(); - const documentElementHandler = jest.fn(); - const documentHandler = jest.fn(); - - Fantom.runTask(() => { - root.render( - - - - - - - , - ); - }); - - asEventTarget(root.document.documentElement).addEventListener( - 'pointerup', - documentElementHandler, - ); - asEventTarget(root.document).addEventListener( - 'pointerup', - documentHandler, - ); - - Fantom.dispatchNativeEvent( - childRef, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); - - expect(documentElementHandler).toHaveBeenCalledTimes(1); - expect(documentHandler).toHaveBeenCalledTimes(1); - }); - - it('capture phase on document fires before capture phase on document element', () => { - const root = Fantom.createRoot(); - const childRef = React.createRef>(); - const order: Array = []; - - Fantom.runTask(() => { - root.render( - { - order.push('child-capture'); - }} - onPointerUp={() => { - order.push('child-bubble'); - }} - />, - ); - }); - - asEventTarget(root.document).addEventListener( - 'pointerup', - () => { - order.push('document-capture'); - }, - {capture: true}, - ); - asEventTarget(root.document.documentElement).addEventListener( - 'pointerup', - () => { - order.push('documentElement-capture'); - }, - {capture: true}, - ); - asEventTarget(root.document.documentElement).addEventListener( - 'pointerup', - () => { - order.push('documentElement-bubble'); - }, - ); - asEventTarget(root.document).addEventListener('pointerup', () => { - order.push('document-bubble'); - }); - - Fantom.dispatchNativeEvent( - childRef, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); - - expect(order).toEqual([ - 'document-capture', - 'documentElement-capture', - 'child-capture', - 'child-bubble', - 'documentElement-bubble', - 'document-bubble', - ]); - }); - - it('event.target points to the original child and event.currentTarget transitions through document element and document', () => { - const root = Fantom.createRoot(); - const childRef = React.createRef>(); - const targets: Array<{target: unknown, currentTarget: unknown}> = []; - - Fantom.runTask(() => { - root.render(); - }); - - asEventTarget(root.document.documentElement).addEventListener( - 'pointerup', - (e: $FlowFixMe) => { - targets.push({target: e.target, currentTarget: e.currentTarget}); - }, - ); - asEventTarget(root.document).addEventListener( - 'pointerup', - (e: $FlowFixMe) => { - targets.push({target: e.target, currentTarget: e.currentTarget}); - }, - ); - - Fantom.dispatchNativeEvent( - childRef, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); - - expect(targets).toHaveLength(2); - - // event.target is always the original target element - expect(targets[0].target).toBe(childRef.current); - expect(targets[1].target).toBe(childRef.current); - - // event.currentTarget changes at each propagation step - expect(targets[0].currentTarget).toBe(root.document.documentElement); - expect(targets[1].currentTarget).toBe(root.document); - }); - - it('stopPropagation on document element prevents document handler from firing', () => { - const root = Fantom.createRoot(); - const childRef = React.createRef>(); - const documentHandler = jest.fn(); - const documentElementHandler = jest.fn((e: $FlowFixMe) => { - e.stopPropagation(); - }); - - Fantom.runTask(() => { - root.render(); - }); - - asEventTarget(root.document.documentElement).addEventListener( - 'pointerup', - documentElementHandler, - ); - asEventTarget(root.document).addEventListener( - 'pointerup', - documentHandler, - ); + it('includes registrationName on direct events', () => { + const root = Fantom.createRoot(); + const ref = React.createRef>(); + let capturedDispatchConfig: $FlowFixMe = null; - Fantom.dispatchNativeEvent( - childRef, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, + Fantom.runTask(() => { + root.render( + { + capturedDispatchConfig = e.dispatchConfig; + }} + />, ); - - expect(documentElementHandler).toHaveBeenCalledTimes(1); - expect(documentHandler).toHaveBeenCalledTimes(0); }); - it('removeEventListener on document element stops events from being received', () => { - const root = Fantom.createRoot(); - const childRef = React.createRef>(); - const documentElementHandler = jest.fn(); + Fantom.dispatchNativeEvent( + ref, + 'onLayout', + {x: 0, y: 0, width: 100, height: 50}, + {category: Fantom.NativeEventCategory.Discrete}, + ); - Fantom.runTask(() => { - root.render(); - }); + expect(capturedDispatchConfig).not.toBeNull(); + expect(capturedDispatchConfig.registrationName).toBe('onLayout'); + }); + }); - asEventTarget(root.document.documentElement).addEventListener( - 'pointerup', - documentElementHandler, - ); + // --- skipBubbling --- - Fantom.dispatchNativeEvent( - childRef, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); + describe('skipBubbling (pointerenter / pointerleave)', () => { + it('does not bubble onPointerEnter to ancestor views', () => { + const root = Fantom.createRoot(); - expect(documentElementHandler).toHaveBeenCalledTimes(1); + const childRef = React.createRef>(); - asEventTarget(root.document.documentElement).removeEventListener( - 'pointerup', - documentElementHandler, - ); + const parentSpy = jest.fn((_e: PointerEvent) => {}); + const childSpy = jest.fn((_e: PointerEvent) => {}); - Fantom.dispatchNativeEvent( - childRef, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, + Fantom.runTask(() => { + root.render( + + + , ); - - expect(documentElementHandler).toHaveBeenCalledTimes(1); }); - it('direct (non-bubbling) events do not reach the document element or document', () => { - const root = Fantom.createRoot(); - const childRef = React.createRef>(); - const childHandler = jest.fn(); - const documentElementHandler = jest.fn(); - const documentHandler = jest.fn(); - - Fantom.runTask(() => { - root.render(); - }); - - asEventTarget(root.document.documentElement).addEventListener( - 'layout', - documentElementHandler, - ); - asEventTarget(root.document).addEventListener( - 'layout', - documentHandler, - ); - - const childCallsBefore = childHandler.mock.calls.length; - - Fantom.dispatchNativeEvent( - childRef, - 'onLayout', - {layout: {x: 0, y: 0, width: 100, height: 50}}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); + Fantom.dispatchNativeEvent( + childRef, + 'onPointerEnter', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.ContinuousStart, + }, + ); - // Child handler fires - expect( - childHandler.mock.calls.length - childCallsBefore, - ).toBeGreaterThan(0); - // Non-bubbling events don't reach the document element or the document - expect(documentElementHandler).toHaveBeenCalledTimes(0); - expect(documentHandler).toHaveBeenCalledTimes(0); - }); + expect(childSpy).toHaveBeenCalledTimes(1); + expect(parentSpy).toHaveBeenCalledTimes(0); }); - it('stopPropagation in capture phase prevents all bubble-phase handlers', () => { + it('does not bubble onPointerLeave to ancestor views', () => { const root = Fantom.createRoot(); + const childRef = React.createRef>(); - const order: Array = []; + + const parentSpy = jest.fn((_e: PointerEvent) => {}); + const childSpy = jest.fn((_e: PointerEvent) => {}); Fantom.runTask(() => { root.render( - { - order.push('parent-capture'); - e.stopPropagation(); - }} - onPointerUp={() => { - order.push('parent-bubble'); - }}> - { - order.push('child-capture'); - }} - onPointerUp={() => { - order.push('child-bubble'); - }} - /> + + , ); }); Fantom.dispatchNativeEvent( childRef, - 'onPointerUp', + 'onPointerLeave', {x: 0, y: 0}, { - category: Fantom.NativeEventCategory.Discrete, + category: Fantom.NativeEventCategory.ContinuousEnd, }, ); - // Only the parent capture handler should fire; everything else is stopped - expect(order).toEqual(['parent-capture']); + expect(childSpy).toHaveBeenCalledTimes(1); + expect(parentSpy).toHaveBeenCalledTimes(0); }); - // When enableNativeEventTargetEventDispatching is true, EventTarget.js - // defers handler errors via setTimeout(0) in reportListenerError. This - // leaves a pending callback that Fantom's validateEmptyMessageQueue - // catches, and the error leaks into subsequent tests. Skip in that - // configuration until the error propagation mechanism is made - // synchronous (matching the legacy rethrowCaughtError pattern). - (ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() - ? describe.skip - : describe)('error handling', () => { - it('error in event handler does not break dispatch to subsequent listeners', () => { - const root = Fantom.createRoot(); - const childRef = React.createRef>(); - const parentHandler = jest.fn(); - - Fantom.runTask(() => { - root.render( - - { - throw new Error('handler error'); - }} - /> - , - ); - }); - - const dispatch = () => - Fantom.dispatchNativeEvent( - childRef, - 'onPointerUp', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); - - expect(dispatch).toThrow('handler error'); - - // The parent bubble handler should still fire despite child's error - expect(parentHandler).toHaveBeenCalledTimes(1); - }); - }); - - (ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching() - ? describe - : describe.skip)('event timestamps', () => { - it('event preserves native timestamp from nativeEvent.timeStamp', () => { - const root = Fantom.createRoot(); - const ref = React.createRef>(); - let eventTimeStamp: unknown = null; - - Fantom.runTask(() => { - root.render( - { - eventTimeStamp = e.timeStamp; - }} - />, - ); - }); - - const nativeTimestamp = 12345.678; - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0, timeStamp: nativeTimestamp}, - {category: Fantom.NativeEventCategory.Discrete}, - ); - - expect(eventTimeStamp).toBe(nativeTimestamp); - }); - }); - - // --- dispatchConfig --- - - describe('dispatchConfig', () => { - it('includes phasedRegistrationNames on bubbling events', () => { - const root = Fantom.createRoot(); - const ref = React.createRef>(); - let capturedDispatchConfig: $FlowFixMe = null; - - Fantom.runTask(() => { - root.render( - { - capturedDispatchConfig = e.dispatchConfig; - }} - />, - ); - }); + it('still fires onPointerEnterCapture on ancestors during the capture phase', () => { + const root = Fantom.createRoot(); - Fantom.dispatchNativeEvent( - ref, - 'onPointerUp', - {x: 0, y: 0}, - {category: Fantom.NativeEventCategory.Discrete}, - ); + const childRef = React.createRef>(); - expect(capturedDispatchConfig).not.toBeNull(); - expect(capturedDispatchConfig.phasedRegistrationNames.bubbled).toBe( - 'onPointerUp', - ); - expect(capturedDispatchConfig.phasedRegistrationNames.captured).toBe( - 'onPointerUpCapture', - ); + const callOrder: Array = []; + const parentCaptureSpy = jest.fn((_e: PointerEvent) => { + callOrder.push('parentCapture'); }); - - it('includes registrationName on direct events', () => { - const root = Fantom.createRoot(); - const ref = React.createRef>(); - let capturedDispatchConfig: $FlowFixMe = null; - - Fantom.runTask(() => { - root.render( - { - capturedDispatchConfig = e.dispatchConfig; - }} - />, - ); - }); - - Fantom.dispatchNativeEvent( - ref, - 'onLayout', - {x: 0, y: 0, width: 100, height: 50}, - {category: Fantom.NativeEventCategory.Discrete}, - ); - - expect(capturedDispatchConfig).not.toBeNull(); - expect(capturedDispatchConfig.registrationName).toBe('onLayout'); + const childSpy = jest.fn((_e: PointerEvent) => { + callOrder.push('child'); }); - }); - - // --- skipBubbling --- - - describe('skipBubbling (pointerenter / pointerleave)', () => { - it('does not bubble onPointerEnter to ancestor views', () => { - const root = Fantom.createRoot(); - - const childRef = React.createRef>(); - const parentSpy = jest.fn((_e: PointerEvent) => {}); - const childSpy = jest.fn((_e: PointerEvent) => {}); - - Fantom.runTask(() => { - root.render( - - - , - ); - }); - - Fantom.dispatchNativeEvent( - childRef, - 'onPointerEnter', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.ContinuousStart, - }, + Fantom.runTask(() => { + root.render( + + + , ); - - expect(childSpy).toHaveBeenCalledTimes(1); - expect(parentSpy).toHaveBeenCalledTimes(0); }); - it('does not bubble onPointerLeave to ancestor views', () => { - const root = Fantom.createRoot(); - - const childRef = React.createRef>(); - - const parentSpy = jest.fn((_e: PointerEvent) => {}); - const childSpy = jest.fn((_e: PointerEvent) => {}); - - Fantom.runTask(() => { - root.render( - - - , - ); - }); - - Fantom.dispatchNativeEvent( - childRef, - 'onPointerLeave', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.ContinuousEnd, - }, - ); - - expect(childSpy).toHaveBeenCalledTimes(1); - expect(parentSpy).toHaveBeenCalledTimes(0); - }); + Fantom.dispatchNativeEvent( + childRef, + 'onPointerEnter', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.ContinuousStart, + }, + ); - it('still fires onPointerEnterCapture on ancestors during the capture phase', () => { - const root = Fantom.createRoot(); + expect(parentCaptureSpy).toHaveBeenCalledTimes(1); + expect(childSpy).toHaveBeenCalledTimes(1); + expect(callOrder).toEqual(['parentCapture', 'child']); + }); - const childRef = React.createRef>(); + it('still bubbles non-skipBubbling events (onPointerDown) to ancestor views', () => { + const root = Fantom.createRoot(); - const callOrder: Array = []; - const parentCaptureSpy = jest.fn((_e: PointerEvent) => { - callOrder.push('parentCapture'); - }); - const childSpy = jest.fn((_e: PointerEvent) => { - callOrder.push('child'); - }); + const childRef = React.createRef>(); - Fantom.runTask(() => { - root.render( - - - , - ); - }); + const parentSpy = jest.fn((_e: PointerEvent) => {}); + const childSpy = jest.fn((_e: PointerEvent) => {}); - Fantom.dispatchNativeEvent( - childRef, - 'onPointerEnter', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.ContinuousStart, - }, + Fantom.runTask(() => { + root.render( + + + , ); - - expect(parentCaptureSpy).toHaveBeenCalledTimes(1); - expect(childSpy).toHaveBeenCalledTimes(1); - expect(callOrder).toEqual(['parentCapture', 'child']); }); - it('still bubbles non-skipBubbling events (onPointerDown) to ancestor views', () => { - const root = Fantom.createRoot(); - - const childRef = React.createRef>(); - - const parentSpy = jest.fn((_e: PointerEvent) => {}); - const childSpy = jest.fn((_e: PointerEvent) => {}); - - Fantom.runTask(() => { - root.render( - - - , - ); - }); - - Fantom.dispatchNativeEvent( - childRef, - 'onPointerDown', - {x: 0, y: 0}, - { - category: Fantom.NativeEventCategory.Discrete, - }, - ); + Fantom.dispatchNativeEvent( + childRef, + 'onPointerDown', + {x: 0, y: 0}, + { + category: Fantom.NativeEventCategory.Discrete, + }, + ); - expect(childSpy).toHaveBeenCalledTimes(1); - expect(parentSpy).toHaveBeenCalledTimes(1); - }); + expect(childSpy).toHaveBeenCalledTimes(1); + expect(parentSpy).toHaveBeenCalledTimes(1); }); - }, -); + }); +}); diff --git a/packages/react-native/src/private/renderer/core/__tests__/ResponderEventTarget-itest.js b/packages/react-native/src/private/renderer/core/__tests__/ResponderEventTarget-itest.js index 58672b16e95..605d90317e3 100644 --- a/packages/react-native/src/private/renderer/core/__tests__/ResponderEventTarget-itest.js +++ b/packages/react-native/src/private/renderer/core/__tests__/ResponderEventTarget-itest.js @@ -46,9 +46,7 @@ function touchEnd( }; } -const {isOSS} = Fantom.getConstants(); - -(isOSS ? describe.skip : describe)('Responder System', () => { +describe('Responder System', () => { // --- Basic Grant / Release --- it('grants responder on touch start when onStartShouldSetResponder returns true', () => {