From e58bbbcd8e89ed9ae955c0d36aa778f9b430d992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Tue, 21 Oct 2025 09:34:49 -0700 Subject: [PATCH] Allow passing refs where nodes are expected (#54221) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/54221 Changelog: [internal] Small change in the Fantom API to accept refs and reduce boilerplate. See next diff for examples Reviewed By: javache Differential Revision: D85143177 fbshipit-source-id: fd65abcc1b107df8726fee172b284f5dc96e88a6 --- .../src/__tests__/Fantom-itest.js | 137 ++++++++++++++++++ private/react-native-fantom/src/index.js | 91 +++++++----- 2 files changed, 194 insertions(+), 34 deletions(-) diff --git a/private/react-native-fantom/src/__tests__/Fantom-itest.js b/private/react-native-fantom/src/__tests__/Fantom-itest.js index 4cde5fb1fcc..f13777543b6 100644 --- a/private/react-native-fantom/src/__tests__/Fantom-itest.js +++ b/private/react-native-fantom/src/__tests__/Fantom-itest.js @@ -584,6 +584,31 @@ describe('Fantom', () => { expect(focusEvent).toHaveBeenCalledTimes(1); }); + it('sends event without payload (with ref)', () => { + const root = Fantom.createRoot(); + + let focusEvent = jest.fn(); + + const ref = createRef(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(focusEvent).toHaveBeenCalledTimes(0); + + Fantom.runOnUIThread(() => { + Fantom.enqueueNativeEvent(ref, 'focus'); + }); + + // The tasks have not run. + expect(focusEvent).toHaveBeenCalledTimes(0); + + Fantom.runWorkLoop(); + + expect(focusEvent).toHaveBeenCalledTimes(1); + }); + it('sends event with payload', () => { const root = Fantom.createRoot(); const ref = createRef(); @@ -692,6 +717,23 @@ describe('Fantom', () => { expect(focusEvent).toHaveBeenCalledTimes(1); }); + + it('flushes the event and runs the work loop (with ref)', () => { + const root = Fantom.createRoot(); + const ref = createRef(); + + let focusEvent = jest.fn(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(focusEvent).toHaveBeenCalledTimes(0); + + Fantom.dispatchNativeEvent(ref, 'focus'); + + expect(focusEvent).toHaveBeenCalledTimes(1); + }); }); describe('enqueueScrollEvent', () => { @@ -783,6 +825,36 @@ describe('Fantom', () => { root.destroy(); }); + + it('delivers onScroll event (with ref)', () => { + const root = Fantom.createRoot(); + const viewRef = createRef(); + const scrollViewRef = createRef(); + const onScroll = jest.fn(); + + Fantom.runTask(() => { + root.render( + { + onScroll(event.nativeEvent); + }} + ref={scrollViewRef}> + + , + ); + }); + + Fantom.runOnUIThread(() => { + Fantom.enqueueScrollEvent(scrollViewRef, { + x: 0, + y: 1, + }); + }); + + Fantom.runWorkLoop(); + + expect(onScroll).toHaveBeenCalledTimes(1); + }); }); describe('scrollTo', () => { @@ -872,6 +944,39 @@ describe('Fantom', () => { root.destroy(); }); + + it('delivers onScroll event and affects position of elements on screen (with ref)', () => { + const root = Fantom.createRoot(); + const scrollViewRef = createRef(); + const viewRef = createRef(); + const onScroll = jest.fn(); + + Fantom.runTask(() => { + root.render( + { + onScroll(event.nativeEvent); + }} + ref={scrollViewRef}> + + , + ); + }); + + const scrollViewElement = ensureInstance( + scrollViewRef.current, + ReactNativeElement, + ); + + expect(scrollViewElement.scrollTop).toBe(0); + + Fantom.scrollTo(scrollViewRef, { + x: 0, + y: 1, + }); + + expect(scrollViewElement.scrollTop).toBe(1); + }); }); describe('flushAllNativeEvents', () => { @@ -956,5 +1061,37 @@ describe('Fantom', () => { expect(boundingClientRect.height).toBe(25); expect(boundingClientRect.width).toBe(50); }); + + it('change size of (with ref)', () => { + const root = Fantom.createRoot(); + const modalNodeRef = createRef(); + const viewNodeRef = createRef(); + + Fantom.runTask(() => { + root.render( + + + , + ); + }); + + Fantom.runOnUIThread(() => { + Fantom.enqueueModalSizeUpdate(modalNodeRef, { + width: 100, + height: 100, + }); + }); + + Fantom.runWorkLoop(); + + const viewElement = ensureInstance( + viewNodeRef.current, + ReactNativeElement, + ); + + const boundingClientRect = viewElement.getBoundingClientRect(); + expect(boundingClientRect.height).toBe(25); + expect(boundingClientRect.width).toBe(50); + }); }); }); diff --git a/private/react-native-fantom/src/index.js b/private/react-native-fantom/src/index.js index 6cebae19ebe..fb72b567393 100644 --- a/private/react-native-fantom/src/index.js +++ b/private/react-native-fantom/src/index.js @@ -15,7 +15,6 @@ import type { import type {MixedElement} from 'react'; import type {RootTag} from 'react-native'; import type ReactNativeDocument from 'react-native/src/private/webapis/dom/nodes/ReactNativeDocument'; -import type ReadOnlyNode from 'react-native/src/private/webapis/dom/nodes/ReadOnlyNode'; import * as Benchmark from './Benchmark'; import {getConstants} from './Constants'; @@ -25,11 +24,14 @@ import NativeFantom, { NativeEventCategory, } from 'react-native/src/private/testing/fantom/specs/NativeFantom'; import {getNativeNodeReference} from 'react-native/src/private/webapis/dom/nodes/internals/NodeInternals'; +import ReadOnlyNode from 'react-native/src/private/webapis/dom/nodes/ReadOnlyNode'; const nativeRuntimeScheduler = global.nativeRuntimeScheduler; const {unstable_scheduleCallback, unstable_ImmediatePriority} = nativeRuntimeScheduler; +type NodeOrRef = ReadOnlyNode | $ReadOnly<{current: ?ReadOnlyNode}>; + export type RootConfig = { viewportWidth?: number, viewportHeight?: number, @@ -218,17 +220,19 @@ export function unstable_produceFramesForDuration(milliseconds: number) { * Note: This API is marked as unstable and may change in future versions. */ export function unstable_getDirectManipulationProps( - node: ReadOnlyNode, + nodeOrRef: NodeOrRef, ): $ReadOnly<{ [string]: mixed, }> { + const node = getNode(nodeOrRef); const shadowNode = getNativeNodeReference(node); return NativeFantom.getDirectManipulationProps(shadowNode); } -export function unstable_getFabricUpdateProps(node: ReadOnlyNode): $ReadOnly<{ +export function unstable_getFabricUpdateProps(nodeOrRef: NodeOrRef): $ReadOnly<{ [string]: mixed, }> { + const node = getNode(nodeOrRef); const shadowNode = getNativeNodeReference(node); return NativeFantom.getFabricUpdateProps(shadowNode); } @@ -394,11 +398,12 @@ export function createRoot(rootConfig?: RootConfig): Root { * ``` */ export function enqueueNativeEvent( - node: ReadOnlyNode, + nodeOrRef: NodeOrRef, type: string, payload?: $ReadOnly<{[key: string]: mixed}>, options?: $ReadOnly<{category?: NativeEventCategory, isUnique?: boolean}>, ) { + const node = getNode(nodeOrRef); const shadowNode = getNativeNodeReference(node); NativeFantom.enqueueNativeEvent( shadowNode, @@ -425,11 +430,13 @@ export function enqueueNativeEvent( * ``` */ export function dispatchNativeEvent( - node: ReadOnlyNode, + nodeOrRef: NodeOrRef, type: string, payload?: $ReadOnly<{[key: string]: mixed}>, options?: $ReadOnly<{category?: NativeEventCategory, isUnique?: boolean}>, ) { + const node = getNode(nodeOrRef); + runOnUIThread(() => { enqueueNativeEvent(node, type, payload, options); }); @@ -487,9 +494,10 @@ export type ScrollEventOptions = { * ``` */ export function enqueueScrollEvent( - node: ReadOnlyNode, + nodeOrRef: NodeOrRef, options: ScrollEventOptions, ) { + const node = getNode(nodeOrRef); const shadowNode = getNativeNodeReference(node); NativeFantom.enqueueScrollEvent(shadowNode, options); } @@ -525,7 +533,9 @@ export function enqueueScrollEvent( * // Assert that changes from Fantom.scrollTo are in effect. * ``` */ -export function scrollTo(node: ReadOnlyNode, options: ScrollEventOptions) { +export function scrollTo(nodeOrRef: NodeOrRef, options: ScrollEventOptions) { + const node = getNode(nodeOrRef); + runOnUIThread(() => { enqueueScrollEvent(node, options); }); @@ -558,9 +568,10 @@ export function scrollTo(node: ReadOnlyNode, options: ScrollEventOptions) { * ``` */ export function enqueueModalSizeUpdate( - node: ReadOnlyNode, + nodeOrRef: NodeOrRef, size: $ReadOnly<{width: number, height: number}>, ) { + const node = getNode(nodeOrRef); const shadowNode = getNativeNodeReference(node); NativeFantom.enqueueModalSizeUpdate(shadowNode, size.width, size.height); } @@ -572,28 +583,6 @@ export type { TestOptions as BenchmarkTestOptions, } from './Benchmark'; -/** - * Quick and dirty polyfills required by tinybench. - */ - -if (typeof global.Event === 'undefined') { - global.Event = - require('react-native/src/private/webapis/dom/events/Event').default; -} else { - console.warn( - 'The global Event class is already defined. If this API is already defined by React Native, you might want to remove this logic.', - ); -} - -if (typeof global.EventTarget === 'undefined') { - global.EventTarget = - require('react-native/src/private/webapis/dom/events/EventTarget').default; -} else { - console.warn( - 'The global Event class is already defined. If this API is already defined by React Native, you might want to remove this logic.', - ); -} - /** * Returns a function that returns the current reference count for the supplied * element's shadow node. If the reference count is zero, that means the shadow @@ -602,9 +591,10 @@ if (typeof global.EventTarget === 'undefined') { * @param node The node for which to create a reference counting function. */ export function createShadowNodeReferenceCounter( - node: ReadOnlyNode, + nodeOrRef: NodeOrRef, ): () => number { - let shadowNode = getNativeNodeReference(node); + const node = getNode(nodeOrRef); + const shadowNode = getNativeNodeReference(node); return NativeFantom.createShadowNodeReferenceCounter(shadowNode); } @@ -615,9 +605,10 @@ export function createShadowNodeReferenceCounter( * @param node The node for which to create a revision getter. */ export function createShadowNodeRevisionGetter( - node: ReadOnlyNode, + nodeOrRef: NodeOrRef, ): () => ?number { - let shadowNode = getNativeNodeReference(node); + const node = getNode(nodeOrRef); + const shadowNode = getNativeNodeReference(node); return NativeFantom.createShadowNodeRevisionGetter(shadowNode); } @@ -681,4 +672,36 @@ function runLogBoxCheck() { } } +function getNode(nodeOrRef: NodeOrRef): ReadOnlyNode { + if (nodeOrRef instanceof ReadOnlyNode) { + return nodeOrRef; + } else if (nodeOrRef.current != null) { + return nodeOrRef.current; + } else { + throw new TypeError('Could not get node from ref'); + } +} + +/** + * Quick and dirty polyfills required by tinybench. + */ + +if (typeof global.Event === 'undefined') { + global.Event = + require('react-native/src/private/webapis/dom/events/Event').default; +} else { + console.warn( + 'The global Event class is already defined. If this API is already defined by React Native, you might want to remove this logic.', + ); +} + +if (typeof global.EventTarget === 'undefined') { + global.EventTarget = + require('react-native/src/private/webapis/dom/events/EventTarget').default; +} else { + console.warn( + 'The global Event class is already defined. If this API is already defined by React Native, you might want to remove this logic.', + ); +} + global.__FANTOM_PACKAGE_LOADED__ = true;