mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
[DevTools] Elevate Suspense rects to visualize hierarchy (#34455)
This commit is contained in:
parent
581321160f
commit
755cebad6b
|
|
@ -163,6 +163,9 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
|
||||||
'--color-scroll-track': '#fafafa',
|
'--color-scroll-track': '#fafafa',
|
||||||
'--color-tooltip-background': 'rgba(0, 0, 0, 0.9)',
|
'--color-tooltip-background': 'rgba(0, 0, 0, 0.9)',
|
||||||
'--color-tooltip-text': '#ffffff',
|
'--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: {
|
dark: {
|
||||||
'--color-attribute-name': '#9d87d2',
|
'--color-attribute-name': '#9d87d2',
|
||||||
|
|
@ -315,6 +318,9 @@ export const THEME_STYLES: {[style: Theme | DisplayDensity]: any, ...} = {
|
||||||
'--color-scroll-track': '#313640',
|
'--color-scroll-track': '#313640',
|
||||||
'--color-tooltip-background': 'rgba(255, 255, 255, 0.95)',
|
'--color-tooltip-background': 'rgba(255, 255, 255, 0.95)',
|
||||||
'--color-tooltip-text': '#000000',
|
'--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: {
|
compact: {
|
||||||
'--font-size-monospace-small': '9px',
|
'--font-size-monospace-small': '9px',
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,40 @@
|
||||||
padding: .25rem;
|
padding: .25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SuspenseRect {
|
.SuspenseRectsViewBox {
|
||||||
fill: transparent;
|
position: relative;
|
||||||
stroke: var(--color-background-selected);
|
|
||||||
stroke-width: 1px;
|
|
||||||
vector-effect: non-scaling-stroke;
|
|
||||||
paint-order: stroke;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-highlighted='true'] > .SuspenseRect {
|
.SuspenseRectsBoundary {
|
||||||
fill: var(--color-selected-tree-highlight-active);
|
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.
|
||||||
|
*/
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SuspenseRectsRect {
|
||||||
|
box-shadow: var(--elevation-4);
|
||||||
|
pointer-events: all;
|
||||||
|
outline-style: solid;
|
||||||
|
outline-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SuspenseRectsScaledRect {
|
||||||
|
position: absolute;
|
||||||
|
outline-color: var(--color-background-selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* highlight this boundary */
|
||||||
|
.SuspenseRectsBoundary:hover:not(:has(.SuspenseRectsBoundary:hover)) > .SuspenseRectsRect, .SuspenseRectsBoundary[data-highlighted='true'] > .SuspenseRectsRect {
|
||||||
|
background-color: var(--color-background-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.SuspenseRectsRect[data-highlighted='true'] {
|
||||||
|
background-color: var(--color-selected-tree-highlight-active);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,13 @@ import type {
|
||||||
SuspenseNode,
|
SuspenseNode,
|
||||||
Rect,
|
Rect,
|
||||||
} from 'react-devtools-shared/src/frontend/types';
|
} from 'react-devtools-shared/src/frontend/types';
|
||||||
|
import typeof {
|
||||||
|
SyntheticMouseEvent,
|
||||||
|
SyntheticPointerEvent,
|
||||||
|
} from 'react-dom-bindings/src/events/SyntheticEvent';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {useContext} from 'react';
|
import {createContext, useContext} from 'react';
|
||||||
import {
|
import {
|
||||||
TreeDispatcherContext,
|
TreeDispatcherContext,
|
||||||
TreeStateContext,
|
TreeStateContext,
|
||||||
|
|
@ -26,19 +30,32 @@ import {
|
||||||
SuspenseTreeStateContext,
|
SuspenseTreeStateContext,
|
||||||
SuspenseTreeDispatcherContext,
|
SuspenseTreeDispatcherContext,
|
||||||
} from './SuspenseTreeContext';
|
} 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 (
|
return (
|
||||||
<rect
|
<div
|
||||||
className={styles.SuspenseRect}
|
{...props}
|
||||||
x={rect.x}
|
className={styles.SuspenseRectsScaledRect + ' ' + className}
|
||||||
y={rect.y}
|
style={{
|
||||||
width={rect.width}
|
width,
|
||||||
height={rect.height}
|
height,
|
||||||
|
top: y,
|
||||||
|
left: x,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -97,24 +114,67 @@ function SuspenseRects({
|
||||||
// TODO: Use the nearest Suspense boundary
|
// TODO: Use the nearest Suspense boundary
|
||||||
const selected = inspectedElementID === suspenseID;
|
const selected = inspectedElementID === suspenseID;
|
||||||
|
|
||||||
|
const boundingBox = getBoundingBox(suspense.rects);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g
|
<ScaledRect rect={boundingBox} className={styles.SuspenseRectsBoundary}>
|
||||||
data-highlighted={selected}
|
<ViewBox.Provider value={boundingBox}>
|
||||||
onClick={handleClick}
|
{suspense.rects !== null &&
|
||||||
onPointerOver={handlePointerOver}
|
suspense.rects.map((rect, index) => {
|
||||||
onPointerLeave={handlePointerLeave}>
|
return (
|
||||||
<title>{suspense.name}</title>
|
<ScaledRect
|
||||||
{suspense.rects !== null &&
|
key={index}
|
||||||
suspense.rects.map((rect, index) => {
|
className={styles.SuspenseRectsRect}
|
||||||
return <SuspenseRect key={index} rect={rect} />;
|
rect={rect}
|
||||||
})}
|
data-highlighted={selected}
|
||||||
{suspense.children.map(childID => {
|
onClick={handleClick}
|
||||||
return <SuspenseRects key={childID} suspenseID={childID} />;
|
onPointerOver={handlePointerOver}
|
||||||
})}
|
onPointerLeave={handlePointerLeave}
|
||||||
</g>
|
// Reach-UI tooltip will go out of bounds of parent scroll container.
|
||||||
|
title={suspense.name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{suspense.children.length > 0 && (
|
||||||
|
<ScaledRect
|
||||||
|
className={styles.SuspenseRectsBoundaryChildren}
|
||||||
|
rect={boundingBox}>
|
||||||
|
{suspense.children.map(childID => {
|
||||||
|
return <SuspenseRects key={childID} suspenseID={childID} />;
|
||||||
|
})}
|
||||||
|
</ScaledRect>
|
||||||
|
)}
|
||||||
|
</ViewBox.Provider>
|
||||||
|
</ScaledRect>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBoundingBox(rects: $ReadOnlyArray<Rect> | 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(
|
function getDocumentBoundingRect(
|
||||||
store: Store,
|
store: Store,
|
||||||
roots: $ReadOnlyArray<SuspenseNode['id']>,
|
roots: $ReadOnlyArray<SuspenseNode['id']>,
|
||||||
|
|
@ -169,42 +229,42 @@ function SuspenseRectsShell({
|
||||||
const store = useContext(StoreContext);
|
const store = useContext(StoreContext);
|
||||||
const root = store.getSuspenseByID(rootID);
|
const root = store.getSuspenseByID(rootID);
|
||||||
if (root === null) {
|
if (root === null) {
|
||||||
console.warn(`<Element> Could not find suspense node id ${rootID}`);
|
// getSuspenseByID will have already warned
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return root.children.map(childID => {
|
||||||
<g>
|
return <SuspenseRects key={childID} suspenseID={childID} />;
|
||||||
{root.children.map(childID => {
|
});
|
||||||
return <SuspenseRects key={childID} suspenseID={childID} />;
|
|
||||||
})}
|
|
||||||
</g>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ViewBox = createContext<Rect>((null: any));
|
||||||
|
|
||||||
function SuspenseRectsContainer(): React$Node {
|
function SuspenseRectsContainer(): React$Node {
|
||||||
const store = useContext(StoreContext);
|
const store = useContext(StoreContext);
|
||||||
// TODO: This relies on a full re-render of all children when the Suspense tree changes.
|
// TODO: This relies on a full re-render of all children when the Suspense tree changes.
|
||||||
const {roots} = useContext(SuspenseTreeStateContext);
|
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 width = '100%';
|
||||||
const boundingRectWidth = boundingRect.width;
|
const aspectRatio = `1 / ${heightScale}`;
|
||||||
const height =
|
|
||||||
(boundingRectWidth === 0 ? 0 : boundingRect.height / boundingRect.width) *
|
|
||||||
100 +
|
|
||||||
'%';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.SuspenseRectsContainer}>
|
<div className={styles.SuspenseRectsContainer}>
|
||||||
<svg
|
<ViewBox.Provider value={boundingBox}>
|
||||||
style={{width, height}}
|
<div
|
||||||
viewBox={`${boundingRect.x} ${boundingRect.y} ${boundingRect.width} ${boundingRect.height}`}>
|
className={styles.SuspenseRectsViewBox}
|
||||||
{roots.map(rootID => {
|
style={{aspectRatio, width}}>
|
||||||
return <SuspenseRectsShell key={rootID} rootID={rootID} />;
|
{roots.map(rootID => {
|
||||||
})}
|
return <SuspenseRectsShell key={rootID} rootID={rootID} />;
|
||||||
</svg>
|
})}
|
||||||
|
</div>
|
||||||
|
</ViewBox.Provider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user