@@ -12,9 +12,13 @@ import type {
12
12
SuspenseNode ,
13
13
Rect ,
14
14
} from 'react-devtools-shared/src/frontend/types' ;
15
+ import typeof {
16
+ SyntheticMouseEvent ,
17
+ SyntheticPointerEvent ,
18
+ } from 'react-dom-bindings/src/events/SyntheticEvent' ;
15
19
16
20
import * as React from 'react' ;
17
- import { useContext } from 'react' ;
21
+ import { createContext , useContext } from 'react' ;
18
22
import {
19
23
TreeDispatcherContext ,
20
24
TreeStateContext ,
@@ -26,19 +30,32 @@ import {
26
30
SuspenseTreeStateContext ,
27
31
SuspenseTreeDispatcherContext ,
28
32
} from './SuspenseTreeContext' ;
29
- import typeof {
30
- SyntheticMouseEvent ,
31
- SyntheticPointerEvent ,
32
- } from 'react-dom-bindings/src/events/SyntheticEvent' ;
33
33
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
+
35
49
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
+ } }
42
59
/>
43
60
) ;
44
61
}
@@ -97,24 +114,67 @@ function SuspenseRects({
97
114
// TODO: Use the nearest Suspense boundary
98
115
const selected = inspectedElementID === suspenseID ;
99
116
117
+ const boundingBox = getBoundingBox ( suspense . rects ) ;
118
+
100
119
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 >
115
149
) ;
116
150
}
117
151
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
+
118
178
function getDocumentBoundingRect (
119
179
store : Store ,
120
180
roots : $ReadOnlyArray < SuspenseNode [ 'id' ] > ,
@@ -169,42 +229,42 @@ function SuspenseRectsShell({
169
229
const store = useContext ( StoreContext ) ;
170
230
const root = store . getSuspenseByID ( rootID ) ;
171
231
if ( root === null ) {
172
- console . warn ( `<Element> Could not find suspense node id ${ rootID } ` ) ;
232
+ // getSuspenseByID will have already warned
173
233
return null ;
174
234
}
175
235
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
+ } ) ;
183
239
}
184
240
241
+ const ViewBox = createContext < Rect > ( ( null : any ) ) ;
242
+
185
243
function SuspenseRectsContainer ( ) : React$Node {
186
244
const store = useContext ( StoreContext ) ;
187
245
// TODO: This relies on a full re-render of all children when the Suspense tree changes.
188
246
const { roots} = useContext ( SuspenseTreeStateContext ) ;
189
247
190
- const boundingRect = getDocumentBoundingRect ( store , roots ) ;
248
+ const boundingBox = getDocumentBoundingRect ( store , roots ) ;
191
249
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
192
254
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 } ` ;
198
256
199
257
return (
200
258
< 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 >
208
268
</ div >
209
269
) ;
210
270
}
0 commit comments