Skip to content

Commit 958cc57

Browse files
committed
[DevTools] Elevate Suspense rects to visualize hierarchy
1 parent 68570bc commit 958cc57

File tree

3 files changed

+139
-47
lines changed

3 files changed

+139
-47
lines changed

packages/react-devtools-shared/src/devtools/constants.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
163163
'--color-scroll-track': '#fafafa',
164164
'--color-tooltip-background': 'rgba(0, 0, 0, 0.9)',
165165
'--color-tooltip-text': '#ffffff',
166+
167+
'--elevation-4':
168+
'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)',
166169
},
167170
dark: {
168171
'--color-attribute-name': '#9d87d2',
@@ -315,6 +318,9 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
315318
'--color-scroll-track': '#313640',
316319
'--color-tooltip-background': 'rgba(255, 255, 255, 0.95)',
317320
'--color-tooltip-text': '#000000',
321+
322+
'--elevation-4':
323+
'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)',
318324
},
319325
compact: {
320326
'--font-size-monospace-small': '9px',

packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,36 @@
22
padding: .25rem;
33
}
44

5-
.SuspenseRect {
6-
fill: transparent;
7-
stroke: var(--color-background-selected);
8-
stroke-width: 1px;
9-
vector-effect: non-scaling-stroke;
10-
paint-order: stroke;
5+
.SuspenseRectsViewBox {
6+
position: relative;
117
}
128

13-
[data-highlighted='true'] > .SuspenseRect {
14-
fill: var(--color-selected-tree-highlight-active);
9+
.SuspenseRectsBoundary {
10+
box-shadow: var(--elevation-4);
11+
outline-style: solid;
12+
outline-width: 1px;
13+
/**
14+
* So that the shadow of Boundaries within is clipped off.
15+
* Otherwise it would look like this boundary is further elevated
16+
*/
17+
overflow: hidden;
18+
}
19+
20+
.SuspenseRectsRect {
21+
outline-style: dashed;
22+
outline-width: 0px;
23+
}
24+
25+
.SuspenseRectsScaledRect {
26+
position: absolute;
27+
outline-color: var(--color-background-selected);
28+
}
29+
30+
.SuspenseRectsBoundary[data-highlighted='true'] {
31+
background-color: var(--color-selected-tree-highlight-active);
32+
}
33+
34+
/* highlight individual rects of this boundary */
35+
.SuspenseRectsBoundary:hover:not(:has(.SuspenseRectsBoundary:hover)) > .SuspenseRectsRect, .SuspenseRectsBoundary[data-highlighted='true'] > .SuspenseRectsRect {
36+
outline-width: 1px;
1537
}

packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js

Lines changed: 103 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@ import type {
1212
SuspenseNode,
1313
Rect,
1414
} from 'react-devtools-shared/src/frontend/types';
15+
import typeof {
16+
SyntheticMouseEvent,
17+
SyntheticPointerEvent,
18+
} from 'react-dom-bindings/src/events/SyntheticEvent';
1519

1620
import * as React from 'react';
17-
import {useContext} from 'react';
21+
import {createContext, useContext} from 'react';
1822
import {
1923
TreeDispatcherContext,
2024
TreeStateContext,
@@ -26,19 +30,32 @@ import {
2630
SuspenseTreeStateContext,
2731
SuspenseTreeDispatcherContext,
2832
} from './SuspenseTreeContext';
29-
import typeof {
30-
SyntheticMouseEvent,
31-
SyntheticPointerEvent,
32-
} from 'react-dom-bindings/src/events/SyntheticEvent';
3333

34-
function SuspenseRect({rect}: {rect: Rect}): React$Node {
34+
function ScaledRect({
35+
className,
36+
rect,
37+
...props
38+
}: {
39+
className: string,
40+
rect: Rect,
41+
...
42+
}): React$Node {
43+
const viewBox = useContext(ViewBox);
44+
const width = (rect.width / viewBox.width) * 100 + '%';
45+
const height = (rect.height / viewBox.height) * 100 + '%';
46+
const x = ((rect.x - viewBox.x) / viewBox.width) * 100 + '%';
47+
const y = ((rect.y - viewBox.y) / viewBox.height) * 100 + '%';
48+
3549
return (
36-
<rect
37-
className={styles.SuspenseRect}
38-
x={rect.x}
39-
y={rect.y}
40-
width={rect.width}
41-
height={rect.height}
50+
<div
51+
{...props}
52+
className={styles.SuspenseRectsScaledRect + ' ' + className}
53+
style={{
54+
width,
55+
height,
56+
top: y,
57+
left: x,
58+
}}
4259
/>
4360
);
4461
}
@@ -97,24 +114,67 @@ function SuspenseRects({
97114
// TODO: Use the nearest Suspense boundary
98115
const selected = inspectedElementID === suspenseID;
99116

117+
const boundingBox = getBoundingBox(suspense.rects);
118+
100119
return (
101-
<g
102-
data-highlighted={selected}
103-
onClick={handleClick}
104-
onPointerOver={handlePointerOver}
105-
onPointerLeave={handlePointerLeave}>
106-
<title>{suspense.name}</title>
107-
{suspense.rects !== null &&
108-
suspense.rects.map((rect, index) => {
109-
return <SuspenseRect key={index} rect={rect} />;
110-
})}
111-
{suspense.children.map(childID => {
112-
return <SuspenseRects key={childID} suspenseID={childID} />;
113-
})}
114-
</g>
120+
<>
121+
<ScaledRect
122+
rect={boundingBox}
123+
className={styles.SuspenseRectsBoundary}
124+
data-highlighted={selected}
125+
onClick={handleClick}
126+
onPointerOver={handlePointerOver}
127+
onPointerLeave={handlePointerLeave}
128+
// Reach-UI tooltip will go out of bounds of parent scroll container.
129+
// Native title will change position for each Client Rect.
130+
// changing position is better than going oob.
131+
title={suspense.name}>
132+
<ViewBox.Provider value={boundingBox}>
133+
{suspense.rects !== null &&
134+
suspense.rects.map((rect, index) => {
135+
return (
136+
<ScaledRect
137+
key={index}
138+
className={styles.SuspenseRectsRect}
139+
rect={rect}
140+
/>
141+
);
142+
})}
143+
{suspense.children.map(childID => {
144+
return <SuspenseRects key={childID} suspenseID={childID} />;
145+
})}
146+
</ViewBox.Provider>
147+
</ScaledRect>
148+
</>
115149
);
116150
}
117151

152+
function getBoundingBox(rects: $ReadOnlyArray<Rect> | null): Rect {
153+
if (rects === null || rects.length === 0) {
154+
return {x: 0, y: 0, width: 0, height: 0};
155+
}
156+
157+
let minX = Number.POSITIVE_INFINITY;
158+
let minY = Number.POSITIVE_INFINITY;
159+
let maxX = Number.NEGATIVE_INFINITY;
160+
let maxY = Number.NEGATIVE_INFINITY;
161+
162+
for (let i = 0; i < rects.length; i++) {
163+
const rect = rects[i];
164+
minX = Math.min(minX, rect.x);
165+
minY = Math.min(minY, rect.y);
166+
maxX = Math.max(maxX, rect.x + rect.width);
167+
maxY = Math.max(maxY, rect.y + rect.height);
168+
}
169+
170+
return {
171+
x: minX,
172+
y: minY,
173+
width: maxX - minX,
174+
height: maxY - minY,
175+
};
176+
}
177+
118178
function getDocumentBoundingRect(
119179
store: Store,
120180
roots: $ReadOnlyArray<SuspenseNode['id']>,
@@ -161,29 +221,33 @@ function getDocumentBoundingRect(
161221
};
162222
}
163223

224+
const ViewBox = createContext<Rect>((null: any));
225+
164226
function SuspenseRectsContainer(): React$Node {
165227
const store = useContext(StoreContext);
166228
// TODO: This relies on a full re-render of all children when the Suspense tree changes.
167229
const {roots} = useContext(SuspenseTreeStateContext);
168230

169-
const boundingRect = getDocumentBoundingRect(store, roots);
231+
const boundingBox = getDocumentBoundingRect(store, roots);
170232

233+
const boundingBoxWidth = boundingBox.width;
234+
const heightScale =
235+
boundingBoxWidth === 0 ? 1 : boundingBox.height / boundingBoxWidth;
236+
// Scales the inspected document to fit into the available width
171237
const width = '100%';
172-
const boundingRectWidth = boundingRect.width;
173-
const height =
174-
(boundingRectWidth === 0 ? 0 : boundingRect.height / boundingRect.width) *
175-
100 +
176-
'%';
238+
const aspectRatio = `1 / ${heightScale}`;
177239

178240
return (
179241
<div className={styles.SuspenseRectsContainer}>
180-
<svg
181-
style={{width, height}}
182-
viewBox={`${boundingRect.x} ${boundingRect.y} ${boundingRect.width} ${boundingRect.height}`}>
183-
{roots.map(rootID => {
184-
return <SuspenseRects key={rootID} suspenseID={rootID} />;
185-
})}
186-
</svg>
242+
<ViewBox.Provider value={boundingBox}>
243+
<div
244+
className={styles.SuspenseRectsViewBox}
245+
style={{aspectRatio, width}}>
246+
{roots.map(rootID => {
247+
return <SuspenseRects key={rootID} suspenseID={rootID} />;
248+
})}
249+
</div>
250+
</ViewBox.Provider>
187251
</div>
188252
);
189253
}

0 commit comments

Comments
 (0)