@@ -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
1620import * as React from 'react' ;
17- import { useContext } from 'react' ;
21+ import { createContext , useContext } from 'react' ;
1822import {
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+ < ScaledRect rect = { boundingBox } className = { styles . SuspenseRectsBoundary } >
121+ < ViewBox . Provider value = { boundingBox } >
122+ { suspense . rects !== null &&
123+ suspense . rects . map ( ( rect , index ) => {
124+ return (
125+ < ScaledRect
126+ key = { index }
127+ className = { styles . SuspenseRectsRect }
128+ rect = { rect }
129+ data-highlighted = { selected }
130+ onClick = { handleClick }
131+ onPointerOver = { handlePointerOver }
132+ onPointerLeave = { handlePointerLeave }
133+ // Reach-UI tooltip will go out of bounds of parent scroll container.
134+ title = { suspense . name }
135+ />
136+ ) ;
137+ } ) }
138+ { suspense . children . length > 0 && (
139+ < ScaledRect
140+ className = { styles . SuspenseRectsBoundaryChildren }
141+ rect = { boundingBox } >
142+ { suspense . children . map ( childID => {
143+ return < SuspenseRects key = { childID } suspenseID = { childID } /> ;
144+ } ) }
145+ </ ScaledRect >
146+ ) }
147+ </ ViewBox . Provider >
148+ </ ScaledRect >
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+
118178function getDocumentBoundingRect (
119179 store : Store ,
120180 roots : $ReadOnlyArray < SuspenseNode [ 'id' ] > ,
@@ -169,42 +229,42 @@ function SuspenseRectsShell({
169229 const store = useContext ( StoreContext ) ;
170230 const root = store . getSuspenseByID ( rootID ) ;
171231 if ( root === null ) {
172- console . warn ( `<Element> Could not find suspense node id ${ rootID } ` ) ;
232+ // getSuspenseByID will have already warned
173233 return null ;
174234 }
175235
176- return (
177- < g >
178- { root . children . map ( childID => {
179- return < SuspenseRects key = { childID } suspenseID = { childID } /> ;
180- } ) }
181- </ g >
182- ) ;
236+ return root . children . map ( childID => {
237+ return < SuspenseRects key = { childID } suspenseID = { childID } /> ;
238+ } ) ;
183239}
184240
241+ const ViewBox = createContext < Rect > ( ( null : any ) ) ;
242+
185243function SuspenseRectsContainer ( ) : React$Node {
186244 const store = useContext ( StoreContext ) ;
187245 // TODO: This relies on a full re-render of all children when the Suspense tree changes.
188246 const { roots} = useContext ( SuspenseTreeStateContext ) ;
189247
190- const boundingRect = getDocumentBoundingRect ( store , roots ) ;
248+ const boundingBox = getDocumentBoundingRect ( store , roots ) ;
191249
250+ const boundingBoxWidth = boundingBox . width ;
251+ const heightScale =
252+ boundingBoxWidth === 0 ? 1 : boundingBox . height / boundingBoxWidth ;
253+ // Scales the inspected document to fit into the available width
192254 const width = '100%' ;
193- const boundingRectWidth = boundingRect . width ;
194- const height =
195- ( boundingRectWidth === 0 ? 0 : boundingRect . height / boundingRect . width ) *
196- 100 +
197- '%' ;
255+ const aspectRatio = `1 / ${ heightScale } ` ;
198256
199257 return (
200258 < div className = { styles . SuspenseRectsContainer } >
201- < svg
202- style = { { width, height} }
203- viewBox = { `${ boundingRect . x } ${ boundingRect . y } ${ boundingRect . width } ${ boundingRect . height } ` } >
204- { roots . map ( rootID => {
205- return < SuspenseRectsShell key = { rootID } rootID = { rootID } /> ;
206- } ) }
207- </ svg >
259+ < ViewBox . Provider value = { boundingBox } >
260+ < div
261+ className = { styles . SuspenseRectsViewBox }
262+ style = { { aspectRatio, width} } >
263+ { roots . map ( rootID => {
264+ return < SuspenseRectsShell key = { rootID } rootID = { rootID } /> ;
265+ } ) }
266+ </ div >
267+ </ ViewBox . Provider >
208268 </ div >
209269 ) ;
210270}
0 commit comments