From bf351089a0ee27405b3d5c0f474941a4946f2f37 Mon Sep 17 00:00:00 2001 From: Ricky Date: Wed, 11 Mar 2020 16:12:41 +0000 Subject: [PATCH] [React Native] Add getInspectorDataForViewAtPoint (#18233) --- .../react-native-renderer/src/ReactFabric.js | 9 +- .../src/ReactNativeFiberHostComponent.js | 8 +- .../src/ReactNativeFiberInspector.js | 146 +++++++++++++++++- .../src/ReactNativeHostConfig.js | 6 +- .../src/ReactNativeRenderer.js | 9 +- .../src/ReactNativeTypes.js | 40 +++++ .../src/ReactFiberReconciler.js | 8 + scripts/error-codes/codes.json | 3 +- scripts/flow/react-native-host-hooks.js | 18 +++ 9 files changed, 235 insertions(+), 12 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFabric.js b/packages/react-native-renderer/src/ReactFabric.js index c5d49f81283d7..cdd338dec2b3e 100644 --- a/packages/react-native-renderer/src/ReactFabric.js +++ b/packages/react-native-renderer/src/ReactFabric.js @@ -34,7 +34,10 @@ import ReactVersion from 'shared/ReactVersion'; import {UIManager} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; import {getClosestInstanceFromNode} from './ReactFabricComponentTree'; -import {getInspectorDataForViewTag} from './ReactNativeFiberInspector'; +import { + getInspectorDataForViewAtPoint, + getInspectorDataForViewTag, +} from './ReactNativeFiberInspector'; import {LegacyRoot} from 'shared/ReactRootTags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; @@ -233,6 +236,10 @@ export { injectIntoDevTools({ findFiberByHostInstance: getClosestInstanceFromNode, getInspectorDataForViewTag: getInspectorDataForViewTag, + getInspectorDataForViewAtPoint: getInspectorDataForViewAtPoint.bind( + null, + findNodeHandle, + ), bundleType: __DEV__ ? 1 : 0, version: ReactVersion, rendererPackageName: 'react-native-renderer', diff --git a/packages/react-native-renderer/src/ReactNativeFiberHostComponent.js b/packages/react-native-renderer/src/ReactNativeFiberHostComponent.js index f695971f4db49..1bc9aabc16658 100644 --- a/packages/react-native-renderer/src/ReactNativeFiberHostComponent.js +++ b/packages/react-native-renderer/src/ReactNativeFiberHostComponent.js @@ -34,11 +34,17 @@ class ReactNativeFiberHostComponent { _children: Array; _nativeTag: number; viewConfig: ReactNativeBaseComponentViewConfig<>; + _internalFiberInstanceHandle: Object; - constructor(tag: number, viewConfig: ReactNativeBaseComponentViewConfig<>) { + constructor( + tag: number, + viewConfig: ReactNativeBaseComponentViewConfig<>, + internalInstanceHandle: Object, + ) { this._nativeTag = tag; this._children = []; this.viewConfig = viewConfig; + this._internalFiberInstanceHandle = internalInstanceHandle; } blur() { diff --git a/packages/react-native-renderer/src/ReactNativeFiberInspector.js b/packages/react-native-renderer/src/ReactNativeFiberInspector.js index bdd4cdef78280..658da640024d1 100644 --- a/packages/react-native-renderer/src/ReactNativeFiberInspector.js +++ b/packages/react-native-renderer/src/ReactNativeFiberInspector.js @@ -8,6 +8,7 @@ */ import type {Fiber} from 'react-reconciler/src/ReactFiber'; +import type {TouchedViewDataAtPoint, InspectorData} from './ReactNativeTypes'; import { findCurrentHostFiber, @@ -27,6 +28,7 @@ if (__DEV__) { } let getInspectorDataForViewTag; +let getInspectorDataForViewAtPoint; if (__DEV__) { const traverseOwnerTreeUp = function(hierarchy, instance: any) { @@ -80,15 +82,68 @@ if (__DEV__) { const createHierarchy = function(fiberHierarchy) { return fiberHierarchy.map(fiber => ({ name: getComponentName(fiber.type), - getInspectorData: findNodeHandle => ({ - measure: callback => - UIManager.measure(getHostNode(fiber, findNodeHandle), callback), - props: getHostProps(fiber), - source: fiber._debugSource, - }), + getInspectorData: findNodeHandle => { + return { + props: getHostProps(fiber), + source: fiber._debugSource, + measure: callback => { + // If this is Fabric, we'll find a ShadowNode and use that to measure. + const hostFiber = findCurrentHostFiber(fiber); + const shadowNode = + hostFiber != null && + hostFiber.stateNode !== null && + hostFiber.stateNode.node; + + if (shadowNode) { + nativeFabricUIManager.measure(shadowNode, function( + x, + y, + width, + height, + pageX, + pageY, + ) { + callback(x, y, width, height, pageX, pageY); + }); + } else { + return UIManager.measure( + getHostNode(fiber, findNodeHandle), + callback, + ); + } + }, + }; + }, })); }; + const getInspectorDataForInstance = function(closestInstance): InspectorData { + // Handle case where user clicks outside of ReactNative + if (!closestInstance) { + return { + hierarchy: [], + props: emptyObject, + selection: null, + source: null, + }; + } + + const fiber = findCurrentFiberUsingSlowPath(closestInstance); + const fiberHierarchy = getOwnerHierarchy(fiber); + const instance = lastNonHostInstance(fiberHierarchy); + const hierarchy = createHierarchy(fiberHierarchy); + const props = getHostProps(instance); + const source = instance._debugSource; + const selection = fiberHierarchy.indexOf(instance); + + return { + hierarchy, + props, + selection, + source, + }; + }; + getInspectorDataForViewTag = function(viewTag: number): Object { const closestInstance = getClosestInstanceFromNode(viewTag); @@ -117,6 +172,70 @@ if (__DEV__) { source, }; }; + + getInspectorDataForViewAtPoint = function( + findNodeHandle: (componentOrHandle: any) => ?number, + inspectedView: Object, + locationX: number, + locationY: number, + callback: (viewData: TouchedViewDataAtPoint) => mixed, + ): void { + let closestInstance = null; + + if (inspectedView._internalInstanceHandle != null) { + // For Fabric we can look up the instance handle directly and measure it. + nativeFabricUIManager.findNodeAtPoint( + inspectedView._internalInstanceHandle.stateNode.node, + locationX, + locationY, + internalInstanceHandle => { + if (internalInstanceHandle == null) { + callback({ + pointerY: locationY, + frame: {left: 0, top: 0, width: 0, height: 0}, + ...getInspectorDataForInstance(closestInstance), + }); + } + + closestInstance = + internalInstanceHandle.stateNode.canonical._internalInstanceHandle; + nativeFabricUIManager.measure( + internalInstanceHandle.stateNode.node, + (x, y, width, height, pageX, pageY) => { + callback({ + pointerY: locationY, + frame: {left: pageX, top: pageY, width, height}, + ...getInspectorDataForInstance(closestInstance), + }); + }, + ); + }, + ); + } else if (inspectedView._internalFiberInstanceHandle != null) { + // For Paper we fall back to the old strategy using the React tag. + UIManager.findSubviewIn( + findNodeHandle(inspectedView), + [locationX, locationY], + (nativeViewTag, left, top, width, height) => { + const inspectorData = getInspectorDataForInstance( + getClosestInstanceFromNode(nativeViewTag), + ); + callback({ + ...inspectorData, + pointerY: locationY, + frame: {left, top, width, height}, + touchedViewTag: nativeViewTag, + }); + }, + ); + } else if (__DEV__) { + console.error( + 'getInspectorDataForViewAtPoint expects to receieve a host component', + ); + + return; + } + }; } else { getInspectorDataForViewTag = () => { invariant( @@ -124,6 +243,19 @@ if (__DEV__) { 'getInspectorDataForViewTag() is not available in production', ); }; + + getInspectorDataForViewAtPoint = ( + findNodeHandle: (componentOrHandle: any) => ?number, + inspectedView: Object, + locationX: number, + locationY: number, + callback: (viewData: TouchedViewDataAtPoint) => mixed, + ): void => { + invariant( + false, + 'getInspectorDataForViewAtPoint() is not available in production.', + ); + }; } -export {getInspectorDataForViewTag}; +export {getInspectorDataForViewAtPoint, getInspectorDataForViewTag}; diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js index 86f3e92bb382e..4b9b039e0912a 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -112,7 +112,11 @@ export function createInstance( updatePayload, // props ); - const component = new ReactNativeFiberHostComponent(tag, viewConfig); + const component = new ReactNativeFiberHostComponent( + tag, + viewConfig, + internalInstanceHandle, + ); precacheFiberNode(internalInstanceHandle, tag); updateFiberProps(tag, props); diff --git a/packages/react-native-renderer/src/ReactNativeRenderer.js b/packages/react-native-renderer/src/ReactNativeRenderer.js index 0d587355cffbc..f448962dff025 100644 --- a/packages/react-native-renderer/src/ReactNativeRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeRenderer.js @@ -36,7 +36,10 @@ import ReactVersion from 'shared/ReactVersion'; import {UIManager} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; import {getClosestInstanceFromNode} from './ReactNativeComponentTree'; -import {getInspectorDataForViewTag} from './ReactNativeFiberInspector'; +import { + getInspectorDataForViewTag, + getInspectorDataForViewAtPoint, +} from './ReactNativeFiberInspector'; import {LegacyRoot} from 'shared/ReactRootTags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; @@ -247,6 +250,10 @@ export { injectIntoDevTools({ findFiberByHostInstance: getClosestInstanceFromNode, getInspectorDataForViewTag: getInspectorDataForViewTag, + getInspectorDataForViewAtPoint: getInspectorDataForViewAtPoint.bind( + null, + findNodeHandle, + ), bundleType: __DEV__ ? 1 : 0, version: ReactVersion, rendererPackageName: 'react-native-renderer', diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js index 4c2df9a44fe06..d36378f4d1eb8 100644 --- a/packages/react-native-renderer/src/ReactNativeTypes.js +++ b/packages/react-native-renderer/src/ReactNativeTypes.js @@ -100,6 +100,46 @@ type SecretInternalsType = { ... }; +type InspectorDataProps = $ReadOnly<{ + [propName: string]: string, + ..., +}>; + +type InspectorDataSource = $ReadOnly<{| + fileName?: string, + lineNumber?: number, +|}>; + +type InspectorDataGetter = ( + (componentOrHandle: any) => ?number, +) => $ReadOnly<{| + measure: Function, + props: InspectorDataProps, + source: InspectorDataSource, +|}>; + +export type InspectorData = $ReadOnly<{| + hierarchy: Array<{| + name: ?string, + getInspectorData: InspectorDataGetter, + |}>, + selection: ?number, + props: InspectorDataProps, + source: ?InspectorDataSource, +|}>; + +export type TouchedViewDataAtPoint = $ReadOnly<{| + pointerY: number, + touchedViewTag?: number, + frame: $ReadOnly<{| + top: number, + left: number, + width: number, + height: number, + |}>, + ...InspectorData, +|}>; + /** * Flat ReactNative renderer bundles are too big for Flow to parse efficiently. * Provide minimal Flow typing for the high-level RN API and call it a day. diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index fbb258507d314..60372b81796bd 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -10,6 +10,7 @@ import type {Fiber} from './ReactFiber'; import type {FiberRoot} from './ReactFiberRoot'; import type {RootTag} from 'shared/ReactRootTags'; +import type {TouchedViewDataAtPoint} from 'react-native-renderer/src/ReactNativeTypes'; import type { Instance, TextInstance, @@ -109,6 +110,13 @@ type DevToolsConfig = {| // This API is unfortunately RN-specific. // TODO: Change it to accept Fiber instead and type it properly. getInspectorDataForViewTag?: (tag: number) => Object, + // Used by RN in-app inspector. + getInspectorDataForViewAtPoint?: ( + inspectedView: Object, + locationX: number, + locationY: number, + callback: (viewData: TouchedViewDataAtPoint) => mixed, + ) => void, |}; let didWarnAboutNestedUpdates; diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 9e85caa7c7475..4383e3af74a2b 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -346,5 +346,6 @@ "345": "Root did not complete. This is a bug in React.", "346": "An event responder context was used outside of an event cycle.", "347": "Maps are not valid as a React child (found: %s). Consider converting children to an array of keyed ReactElements instead.", - "348": "ensureListeningTo(): received a container that was not an element node. This is likely a bug in React." + "348": "ensureListeningTo(): received a container that was not an element node. This is likely a bug in React.", + "349": "getInspectorDataForViewAtPoint() is not available in production." } diff --git a/scripts/flow/react-native-host-hooks.js b/scripts/flow/react-native-host-hooks.js index 85ea68f160ce2..151e36e4c47fd 100644 --- a/scripts/flow/react-native-host-hooks.js +++ b/scripts/flow/react-native-host-hooks.js @@ -18,6 +18,7 @@ import type { } from 'react-native-renderer/src/ReactNativeTypes'; import type {RNTopLevelEventType} from 'legacy-events/TopLevelEventTypes'; import type {CapturedError} from 'react-reconciler/src/ReactCapturedValue'; +import type {Fiber} from 'react-reconciler/src/ReactFiber'; type DeepDifferOptions = {|+unsafelyIgnoreFunctions?: boolean|}; @@ -96,6 +97,17 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface' ) => Promise, setJSResponder: (reactTag: number, blockNativeResponder: boolean) => void, clearJSResponder: () => void, + findSubviewIn: ( + reactTag: ?number, + point: Array, + callback: ( + nativeViewTag: number, + left: number, + top: number, + width: number, + height: number, + ) => void, + ) => void, ... }; declare export var BatchedBridge: { @@ -156,6 +168,12 @@ declare var nativeFabricUIManager: { onFail: () => void, onSuccess: MeasureLayoutOnSuccessCallback, ) => void, + findNodeAtPoint: ( + node: Node, + locationX: number, + locationY: number, + callback: (Fiber) => void, + ) => void, ... };