diff --git a/packages/react-native-renderer/src/ReactFabric.js b/packages/react-native-renderer/src/ReactFabric.js index 84d5651f83be1..a6e2bd883b84f 100644 --- a/packages/react-native-renderer/src/ReactFabric.js +++ b/packages/react-native-renderer/src/ReactFabric.js @@ -38,6 +38,7 @@ import { findNodeHandle, dispatchCommand, sendAccessibilityEvent, + getNodeFromInternalInstanceHandle, } from './ReactNativePublicCompat'; // $FlowFixMe[missing-local-annot] @@ -119,6 +120,10 @@ export { // This export is typically undefined in production builds. // See the "enableGetInspectorDataForInstanceInProduction" flag. getInspectorDataForInstance, + // The public instance has a reference to the internal instance handle. + // This method allows it to acess the most recent shadow node for + // the instance (it's only accessible through it). + getNodeFromInternalInstanceHandle, }; injectIntoDevTools({ diff --git a/packages/react-native-renderer/src/ReactFabricComponentTree.js b/packages/react-native-renderer/src/ReactFabricComponentTree.js index 4cdb1ca81505c..c295c42ed102b 100644 --- a/packages/react-native-renderer/src/ReactFabricComponentTree.js +++ b/packages/react-native-renderer/src/ReactFabricComponentTree.js @@ -20,6 +20,15 @@ import {getPublicInstance} from './ReactFabricHostConfig'; // This is ok in DOM because they types are interchangeable, but in React Native // they aren't. function getInstanceFromNode(node: Instance | TextInstance): Fiber | null { + const instance: Instance = (node: $FlowFixMe); // In React Native, node is never a text instance + + if ( + instance.canonical != null && + instance.canonical.internalInstanceHandle != null + ) { + return instance.canonical.internalInstanceHandle; + } + // $FlowFixMe[incompatible-return] DevTools incorrectly passes a fiber in React Native. return node; } diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 717a496453eda..689dabcfeb862 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -7,22 +7,13 @@ * @flow */ -import type {ElementRef} from 'react'; -import type { - HostComponent, - MeasureInWindowOnSuccessCallback, - MeasureLayoutOnSuccessCallback, - MeasureOnSuccessCallback, - INativeMethods, - ViewConfig, - TouchedViewDataAtPoint, -} from './ReactNativeTypes'; - -import {warnForStyleProps} from './NativeMethodsMixinUtils'; +import type {TouchedViewDataAtPoint, ViewConfig} from './ReactNativeTypes'; +import { + createPublicInstance, + type ReactFabricHostComponent, +} from './ReactFabricPublicInstance'; import {create, diff} from './ReactNativeAttributePayload'; - import {dispatchEvent} from './ReactFabricEventEmitter'; - import { DefaultEventPriority, DiscreteEventPriority, @@ -31,7 +22,6 @@ import { // Modules provided by RN: import { ReactNativeViewConfigRegistry, - TextInputState, deepFreezeAndThrowOnMutationInDev, } from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; @@ -46,14 +36,9 @@ const { appendChildToSet: appendChildNodeToSet, completeRoot, registerEventHandler, - measure: fabricMeasure, - measureInWindow: fabricMeasureInWindow, - measureLayout: fabricMeasureLayout, unstable_DefaultEventPriority: FabricDefaultPriority, unstable_DiscreteEventPriority: FabricDiscretePriority, unstable_getCurrentEventPriority: fabricGetCurrentEventPriority, - setNativeProps, - getBoundingClientRect: fabricGetBoundingClientRect, } = nativeFabricUIManager; const {get: getViewConfigForType} = ReactNativeViewConfigRegistry; @@ -68,9 +53,17 @@ type Node = Object; export type Type = string; export type Props = Object; export type Instance = { + // Reference to the shadow node. node: Node, - canonical: ReactFabricHostComponent, - ... + canonical: { + nativeTag: number, + viewConfig: ViewConfig, + currentProps: Props, + // Reference to the React handle (the fiber) + internalInstanceHandle: Object, + // Exposed through refs. + publicInstance: ReactFabricHostComponent, + }, }; export type TextInstance = {node: Node, ...}; export type HydratableInstance = Instance | TextInstance; @@ -104,137 +97,6 @@ if (registerEventHandler) { registerEventHandler(dispatchEvent); } -const noop = () => {}; - -/** - * This is used for refs on host components. - */ -class ReactFabricHostComponent implements INativeMethods { - _nativeTag: number; - viewConfig: ViewConfig; - currentProps: Props; - _internalInstanceHandle: Object; - - constructor( - tag: number, - viewConfig: ViewConfig, - props: Props, - internalInstanceHandle: Object, - ) { - this._nativeTag = tag; - this.viewConfig = viewConfig; - this.currentProps = props; - this._internalInstanceHandle = internalInstanceHandle; - } - - blur() { - TextInputState.blurTextInput(this); - } - - focus() { - TextInputState.focusTextInput(this); - } - - measure(callback: MeasureOnSuccessCallback) { - const node = getShadowNodeFromInternalInstanceHandle( - this._internalInstanceHandle, - ); - if (node != null) { - fabricMeasure(node, callback); - } - } - - measureInWindow(callback: MeasureInWindowOnSuccessCallback) { - const node = getShadowNodeFromInternalInstanceHandle( - this._internalInstanceHandle, - ); - if (node != null) { - fabricMeasureInWindow(node, callback); - } - } - - measureLayout( - relativeToNativeNode: number | ElementRef>, - onSuccess: MeasureLayoutOnSuccessCallback, - onFail?: () => void /* currently unused */, - ) { - if ( - typeof relativeToNativeNode === 'number' || - !(relativeToNativeNode instanceof ReactFabricHostComponent) - ) { - if (__DEV__) { - console.error( - 'Warning: ref.measureLayout must be called with a ref to a native component.', - ); - } - - return; - } - - const toStateNode = getShadowNodeFromInternalInstanceHandle( - this._internalInstanceHandle, - ); - const fromStateNode = getShadowNodeFromInternalInstanceHandle( - relativeToNativeNode._internalInstanceHandle, - ); - - if (toStateNode != null && fromStateNode != null) { - fabricMeasureLayout( - toStateNode, - fromStateNode, - onFail != null ? onFail : noop, - onSuccess != null ? onSuccess : noop, - ); - } - } - - unstable_getBoundingClientRect(): DOMRect { - const node = getShadowNodeFromInternalInstanceHandle( - this._internalInstanceHandle, - ); - if (node != null) { - const rect = fabricGetBoundingClientRect(node); - - if (rect) { - return new DOMRect(rect[0], rect[1], rect[2], rect[3]); - } - } - - // Empty rect if any of the above failed - return new DOMRect(0, 0, 0, 0); - } - - setNativeProps(nativeProps: Object) { - if (__DEV__) { - warnForStyleProps(nativeProps, this.viewConfig.validAttributes); - } - const updatePayload = create(nativeProps, this.viewConfig.validAttributes); - - const node = getShadowNodeFromInternalInstanceHandle( - this._internalInstanceHandle, - ); - if (node != null && updatePayload != null) { - setNativeProps(node, updatePayload); - } - } -} - -type ParamOf = $Call<((arg: T) => mixed) => T, Fn>; -type ShadowNode = ParamOf<(typeof nativeFabricUIManager)['measure']>; - -export function getShadowNodeFromInternalInstanceHandle( - internalInstanceHandle: mixed, -): ?ShadowNode { - return ( - // $FlowExpectedError[incompatible-return] internalInstanceHandle is opaque but we need to make an exception here. - internalInstanceHandle && - // $FlowExpectedError[incompatible-return] - internalInstanceHandle.stateNode && - // $FlowExpectedError[incompatible-use] - internalInstanceHandle.stateNode.node - ); -} - export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMutation'; export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration'; export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes'; @@ -280,16 +142,21 @@ export function createInstance( internalInstanceHandle, // internalInstanceHandle ); - const component = new ReactFabricHostComponent( + const component = createPublicInstance( tag, viewConfig, - props, internalInstanceHandle, ); return { node: node, - canonical: component, + canonical: { + nativeTag: tag, + viewConfig, + currentProps: props, + internalInstanceHandle, + publicInstance: component, + }, }; } @@ -359,12 +226,15 @@ export function getChildHostContext( } export function getPublicInstance(instance: Instance): null | PublicInstance { - if (instance.canonical) { - return instance.canonical; + if (instance.canonical != null && instance.canonical.publicInstance != null) { + return instance.canonical.publicInstance; } - // For compatibility with Paper + // For compatibility with the legacy renderer, in case it's used with Fabric + // in the same app. + // $FlowExpectedError[prop-missing] if (instance._nativeTag != null) { + // $FlowExpectedError[incompatible-return] return instance; } diff --git a/packages/react-native-renderer/src/ReactFabricPublicInstance.js b/packages/react-native-renderer/src/ReactFabricPublicInstance.js new file mode 100644 index 0000000000000..282e5af7cde1e --- /dev/null +++ b/packages/react-native-renderer/src/ReactFabricPublicInstance.js @@ -0,0 +1,153 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + */ + +import type {ElementRef} from 'react'; +import type { + ViewConfig, + INativeMethods, + HostComponent, + MeasureInWindowOnSuccessCallback, + MeasureLayoutOnSuccessCallback, + MeasureOnSuccessCallback, +} from './ReactNativeTypes'; + +import {TextInputState} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; +import {create} from './ReactNativeAttributePayload'; +import {warnForStyleProps} from './NativeMethodsMixinUtils'; +import {getNodeFromInternalInstanceHandle} from './ReactNativePublicCompat'; + +const { + measure: fabricMeasure, + measureInWindow: fabricMeasureInWindow, + measureLayout: fabricMeasureLayout, + setNativeProps, + getBoundingClientRect: fabricGetBoundingClientRect, +} = nativeFabricUIManager; + +const noop = () => {}; + +/** + * This is used for refs on host components. + */ +export class ReactFabricHostComponent implements INativeMethods { + // These need to be accessible from `ReactFabricPublicInstanceUtils`. + __nativeTag: number; + __internalInstanceHandle: mixed; + + _viewConfig: ViewConfig; + + constructor( + tag: number, + viewConfig: ViewConfig, + internalInstanceHandle: mixed, + ) { + this.__nativeTag = tag; + this._viewConfig = viewConfig; + this.__internalInstanceHandle = internalInstanceHandle; + } + + blur() { + TextInputState.blurTextInput(this); + } + + focus() { + TextInputState.focusTextInput(this); + } + + measure(callback: MeasureOnSuccessCallback) { + const node = getNodeFromInternalInstanceHandle( + this.__internalInstanceHandle, + ); + if (node != null) { + fabricMeasure(node, callback); + } + } + + measureInWindow(callback: MeasureInWindowOnSuccessCallback) { + const node = getNodeFromInternalInstanceHandle( + this.__internalInstanceHandle, + ); + if (node != null) { + fabricMeasureInWindow(node, callback); + } + } + + measureLayout( + relativeToNativeNode: number | ElementRef>, + onSuccess: MeasureLayoutOnSuccessCallback, + onFail?: () => void /* currently unused */, + ) { + if ( + typeof relativeToNativeNode === 'number' || + !(relativeToNativeNode instanceof ReactFabricHostComponent) + ) { + if (__DEV__) { + console.error( + 'Warning: ref.measureLayout must be called with a ref to a native component.', + ); + } + + return; + } + + const toStateNode = getNodeFromInternalInstanceHandle( + this.__internalInstanceHandle, + ); + const fromStateNode = getNodeFromInternalInstanceHandle( + relativeToNativeNode.__internalInstanceHandle, + ); + + if (toStateNode != null && fromStateNode != null) { + fabricMeasureLayout( + toStateNode, + fromStateNode, + onFail != null ? onFail : noop, + onSuccess != null ? onSuccess : noop, + ); + } + } + + unstable_getBoundingClientRect(): DOMRect { + const node = getNodeFromInternalInstanceHandle( + this.__internalInstanceHandle, + ); + if (node != null) { + const rect = fabricGetBoundingClientRect(node); + + if (rect) { + return new DOMRect(rect[0], rect[1], rect[2], rect[3]); + } + } + + // Empty rect if any of the above failed + return new DOMRect(0, 0, 0, 0); + } + + setNativeProps(nativeProps: {...}): void { + if (__DEV__) { + warnForStyleProps(nativeProps, this._viewConfig.validAttributes); + } + const updatePayload = create(nativeProps, this._viewConfig.validAttributes); + + const node = getNodeFromInternalInstanceHandle( + this.__internalInstanceHandle, + ); + if (node != null && updatePayload != null) { + setNativeProps(node, updatePayload); + } + } +} + +export function createPublicInstance( + tag: number, + viewConfig: ViewConfig, + internalInstanceHandle: mixed, +): ReactFabricHostComponent { + return new ReactFabricHostComponent(tag, viewConfig, internalInstanceHandle); +} diff --git a/packages/react-native-renderer/src/ReactFabricPublicInstanceUtils.js b/packages/react-native-renderer/src/ReactFabricPublicInstanceUtils.js new file mode 100644 index 0000000000000..a1d9fbea4dcc5 --- /dev/null +++ b/packages/react-native-renderer/src/ReactFabricPublicInstanceUtils.js @@ -0,0 +1,29 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + */ + +import type {ReactFabricHostComponent} from './ReactFabricPublicInstance'; + +/** + * IMPORTANT: This module is used in Paper and Fabric. It needs to be defined + * outside of `ReactFabricPublicInstance` because that module requires + * `nativeFabricUIManager` to be defined in the global scope (which does not + * happen in Paper). + */ + +export function getNativeTagFromPublicInstance( + publicInstance: ReactFabricHostComponent, +): number { + return publicInstance.__nativeTag; +} + +export function getInternalInstanceHandleFromPublicInstance( + publicInstance: ReactFabricHostComponent, +): mixed { + return publicInstance.__internalInstanceHandle; +} diff --git a/packages/react-native-renderer/src/ReactNativeComponentTree.js b/packages/react-native-renderer/src/ReactNativeComponentTree.js index 3af3fa5a443f5..bd3f1f44146af 100644 --- a/packages/react-native-renderer/src/ReactNativeComponentTree.js +++ b/packages/react-native-renderer/src/ReactNativeComponentTree.js @@ -24,9 +24,10 @@ function getInstanceFromTag(tag) { function getTagFromInstance(inst) { let nativeInstance = inst.stateNode; let tag = nativeInstance._nativeTag; - if (tag === undefined) { - nativeInstance = nativeInstance.canonical; - tag = nativeInstance._nativeTag; + if (tag === undefined && nativeInstance.canonical != null) { + // For compatibility with Fabric + tag = nativeInstance.canonical.nativeTag; + nativeInstance = nativeInstance.canonical.publicInstance; } if (!tag) { diff --git a/packages/react-native-renderer/src/ReactNativeFiberInspector.js b/packages/react-native-renderer/src/ReactNativeFiberInspector.js index d977236a32558..94f9abf35f9a2 100644 --- a/packages/react-native-renderer/src/ReactNativeFiberInspector.js +++ b/packages/react-native-renderer/src/ReactNativeFiberInspector.js @@ -20,6 +20,8 @@ import {HostComponent} from 'react-reconciler/src/ReactWorkTags'; import {UIManager} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; import {enableGetInspectorDataForInstanceInProduction} from 'shared/ReactFeatureFlags'; import {getClosestInstanceFromNode} from './ReactNativeComponentTree'; +import {getInternalInstanceHandleFromPublicInstance} from './ReactFabricPublicInstanceUtils'; +import {getNodeFromInternalInstanceHandle} from './ReactNativePublicCompat'; const emptyObject = {}; if (__DEV__) { @@ -35,15 +37,15 @@ function createHierarchy(fiberHierarchy) { props: getHostProps(fiber), source: fiber._debugSource, measure: callback => { - // If this is Fabric, we'll find a ShadowNode and use that to measure. + // If this is Fabric, we'll find a shadow node and use that to measure. const hostFiber = findCurrentHostFiber(fiber); - const shadowNode = + const node = hostFiber != null && hostFiber.stateNode !== null && hostFiber.stateNode.node; - if (shadowNode) { - nativeFabricUIManager.measure(shadowNode, callback); + if (node) { + nativeFabricUIManager.measure(node, callback); } else { return UIManager.measure( getHostNode(fiber, findNodeHandle), @@ -194,30 +196,41 @@ function getInspectorDataForViewAtPoint( if (__DEV__) { let closestInstance = null; - if (inspectedView._internalInstanceHandle != null) { + const fabricInstanceHandle = + getInternalInstanceHandleFromPublicInstance(inspectedView); + const fabricNode = + fabricInstanceHandle != null + ? getNodeFromInternalInstanceHandle(fabricInstanceHandle) + : null; + if (fabricNode) { // For Fabric we can look up the instance handle directly and measure it. nativeFabricUIManager.findNodeAtPoint( - inspectedView._internalInstanceHandle.stateNode.node, + fabricNode, locationX, locationY, internalInstanceHandle => { - if (internalInstanceHandle == null) { + const node = + internalInstanceHandle != null + ? getNodeFromInternalInstanceHandle(internalInstanceHandle) + : null; + if (internalInstanceHandle == null || node == null) { callback({ pointerY: locationY, frame: {left: 0, top: 0, width: 0, height: 0}, ...getInspectorDataForInstance(closestInstance), }); + return; } closestInstance = - internalInstanceHandle.stateNode.canonical._internalInstanceHandle; + internalInstanceHandle.stateNode.canonical.internalInstanceHandle; // Note: this is deprecated and we want to remove it ASAP. Keeping it here for React DevTools compatibility for now. const nativeViewTag = - internalInstanceHandle.stateNode.canonical._nativeTag; + internalInstanceHandle.stateNode.canonical.nativeTag; nativeFabricUIManager.measure( - internalInstanceHandle.stateNode.node, + node, (x, y, width, height, pageX, pageY) => { const inspectorData = getInspectorDataForInstance(closestInstance); diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js index a3b58315205f3..501d343b1985f 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -218,8 +218,8 @@ export function getChildHostContext( export function getPublicInstance(instance: Instance): * { // $FlowExpectedError[prop-missing] For compatibility with Fabric - if (instance.canonical) { - return instance.canonical; + if (instance.canonical != null && instance.canonical.publicInstance != null) { + return instance.canonical.publicInstance; } return instance; diff --git a/packages/react-native-renderer/src/ReactNativePublicCompat.js b/packages/react-native-renderer/src/ReactNativePublicCompat.js index a6be670d912cb..a2979666d7e14 100644 --- a/packages/react-native-renderer/src/ReactNativePublicCompat.js +++ b/packages/react-native-renderer/src/ReactNativePublicCompat.js @@ -7,7 +7,7 @@ * @flow */ -import type {HostComponent} from './ReactNativeTypes'; +import type {Node, HostComponent} from './ReactNativeTypes'; import type {ElementRef, ElementType} from 'react'; // Modules provided by RN: @@ -23,6 +23,11 @@ import { import ReactSharedInternals from 'shared/ReactSharedInternals'; import getComponentNameFromType from 'shared/getComponentNameFromType'; +import { + getInternalInstanceHandleFromPublicInstance, + getNativeTagFromPublicInstance, +} from './ReactFabricPublicInstanceUtils'; + const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; export function findHostInstance_DEPRECATED( @@ -45,19 +50,27 @@ export function findHostInstance_DEPRECATED( owner.stateNode._warnedAboutRefsInRender = true; } } + if (componentOrHandle == null) { return null; } - // $FlowFixMe Flow has hardcoded values for React DOM that don't work with RN + + // For compatibility with Fabric instances + if ( + componentOrHandle.canonical && + componentOrHandle.canonical.publicInstance + ) { + // $FlowExpectedError[incompatible-return] Can't refine componentOrHandle as a Fabric instance + return componentOrHandle.canonical.publicInstance; + } + + // For compatibility with legacy renderer instances if (componentOrHandle._nativeTag) { - // $FlowFixMe Flow has hardcoded values for React DOM that don't work with RN + // $FlowFixMe[incompatible-exact] Necessary when running Flow on Fabric + // $FlowFixMe[incompatible-return] return componentOrHandle; } - // $FlowFixMe Flow has hardcoded values for React DOM that don't work with RN - if (componentOrHandle.canonical && componentOrHandle.canonical._nativeTag) { - // $FlowFixMe Flow has hardcoded values for React DOM that don't work with RN - return componentOrHandle.canonical; - } + let hostInstance; if (__DEV__) { hostInstance = findHostInstanceWithWarning( @@ -68,6 +81,7 @@ export function findHostInstance_DEPRECATED( hostInstance = findHostInstance(componentOrHandle); } + // findHostInstance handles legacy vs. Fabric differences correctly // $FlowFixMe[incompatible-exact] we need to fix the definition of `HostComponent` to use NativeMethods as an interface, not as a type. return hostInstance; } @@ -90,19 +104,35 @@ export function findNodeHandle(componentOrHandle: any): ?number { owner.stateNode._warnedAboutRefsInRender = true; } } + if (componentOrHandle == null) { return null; } + if (typeof componentOrHandle === 'number') { // Already a node handle return componentOrHandle; } + + // For compatibility with legacy renderer instances if (componentOrHandle._nativeTag) { return componentOrHandle._nativeTag; } - if (componentOrHandle.canonical && componentOrHandle.canonical._nativeTag) { - return componentOrHandle.canonical._nativeTag; + + // For compatibility with Fabric instances + if ( + componentOrHandle.canonical != null && + componentOrHandle.canonical.nativeTag != null + ) { + return componentOrHandle.canonical.nativeTag; } + + // For compatibility with Fabric public instances + const nativeTag = getNativeTagFromPublicInstance(componentOrHandle); + if (nativeTag) { + return nativeTag; + } + let hostInstance; if (__DEV__) { hostInstance = findHostInstanceWithWarning( @@ -117,7 +147,14 @@ export function findNodeHandle(componentOrHandle: any): ?number { return hostInstance; } - return hostInstance._nativeTag; + // $FlowFixMe[prop-missing] For compatibility with legacy renderer instances + if (hostInstance._nativeTag != null) { + // $FlowFixMe[incompatible-return] + return hostInstance._nativeTag; + } + + // $FlowFixMe[incompatible-call] Necessary when running Flow on the legacy renderer + return getNativeTagFromPublicInstance(hostInstance); } export function dispatchCommand( @@ -125,7 +162,11 @@ export function dispatchCommand( command: string, args: Array, ) { - if (handle._nativeTag == null) { + const nativeTag = + handle._nativeTag != null + ? handle._nativeTag + : getNativeTagFromPublicInstance(handle); + if (nativeTag == null) { if (__DEV__) { console.error( "dispatchCommand was called with a ref that isn't a " + @@ -135,18 +176,25 @@ export function dispatchCommand( return; } - if (handle._internalInstanceHandle != null) { - const {stateNode} = handle._internalInstanceHandle; - if (stateNode != null) { - nativeFabricUIManager.dispatchCommand(stateNode.node, command, args); + const internalInstanceHandle = + getInternalInstanceHandleFromPublicInstance(handle); + + if (internalInstanceHandle != null) { + const node = getNodeFromInternalInstanceHandle(internalInstanceHandle); + if (node != null) { + nativeFabricUIManager.dispatchCommand(node, command, args); } } else { - UIManager.dispatchViewManagerCommand(handle._nativeTag, command, args); + UIManager.dispatchViewManagerCommand(nativeTag, command, args); } } export function sendAccessibilityEvent(handle: any, eventType: string) { - if (handle._nativeTag == null) { + const nativeTag = + handle._nativeTag != null + ? handle._nativeTag + : getNativeTagFromPublicInstance(handle); + if (nativeTag == null) { if (__DEV__) { console.error( "sendAccessibilityEvent was called with a ref that isn't a " + @@ -156,12 +204,27 @@ export function sendAccessibilityEvent(handle: any, eventType: string) { return; } - if (handle._internalInstanceHandle != null) { - const {stateNode} = handle._internalInstanceHandle; - if (stateNode != null) { - nativeFabricUIManager.sendAccessibilityEvent(stateNode.node, eventType); + const internalInstanceHandle = + getInternalInstanceHandleFromPublicInstance(handle); + if (internalInstanceHandle != null) { + const node = getNodeFromInternalInstanceHandle(internalInstanceHandle); + if (node != null) { + nativeFabricUIManager.sendAccessibilityEvent(node, eventType); } } else { - legacySendAccessibilityEvent(handle._nativeTag, eventType); + legacySendAccessibilityEvent(nativeTag, eventType); } } + +export function getNodeFromInternalInstanceHandle( + internalInstanceHandle: mixed, +): ?Node { + return ( + // $FlowExpectedError[incompatible-return] internalInstanceHandle is opaque but we need to make an exception here. + internalInstanceHandle && + // $FlowExpectedError[incompatible-return] + internalInstanceHandle.stateNode && + // $FlowExpectedError[incompatible-use] + internalInstanceHandle.stateNode.node + ); +} diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js index a524d4313866c..ca9a9dfc04d03 100644 --- a/packages/react-native-renderer/src/ReactNativeTypes.js +++ b/packages/react-native-renderer/src/ReactNativeTypes.js @@ -212,6 +212,8 @@ export type ReactNativeType = { ... }; +export opaque type Node = mixed; + export type ReactFabricType = { findHostInstance_DEPRECATED( componentOrHandle: ?(ElementRef | number), @@ -235,6 +237,7 @@ export type ReactFabricType = { concurrentRoot: ?boolean, ): ?ElementRef, unmountComponentAtNode(containerTag: number): void, + getNodeFromInternalInstanceHandle(internalInstanceHandle: mixed): ?Node, ... }; diff --git a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js index 935edb0335def..42ed69a8c290c 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js @@ -15,6 +15,8 @@ let ReactFabric; let createReactNativeComponentClass; let StrictMode; let act; +let getNativeTagFromPublicInstance; +let getInternalInstanceHandleFromPublicInstance; const DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT = "Warning: dispatchCommand was called with a ref that isn't a " + @@ -40,6 +42,10 @@ describe('ReactFabric', () => { createReactNativeComponentClass = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface') .ReactNativeViewConfigRegistry.register; + getNativeTagFromPublicInstance = + require('../ReactFabricPublicInstanceUtils').getNativeTagFromPublicInstance; + getInternalInstanceHandleFromPublicInstance = + require('../ReactFabricPublicInstanceUtils').getInternalInstanceHandleFromPublicInstance; act = require('internal-test-utils').act; }); @@ -931,7 +937,7 @@ describe('ReactFabric', () => { '\n in RCTView (at **)' + '\n in ContainsStrictModeChild (at **)', ]); - expect(match).toBe(child._nativeTag); + expect(match).toBe(getNativeTagFromPublicInstance(child)); }); it('findNodeHandle should warn if passed a component that is inside StrictMode', async () => { @@ -968,7 +974,7 @@ describe('ReactFabric', () => { '\n in RCTView (at **)' + '\n in IsInStrictMode (at **)', ]); - expect(match).toBe(child._nativeTag); + expect(match).toBe(getNativeTagFromPublicInstance(child)); }); it('should no-op if calling sendAccessibilityEvent on unmounted refs', async () => { @@ -1002,4 +1008,34 @@ describe('ReactFabric', () => { expect(nativeFabricUIManager.sendAccessibilityEvent).not.toBeCalled(); }); + + it('getNodeFromInternalInstanceHandle should return the correct shadow node', async () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + let viewRef; + await act(() => { + ReactFabric.render( + { + viewRef = ref; + }} + />, + 1, + ); + }); + + const expectedShadowNode = + nativeFabricUIManager.createNode.mock.results[0].value; + expect(expectedShadowNode).toEqual(expect.any(Object)); + + const internalInstanceHandle = + getInternalInstanceHandleFromPublicInstance(viewRef); + expect( + ReactFabric.getNodeFromInternalInstanceHandle(internalInstanceHandle), + ).toBe(expectedShadowNode); + }); }); diff --git a/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js index e494701dd0038..fca6d00c72774 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js @@ -16,6 +16,7 @@ let ReactNative; let UIManager; let createReactNativeComponentClass; let ReactNativePrivateInterface; +let getNativeTagFromPublicInstance; describe('created with ReactFabric called with ReactNative', () => { beforeEach(() => { @@ -35,6 +36,8 @@ describe('created with ReactFabric called with ReactNative', () => { createReactNativeComponentClass = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface') .ReactNativeViewConfigRegistry.register; + getNativeTagFromPublicInstance = + require('../ReactFabricPublicInstanceUtils').getNativeTagFromPublicInstance; }); it('find Fabric instances with the RN renderer', () => { @@ -54,7 +57,7 @@ describe('created with ReactFabric called with ReactNative', () => { ReactFabric.render(, 11); const instance = ReactNative.findHostInstance_DEPRECATED(ref.current); - expect(instance._nativeTag).toBe(2); + expect(getNativeTagFromPublicInstance(instance)).toBe(2); }); it('find Fabric nodes with the RN renderer', () => { diff --git a/scripts/flow/react-native-host-hooks.js b/scripts/flow/react-native-host-hooks.js index e3899db44528d..7cb3e389126c5 100644 --- a/scripts/flow/react-native-host-hooks.js +++ b/scripts/flow/react-native-host-hooks.js @@ -178,19 +178,19 @@ declare var nativeFabricUIManager: { dispatchCommand: (node: Object, command: string, args: Array) => void, sendAccessibilityEvent: (node: Object, eventTypeName: string) => void, - measure: (node: Node, callback: __MeasureOnSuccessCallback) => void, + measure: (node: Object, callback: __MeasureOnSuccessCallback) => void, measureInWindow: ( - node: Node, + node: Object, callback: __MeasureInWindowOnSuccessCallback, ) => void, measureLayout: ( - node: Node, - relativeNode: Node, + node: Object, + relativeNode: Object, onFail: () => void, onSuccess: __MeasureLayoutOnSuccessCallback, ) => void, getBoundingClientRect: ( - node: Node, + node: Object, ) => [ /* x:*/ number, /* y:*/ number, @@ -198,13 +198,13 @@ declare var nativeFabricUIManager: { /* height:*/ number, ], findNodeAtPoint: ( - node: Node, + node: Object, locationX: number, locationY: number, callback: (Object) => void, ) => void, setIsJSResponder: ( - node: Node, + node: Object, isJsResponder: boolean, blockNativeResponder: boolean, ) => void,