feat[React Native]: bump React DevTools to 5.0.0 and support highlighting multiple elements (#41842)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/41842

Changelog: [Internal]

- bumps `react-devtools-*` packages to 5.0.0 across xplat
- added support for highlighting multiple host components, when hovering over component, which is represented by multiple host fibers.

See test plan, this can be reproduced with a named component, which renders multiple host components inside a React Fragment:
```
<>
  <View />
  <View />
  <View />
</>
```

Reviewed By: gsathya

Differential Revision: D51888628

fbshipit-source-id: 2bd2d9fa50c24f478aa9406ee6bb42a47168bf13
This commit is contained in:
Ruslan Lesiutin
2024-01-15 14:11:00 -08:00
committed by Facebook GitHub Bot
parent 9d846f47c2
commit 790d7fd24e
5 changed files with 116 additions and 42 deletions
@@ -22,7 +22,10 @@ import type {
ReactDevToolsAgentEvents,
ReactDevToolsGlobalHook,
} from '../Types/ReactDevToolsTypes';
import type {TraceUpdate} from './DebuggingOverlayNativeComponent';
import type {
ElementRectangle,
TraceUpdate,
} from './DebuggingOverlayNativeComponent';
import {
findNodeHandle,
@@ -90,9 +93,9 @@ class DebuggingOverlayRegistry {
agent.addListener('hideNativeHighlight', this.#onClearElementsHighlights);
};
#getPublicInstanceFromInstance(
#getPublicInstanceFromInstance = (
instanceHandle: InstanceFromReactDevTools,
): NativeMethods | null {
): NativeMethods | null => {
// `canonical.publicInstance` => Fabric
if (instanceHandle.canonical?.publicInstance != null) {
return instanceHandle.canonical?.publicInstance;
@@ -111,7 +114,7 @@ class DebuggingOverlayRegistry {
}
return null;
}
};
#findLowestParentFromRegistryForInstance(
instance: ReactNativeElement,
@@ -360,34 +363,63 @@ class DebuggingOverlayRegistry {
#onHighlightElements: (
...ReactDevToolsAgentEvents['showNativeHighlight']
) => void = node => {
) => void = nodes => {
// First clear highlights for every container
for (const subscriber of this.#registry) {
subscriber.debuggingOverlayRef.current?.clearElementsHighlight();
}
const publicInstance = this.#getPublicInstanceFromInstance(node);
if (publicInstance == null) {
return;
}
// Lazy import to avoid dependency cycle.
const ReactNativeElementClass =
require('../DOM/Nodes/ReactNativeElement').default;
if (publicInstance instanceof ReactNativeElementClass) {
this.#onHighlightElementsModern(publicInstance);
} else {
this.#onHighlightElementsLegacy(publicInstance);
const reactNativeElements: Array<ReactNativeElement> = [];
const legacyPublicInstances: Array<NativeMethods> = [];
for (const node of nodes) {
const publicInstance = this.#getPublicInstanceFromInstance(node);
if (publicInstance == null) {
continue;
}
if (publicInstance instanceof ReactNativeElementClass) {
reactNativeElements.push(publicInstance);
} else {
legacyPublicInstances.push(publicInstance);
}
}
if (reactNativeElements.length > 0) {
this.#onHighlightElementsModern(reactNativeElements);
}
if (legacyPublicInstances.length > 0) {
this.#onHighlightElementsLegacy(legacyPublicInstances);
}
};
#onHighlightElementsModern(publicInstance: ReactNativeElement): void {
const {x, y, width, height} = publicInstance.getBoundingClientRect();
#onHighlightElementsModern(elements: Array<ReactNativeElement>): void {
const parentToElementsMap = new Map<
DebuggingOverlayRegistrySubscriberProtocol,
Array<ReactNativeElement>,
>();
const parent =
this.#findLowestParentFromRegistryForInstance(publicInstance);
for (const element of elements) {
const parent = this.#findLowestParentFromRegistryForInstance(element);
if (parent == null) {
continue;
}
if (parent) {
let childElementOfAParent = parentToElementsMap.get(parent);
if (childElementOfAParent == null) {
childElementOfAParent = [];
parentToElementsMap.set(parent, childElementOfAParent);
}
childElementOfAParent.push(element);
}
for (const [parent, elementsToHighlight] of parentToElementsMap.entries()) {
const rootViewInstance = parent.rootViewRef.current;
if (rootViewInstance == null) {
return;
@@ -400,28 +432,70 @@ class DebuggingOverlayRegistry {
// DebuggingOverlay will scale to the same size as a Root view. Substract Root view position from the element position
// to calculate the element's position relatively to its parent DebuggingOverlay.
// We can't call `getBoundingClientRect` on the debuggingOverlayRef, because its a ref for the native component, which doesn't have it, hopefully yet.
parent.debuggingOverlayRef.current?.highlightElements([
{x: x - parentX, y: y - parentY, width, height},
]);
const elementsRectangles = elementsToHighlight.map(element => {
const {x, y, width, height} = element.getBoundingClientRect();
return {x: x - parentX, y: y - parentY, width, height};
});
parent.debuggingOverlayRef.current?.highlightElements(elementsRectangles);
}
}
// TODO: remove once DOM Node APIs are opt-in by default and Paper is no longer supported.
#onHighlightElementsLegacy(publicInstance: NativeMethods): void {
const container =
this.#findLowestParentFromRegistryForInstanceLegacy(publicInstance);
#onHighlightElementsLegacy(elements: Array<NativeMethods>): void {
const parentToElementsMap = new Map<
DebuggingOverlayRegistrySubscriberProtocol,
Array<NativeMethods>,
>();
if (container != null) {
publicInstance.measure((x, y, width, height, left, top) => {
// measure can execute callback without any values provided to signal error.
if (left == null || top == null || width == null || height == null) {
return;
}
for (const element of elements) {
const parent =
this.#findLowestParentFromRegistryForInstanceLegacy(element);
if (parent == null) {
continue;
}
container.debuggingOverlayRef.current?.highlightElements([
{x: left, y: top, width, height},
]);
});
let childElementOfAParent = parentToElementsMap.get(parent);
if (childElementOfAParent == null) {
childElementOfAParent = [];
parentToElementsMap.set(parent, childElementOfAParent);
}
childElementOfAParent.push(element);
}
for (const [parent, elementsToHighlight] of parentToElementsMap.entries()) {
const promises = elementsToHighlight.map(
element =>
new Promise<ElementRectangle>((resolve, reject) => {
element.measure((x, y, width, height, left, top) => {
// measure can execute callback without any values provided to signal error.
if (
left == null ||
top == null ||
width == null ||
height == null
) {
reject('Unexpectedly failed to call measure on an instance.');
}
resolve({x: left, y: top, width, height});
});
}),
);
Promise.all(promises)
.then(resolvedElementsRectangles =>
parent.debuggingOverlayRef.current?.highlightElements(
resolvedElementsRectangles,
),
)
.catch(() => {
// noop. For legacy architecture (Paper) this can happen for root views or LogBox button.
// LogBox case: it has a separate React root, so `measure` fails.
// Calling `console.error` here would trigger rendering a new LogBox button, for which we will call measure again, this is a cycle.
// Don't spam the UI with errors for such cases.
});
}
}
@@ -29,7 +29,7 @@ export type ReactDevToolsAgentEvents = {
drawTraceUpdates: [Array<{node: InstanceFromReactDevTools, color: string}>],
disableTraceUpdates: [],
showNativeHighlight: [node: InstanceFromReactDevTools],
showNativeHighlight: [nodes: Array<InstanceFromReactDevTools>],
hideNativeHighlight: [],
shutdown: [],
startInspectingNative: [],
@@ -9482,7 +9482,7 @@ export type InstanceFromReactDevTools =
export type ReactDevToolsAgentEvents = {
drawTraceUpdates: [Array<{ node: InstanceFromReactDevTools, color: string }>],
disableTraceUpdates: [],
showNativeHighlight: [node: InstanceFromReactDevTools],
showNativeHighlight: [nodes: Array<InstanceFromReactDevTools>],
hideNativeHighlight: [],
shutdown: [],
startInspectingNative: [],
+1 -1
View File
@@ -122,7 +122,7 @@
"nullthrows": "^1.1.1",
"pretty-format": "^26.5.2",
"promise": "^8.3.0",
"react-devtools-core": "^4.27.7",
"react-devtools-core": "^5.0.0",
"react-refresh": "^0.14.0",
"react-shallow-renderer": "^16.15.0",
"regenerator-runtime": "^0.13.2",
+4 -4
View File
@@ -8100,10 +8100,10 @@ range-parser@~1.2.1:
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
react-devtools-core@^4.27.7:
version "4.27.7"
resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.27.7.tgz#458a6541483078d60a036c75bf88f54c478086ec"
integrity sha512-12N0HrhCPbD76Z7SkyJdGdXdPGouUsgV6tlEsbSpAnLDO06tjXZP+irht4wPdYwJAJRQ85DxL48eQoz7UmrSuQ==
react-devtools-core@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-5.0.0.tgz#50b04a4dbfa62badbe4d86529e9478c396988b31"
integrity sha512-SAAMLacNDfFjMJjmbXURNWtrTyARi9xTqGkY48Btw5cIWlr1wgxfWYZKxoUZav1qqmhbpgTzSmmF+cpMHGHY3A==
dependencies:
shell-quote "^1.6.1"
ws "^7"