Skip to content

Commit

Permalink
Enable inspection from devtools
Browse files Browse the repository at this point in the history
Summary:
Changelog:
[General][Added] - Added an overlay similar to Inspector.js that allows directly selecting elements on RN from React DevTools

This diff updates DevToolsHighlighter into DevToolsOverlay. It now also allows DevTools user to select an element to inspect directly from DevTools.

Depends on facebook/react#25111 to work.

TODOs:
- Currently once an element selected on RN, the inspector toggle isn't turned off automatically.
- Fabric support depends on facebook/react#25118

Reviewed By: lunaruan

Differential Revision: D38815494

fbshipit-source-id: 7e1e3a78f6594960b5dfaec142bafd3ca4b146af
  • Loading branch information
tyao1 authored and facebook-github-bot committed Sep 1, 2022
1 parent 3db19b4 commit c52df02
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 143 deletions.
80 changes: 0 additions & 80 deletions Libraries/Inspector/DevtoolsHighlighter.js

This file was deleted.

167 changes: 167 additions & 0 deletions Libraries/Inspector/DevtoolsOverlay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* 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.
*
* @format
* @flow
*/

import ElementBox from './ElementBox';
import * as React from 'react';
import type {PressEvent} from '../Types/CoreEventTypes';
import View from '../Components/View/View';
import StyleSheet from '../StyleSheet/StyleSheet';
import Dimensions from '../Utilities/Dimensions';
const getInspectorDataForViewAtPoint = require('./getInspectorDataForViewAtPoint');
const ReactNative = require('../Renderer/shims/ReactNative');

import type {HostRef} from './getInspectorDataForViewAtPoint';

const {useEffect, useState, useCallback, useRef} = React;

const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;

export default function DevtoolsOverlay({
inspectedView,
}: {
inspectedView: ?HostRef,
}): React.Node {
const [inspected, setInspected] = useState(null);
const [isInspecting, setIsInspecting] = useState(false);
const devToolsAgentRef = useRef(null);

useEffect(() => {
let devToolsAgent = null;
let hideTimeoutId = null;

function onAgentHideNativeHighlight() {
// we wait to actually hide in order to avoid flicker
clearTimeout(hideTimeoutId);
hideTimeoutId = setTimeout(() => {
setInspected(null);
}, 100);
}

function onAgentShowNativeHighlight(node: any) {
clearTimeout(hideTimeoutId);
// Shape of `node` is different in Fabric.
const component = node.canonical ?? node;

component.measure((x, y, width, height, left, top) => {
setInspected({
frame: {left, top, width, height},
});
});
}

function cleanup() {
const currentAgent = devToolsAgent;
if (currentAgent != null) {
currentAgent.removeListener(
'hideNativeHighlight',
onAgentHideNativeHighlight,
);
currentAgent.removeListener(
'showNativeHighlight',
onAgentShowNativeHighlight,
);
currentAgent.removeListener('shutdown', cleanup);
currentAgent.removeListener(
'startInspectingNative',
onStartInspectingNative,
);
currentAgent.removeListener(
'stopInspectingNative',
onStopInspectingNative,
);
devToolsAgent = null;
}
devToolsAgentRef.current = null;
}

function onStartInspectingNative() {
setIsInspecting(true);
}

function onStopInspectingNative() {
setIsInspecting(false);
}

function _attachToDevtools(agent: Object) {
devToolsAgent = agent;
devToolsAgentRef.current = agent;
agent.addListener('hideNativeHighlight', onAgentHideNativeHighlight);
agent.addListener('showNativeHighlight', onAgentShowNativeHighlight);
agent.addListener('shutdown', cleanup);
agent.addListener('startInspectingNative', onStartInspectingNative);
agent.addListener('stopInspectingNative', onStopInspectingNative);
}

hook.on('react-devtools', _attachToDevtools);
if (hook.reactDevtoolsAgent) {
_attachToDevtools(hook.reactDevtoolsAgent);
}
return () => {
hook.off('react-devtools', _attachToDevtools);
cleanup();
};
}, []);

const findViewForTouchEvent = useCallback(
(e: PressEvent) => {
const agent = devToolsAgentRef.current;
if (agent == null) {
return;
}
const {locationX, locationY} = e.nativeEvent.touches[0];
getInspectorDataForViewAtPoint(
inspectedView,
locationX,
locationY,
viewData => {
const {touchedViewTag} = viewData;
if (touchedViewTag != null) {
agent.selectNode(ReactNative.findNodeHandle(touchedViewTag));
return true;
}
return false;
},
);
},
[inspectedView],
);

const shouldSetResponser = useCallback(
(e: PressEvent): boolean => {
findViewForTouchEvent(e);
return true;
},
[findViewForTouchEvent],
);

let highlight = inspected ? <ElementBox frame={inspected.frame} /> : null;
if (isInspecting) {
return (
<View
onStartShouldSetResponder={shouldSetResponser}
onResponderMove={findViewForTouchEvent}
nativeID="devToolsInspectorOverlay"
style={[styles.inspector, {height: Dimensions.get('window').height}]}>
{highlight}
</View>
);
}
return highlight;
}

const styles = StyleSheet.create({
inspector: {
backgroundColor: 'transparent',
position: 'absolute',
left: 0,
top: 0,
right: 0,
},
});
59 changes: 4 additions & 55 deletions Libraries/Inspector/Inspector.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,70 +20,18 @@ const ReactNative = require('../Renderer/shims/ReactNative');
const StyleSheet = require('../StyleSheet/StyleSheet');
const View = require('../Components/View/View');
const ReactNativeStyleAttributes = require('../Components/View/ReactNativeStyleAttributes');
const getInspectorDataForViewAtPoint = require('./getInspectorDataForViewAtPoint');

const invariant = require('invariant');

import type {
HostComponent,
TouchedViewDataAtPoint,
} from '../Renderer/shims/ReactNativeTypes';

type HostRef = React.ElementRef<HostComponent<mixed>>;

export type ReactRenderer = {
rendererConfig: {
getInspectorDataForViewAtPoint: (
inspectedView: ?HostRef,
locationX: number,
locationY: number,
callback: Function,
) => void,
...
},
};
import type {TouchedViewDataAtPoint} from '../Renderer/shims/ReactNativeTypes';
import type {HostRef} from './getInspectorDataForViewAtPoint';

const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
const renderers = findRenderers();

// Required for React DevTools to view/edit React Native styles in Flipper.
// Flipper doesn't inject these values when initializing DevTools.
hook.resolveRNStyle = require('../StyleSheet/flattenStyle');
hook.nativeStyleEditorValidAttributes = Object.keys(ReactNativeStyleAttributes);

function findRenderers(): $ReadOnlyArray<ReactRenderer> {
const allRenderers = Array.from(hook.renderers.values());
invariant(
allRenderers.length >= 1,
'Expected to find at least one React Native renderer on DevTools hook.',
);
return allRenderers;
}

function getInspectorDataForViewAtPoint(
inspectedView: ?HostRef,
locationX: number,
locationY: number,
callback: (viewData: TouchedViewDataAtPoint) => void,
) {
// Check all renderers for inspector data.
for (let i = 0; i < renderers.length; i++) {
const renderer = renderers[i];
if (renderer?.rendererConfig?.getInspectorDataForViewAtPoint != null) {
renderer.rendererConfig.getInspectorDataForViewAtPoint(
inspectedView,
locationX,
locationY,
viewData => {
// Only return with non-empty view data since only one renderer will have this view.
if (viewData && viewData.hierarchy.length > 0) {
callback(viewData);
}
},
);
}
}
}

class Inspector extends React.Component<
{
inspectedView: ?HostRef,
Expand Down Expand Up @@ -221,6 +169,7 @@ class Inspector extends React.Component<
this._setTouchedViewData(viewData);
this._setTouchedViewData = null;
}
return false;
},
);
}
Expand Down

0 comments on commit c52df02

Please sign in to comment.