From ae7fe3fbc0377460c5f6cb575417cfb3810c13eb Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Wed, 10 Sep 2025 10:47:26 +0200 Subject: [PATCH 1/4] [DevTools] Elevate Suspense rects to visualize hierarchy --- .../src/devtools/constants.js | 6 + .../views/SuspenseTab/SuspenseRects.css | 38 ++++- .../views/SuspenseTab/SuspenseRects.js | 154 ++++++++++++------ 3 files changed, 143 insertions(+), 55 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/constants.js b/packages/react-devtools-shared/src/devtools/constants.js index ee13e5b1630..cfb73713b08 100644 --- a/packages/react-devtools-shared/src/devtools/constants.js +++ b/packages/react-devtools-shared/src/devtools/constants.js @@ -163,6 +163,9 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = { '--color-scroll-track': '#fafafa', '--color-tooltip-background': 'rgba(0, 0, 0, 0.9)', '--color-tooltip-text': '#ffffff', + + '--elevation-4': + '0 2px 4px -1px rgba(0,0,0,.2),0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12)', }, dark: { '--color-attribute-name': '#9d87d2', @@ -315,6 +318,9 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = { '--color-scroll-track': '#313640', '--color-tooltip-background': 'rgba(255, 255, 255, 0.95)', '--color-tooltip-text': '#000000', + + '--elevation-4': + '0 2px 8px 0 rgba(0,0,0,0.32),0 4px 12px 0 rgba(0,0,0,0.24),0 1px 10px 0 rgba(0,0,0,0.18)', }, compact: { '--font-size-monospace-small': '9px', diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css index d9ecf2cad75..2b5051d950a 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css @@ -2,14 +2,36 @@ padding: .25rem; } -.SuspenseRect { - fill: transparent; - stroke: var(--color-background-selected); - stroke-width: 1px; - vector-effect: non-scaling-stroke; - paint-order: stroke; +.SuspenseRectsViewBox { + position: relative; } -[data-highlighted='true'] > .SuspenseRect { - fill: var(--color-selected-tree-highlight-active); +.SuspenseRectsBoundary { + box-shadow: var(--elevation-4); + outline-style: solid; + outline-width: 1px; + /** + * So that the shadow of Boundaries within is clipped off. + * Otherwise it would look like this boundary is further elevated + */ + overflow: hidden; +} + +.SuspenseRectsRect { + outline-style: dashed; + outline-width: 0px; +} + +.SuspenseRectsScaledRect { + position: absolute; + outline-color: var(--color-background-selected); +} + +.SuspenseRectsBoundary[data-highlighted='true'] { + background-color: var(--color-selected-tree-highlight-active); +} + +/* highlight individual rects of this boundary */ +.SuspenseRectsBoundary:hover:not(:has(.SuspenseRectsBoundary:hover)) > .SuspenseRectsRect, .SuspenseRectsBoundary[data-highlighted='true'] > .SuspenseRectsRect { + outline-width: 1px; } diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js index a03439c07d9..b0a376f63de 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js @@ -12,9 +12,13 @@ import type { SuspenseNode, Rect, } from 'react-devtools-shared/src/frontend/types'; +import typeof { + SyntheticMouseEvent, + SyntheticPointerEvent, +} from 'react-dom-bindings/src/events/SyntheticEvent'; import * as React from 'react'; -import {useContext} from 'react'; +import {createContext, useContext} from 'react'; import { TreeDispatcherContext, TreeStateContext, @@ -26,19 +30,32 @@ import { SuspenseTreeStateContext, SuspenseTreeDispatcherContext, } from './SuspenseTreeContext'; -import typeof { - SyntheticMouseEvent, - SyntheticPointerEvent, -} from 'react-dom-bindings/src/events/SyntheticEvent'; -function SuspenseRect({rect}: {rect: Rect}): React$Node { +function ScaledRect({ + className, + rect, + ...props +}: { + className: string, + rect: Rect, + ... +}): React$Node { + const viewBox = useContext(ViewBox); + const width = (rect.width / viewBox.width) * 100 + '%'; + const height = (rect.height / viewBox.height) * 100 + '%'; + const x = ((rect.x - viewBox.x) / viewBox.width) * 100 + '%'; + const y = ((rect.y - viewBox.y) / viewBox.height) * 100 + '%'; + return ( - ); } @@ -97,24 +114,67 @@ function SuspenseRects({ // TODO: Use the nearest Suspense boundary const selected = inspectedElementID === suspenseID; + const boundingBox = getBoundingBox(suspense.rects); + return ( - - {suspense.name} - {suspense.rects !== null && - suspense.rects.map((rect, index) => { - return ; - })} - {suspense.children.map(childID => { - return ; - })} - + <> + + + {suspense.rects !== null && + suspense.rects.map((rect, index) => { + return ( + + ); + })} + {suspense.children.map(childID => { + return ; + })} + + + ); } +function getBoundingBox(rects: $ReadOnlyArray | null): Rect { + if (rects === null || rects.length === 0) { + return {x: 0, y: 0, width: 0, height: 0}; + } + + let minX = Number.POSITIVE_INFINITY; + let minY = Number.POSITIVE_INFINITY; + let maxX = Number.NEGATIVE_INFINITY; + let maxY = Number.NEGATIVE_INFINITY; + + for (let i = 0; i < rects.length; i++) { + const rect = rects[i]; + minX = Math.min(minX, rect.x); + minY = Math.min(minY, rect.y); + maxX = Math.max(maxX, rect.x + rect.width); + maxY = Math.max(maxY, rect.y + rect.height); + } + + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; +} + function getDocumentBoundingRect( store: Store, roots: $ReadOnlyArray, @@ -169,42 +229,42 @@ function SuspenseRectsShell({ const store = useContext(StoreContext); const root = store.getSuspenseByID(rootID); if (root === null) { - console.warn(` Could not find suspense node id ${rootID}`); + // getSuspenseByID will have already warned return null; } - return ( - - {root.children.map(childID => { - return ; - })} - - ); + return root.children.map(childID => { + return ; + }); } +const ViewBox = createContext((null: any)); + function SuspenseRectsContainer(): React$Node { const store = useContext(StoreContext); // TODO: This relies on a full re-render of all children when the Suspense tree changes. const {roots} = useContext(SuspenseTreeStateContext); - const boundingRect = getDocumentBoundingRect(store, roots); + const boundingBox = getDocumentBoundingRect(store, roots); + const boundingBoxWidth = boundingBox.width; + const heightScale = + boundingBoxWidth === 0 ? 1 : boundingBox.height / boundingBoxWidth; + // Scales the inspected document to fit into the available width const width = '100%'; - const boundingRectWidth = boundingRect.width; - const height = - (boundingRectWidth === 0 ? 0 : boundingRect.height / boundingRect.width) * - 100 + - '%'; + const aspectRatio = `1 / ${heightScale}`; return (
- - {roots.map(rootID => { - return ; - })} - + +
+ {roots.map(rootID => { + return ; + })} +
+
); } From 7f86fd79accd472fbca76b917803d2906e069e73 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Fri, 12 Sep 2025 09:51:35 +0200 Subject: [PATCH 2/4] Stop drawing bbox It's misleading unless it would be an actual polygon --- .../views/SuspenseTab/SuspenseRects.css | 28 +++++++++++-------- .../views/SuspenseTab/SuspenseRects.js | 28 +++++++++---------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css index 2b5051d950a..7e74f21eef0 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css @@ -7,19 +7,23 @@ } .SuspenseRectsBoundary { - box-shadow: var(--elevation-4); - outline-style: solid; - outline-width: 1px; + pointer-events: all; +} + +.SuspenseRectsBoundaryChildren { + pointer-events: none; /** * So that the shadow of Boundaries within is clipped off. - * Otherwise it would look like this boundary is further elevated + * Otherwise it would look like this boundary is further elevated. */ - overflow: hidden; + overflow: hidden; } .SuspenseRectsRect { - outline-style: dashed; - outline-width: 0px; + box-shadow: var(--elevation-4); + pointer-events: all; + outline-style: solid; + outline-width: 1px; } .SuspenseRectsScaledRect { @@ -27,11 +31,11 @@ outline-color: var(--color-background-selected); } -.SuspenseRectsBoundary[data-highlighted='true'] { - background-color: var(--color-selected-tree-highlight-active); +/* highlight this boundary */ +.SuspenseRectsBoundary:hover:not(:has(.SuspenseRectsBoundary:hover)) > .SuspenseRectsRect, .SuspenseRectsBoundary[data-highlighted='true'] > .SuspenseRectsRect { + background-color: var(--color-background-hover); } -/* highlight individual rects of this boundary */ -.SuspenseRectsBoundary:hover:not(:has(.SuspenseRectsBoundary:hover)) > .SuspenseRectsRect, .SuspenseRectsBoundary[data-highlighted='true'] > .SuspenseRectsRect { - outline-width: 1px; +.SuspenseRectsRect[data-highlighted='true'] { + background-color: var(--color-selected-tree-highlight-active); } diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js index b0a376f63de..fc744119ddc 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js @@ -118,17 +118,7 @@ function SuspenseRects({ return ( <> - + {suspense.rects !== null && suspense.rects.map((rect, index) => { @@ -137,12 +127,22 @@ function SuspenseRects({ key={index} className={styles.SuspenseRectsRect} rect={rect} + data-highlighted={selected} + onClick={handleClick} + onPointerOver={handlePointerOver} + onPointerLeave={handlePointerLeave} + // Reach-UI tooltip will go out of bounds of parent scroll container. + title={suspense.name} /> ); })} - {suspense.children.map(childID => { - return ; - })} + + {suspense.children.map(childID => { + return ; + })} + From a10e706f8a6c9646bee20ab7c18b2e63aaed41d3 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Fri, 12 Sep 2025 10:27:49 +0200 Subject: [PATCH 3/4] Cleanup --- .../views/SuspenseTab/SuspenseRects.js | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js index fc744119ddc..66b8316f1f2 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js @@ -117,25 +117,25 @@ function SuspenseRects({ const boundingBox = getBoundingBox(suspense.rects); return ( - <> - - - {suspense.rects !== null && - suspense.rects.map((rect, index) => { - return ( - - ); - })} + + + {suspense.rects !== null && + suspense.rects.map((rect, index) => { + return ( + + ); + })} + {suspense.children.length > 0 && ( @@ -143,9 +143,9 @@ function SuspenseRects({ return ; })} - - - + )} + + ); } From 138d38dff30461b7ce5185f365449efa62f8e360 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Thu, 18 Sep 2025 18:28:25 +0200 Subject: [PATCH 4/4] fixup! [DevTools] Elevate Suspense rects to visualize hierarchy --- .../src/devtools/views/SuspenseTab/SuspenseRects.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js index 66b8316f1f2..39c6f1c4925 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js @@ -261,7 +261,7 @@ function SuspenseRectsContainer(): React$Node { className={styles.SuspenseRectsViewBox} style={{aspectRatio, width}}> {roots.map(rootID => { - return ; + return ; })}