@@ -271,13 +271,17 @@ function createVirtualInstance(
271
271
272
272
type DevToolsInstance = FiberInstance | VirtualInstance | FilteredFiberInstance ;
273
273
274
+ // A Generic Rect super type which can include DOMRect and other objects with similar shape like in React Native.
275
+ type Rect = { x : number , y : number , width : number , height : number , ...} ;
276
+
274
277
type SuspenseNode = {
275
278
// The Instance can be a Suspense boundary, a SuspenseList Row, or HostRoot.
276
279
// It can also be disconnected from the main tree if it's a Filtered Instance.
277
280
instance : FiberInstance | FilteredFiberInstance ,
278
281
parent : null | SuspenseNode ,
279
282
firstChild : null | SuspenseNode ,
280
283
nextSibling : null | SuspenseNode ,
284
+ rects : null | Array < Rect > , // The bounding rects of content children.
281
285
suspendedBy : Map < ReactIOInfo , Set < DevToolsInstance> > , // Tracks which data we're suspended by and the children that suspend it.
282
286
// Track whether any of the items in suspendedBy are unique this this Suspense boundaries or if they're all
283
287
// also in the parent sets. This determine whether this could contribute in the loading sequence.
@@ -292,6 +296,7 @@ function createSuspenseNode(
292
296
parent : null ,
293
297
firstChild : null ,
294
298
nextSibling : null ,
299
+ rects : null ,
295
300
suspendedBy : new Map ( ) ,
296
301
hasUniqueSuspenders : false ,
297
302
} ) ;
@@ -2130,6 +2135,69 @@ export function attach(
2130
2135
pendingStringTableLength = 0;
2131
2136
}
2132
2137
2138
+ function measureHostInstance(instance: HostInstance): null | Array<Rect> {
2139
+ // Feature detect measurement capabilities of this environment.
2140
+ // TODO: Consider making this capability injected by the ReactRenderer.
2141
+ if (typeof instance !== 'object' || instance === null) {
2142
+ return null;
2143
+ }
2144
+ if (typeof instance.getClientRects === 'function') {
2145
+ // DOM
2146
+ const result = [];
2147
+ const doc = instance.ownerDocument;
2148
+ const win = doc && doc.defaultView;
2149
+ const scrollX = win ? win.scrollX : 0;
2150
+ const scrollY = win ? win.scrollY : 0;
2151
+ const rects = instance.getClientRects();
2152
+ for (let i = 0; i < rects.length; i++) {
2153
+ const rect = rects[i];
2154
+ result.push({
2155
+ x: rect.x + scrollX,
2156
+ y: rect.y + scrollY,
2157
+ width: rect.width,
2158
+ height: rect.height,
2159
+ });
2160
+ }
2161
+ return result;
2162
+ }
2163
+ if (instance.canonical) {
2164
+ // Native
2165
+ const publicInstance = instance.canonical.publicInstance;
2166
+ if (!publicInstance) {
2167
+ // The publicInstance may not have been initialized yet if there was no ref on this node.
2168
+ // We can't initialize it from any existing Hook but we could fallback to this async form:
2169
+ // renderer.extraDevToolsConfig.getInspectorDataForInstance(instance).hierarchy[last].getInspectorData().measure(callback)
2170
+ return null;
2171
+ }
2172
+ if (typeof publicInstance.getBoundingClientRect === 'function') {
2173
+ // enableAccessToHostTreeInFabric / ReadOnlyElement
2174
+ return [publicInstance.getBoundingClientRect()];
2175
+ }
2176
+ if (typeof publicInstance.unstable_getBoundingClientRect === 'function') {
2177
+ // ReactFabricHostComponent
2178
+ return [publicInstance.unstable_getBoundingClientRect()];
2179
+ }
2180
+ }
2181
+ return null;
2182
+ }
2183
+
2184
+ function measureInstance(instance: DevToolsInstance): null | Array<Rect> {
2185
+ // Synchronously return the client rects of the Host instances directly inside this Instance.
2186
+ const hostInstances = findAllCurrentHostInstances(instance);
2187
+ let result: null | Array<Rect> = null;
2188
+ for (let i = 0; i < hostInstances.length; i++) {
2189
+ const childResult = measureHostInstance(hostInstances[i]);
2190
+ if (childResult !== null) {
2191
+ if (result === null) {
2192
+ result = childResult;
2193
+ } else {
2194
+ result = result.concat(childResult);
2195
+ }
2196
+ }
2197
+ }
2198
+ return result;
2199
+ }
2200
+
2133
2201
function getStringID(string: string | null): number {
2134
2202
if (string === null) {
2135
2203
return 0;
@@ -2439,6 +2507,10 @@ export function attach(
2439
2507
}
2440
2508
}
2441
2509
2510
+ function recordSuspenseResize(suspenseNode: SuspenseNode): void {
2511
+ // TODO: Notify the front end of the change.
2512
+ }
2513
+
2442
2514
// Running state of the remaining children from the previous version of this parent that
2443
2515
// we haven't yet added back. This should be reset anytime we change parent.
2444
2516
// Any remaining ones at the end will be deleted.
@@ -2768,6 +2840,79 @@ export function attach(
2768
2840
return false;
2769
2841
}
2770
2842
2843
+ function areEqualRects(
2844
+ a: null | Array<Rect>,
2845
+ b: null | Array<Rect>,
2846
+ ): boolean {
2847
+ if (a === null) {
2848
+ return b === null;
2849
+ }
2850
+ if (b === null) {
2851
+ return false;
2852
+ }
2853
+ if (a.length !== b.length) {
2854
+ return false;
2855
+ }
2856
+ for (let i = 0; i < a.length; i++) {
2857
+ const aRect = a[i];
2858
+ const bRect = b[i];
2859
+ if (
2860
+ aRect.x !== bRect.x ||
2861
+ aRect.y !== bRect.y ||
2862
+ aRect.width !== bRect.width ||
2863
+ aRect.height !== bRect.height
2864
+ ) {
2865
+ return false;
2866
+ }
2867
+ }
2868
+ return true;
2869
+ }
2870
+
2871
+ function measureUnchangedSuspenseNodesRecursively(
2872
+ suspenseNode: SuspenseNode,
2873
+ ): void {
2874
+ if (isInDisconnectedSubtree) {
2875
+ // We don't update rects inside disconnected subtrees.
2876
+ return;
2877
+ }
2878
+ const nextRects = measureInstance(suspenseNode.instance);
2879
+ const prevRects = suspenseNode.rects;
2880
+ if (areEqualRects(prevRects, nextRects)) {
2881
+ return; // Unchanged
2882
+ }
2883
+ // The rect has changed. While the bailed out root wasn't in a disconnected subtree,
2884
+ // it's possible that this node was in one. So we need to check if we're offscreen.
2885
+ let parent = suspenseNode.instance.parent;
2886
+ while (parent !== null) {
2887
+ if (
2888
+ (parent.kind === FIBER_INSTANCE ||
2889
+ parent.kind === FILTERED_FIBER_INSTANCE) &&
2890
+ parent.data.tag === OffscreenComponent &&
2891
+ parent.data.memoizedState !== null
2892
+ ) {
2893
+ // We're inside a hidden offscreen Fiber. We're in a disconnected tree.
2894
+ return;
2895
+ }
2896
+ if (parent.suspenseNode !== null) {
2897
+ // Found our parent SuspenseNode. We can bail out now.
2898
+ break;
2899
+ }
2900
+ parent = parent.parent;
2901
+ }
2902
+ // We changed inside a visible tree.
2903
+ // Since this boundary changed, it's possible it also affected its children so lets
2904
+ // measure them as well.
2905
+ for (
2906
+ let child = suspenseNode.firstChild;
2907
+ child !== null;
2908
+ child = child.nextSibling
2909
+ ) {
2910
+ measureUnchangedSuspenseNodesRecursively(child);
2911
+ }
2912
+ suspenseNode.rects = nextRects;
2913
+ recordSuspenseResize(suspenseNode);
2914
+ }
2915
+
2771
2916
function consumeSuspenseNodesOfExistingInstance(
2772
2917
instance: DevToolsInstance,
2773
2918
): void {
@@ -2806,6 +2951,9 @@ export function attach(
2806
2951
previouslyReconciledSiblingSuspenseNode.nextSibling = suspenseNode;
2807
2952
}
2808
2953
previouslyReconciledSiblingSuspenseNode = suspenseNode;
2954
+ // While React didn't rerender this node, it's possible that it was affected by
2955
+ // layout due to mutation of a parent or sibling. Check if it changed size.
2956
+ measureUnchangedSuspenseNodesRecursively(suspenseNode);
2809
2957
// Continue
2810
2958
suspenseNode = nextRemainingSibling;
2811
2959
} else if (foundOne) {
@@ -3029,6 +3177,10 @@ export function attach(
3029
3177
newInstance = recordMount(fiber, reconcilingParent);
3030
3178
if (fiber.tag === SuspenseComponent || fiber.tag === HostRoot) {
3031
3179
newSuspenseNode = createSuspenseNode(newInstance);
3180
+ // Measure this Suspense node. In general we shouldn't do this until we have
3181
+ // inserted the new children but since we know this is a FiberInstance we'll
3182
+ // just use the Fiber anyway.
3183
+ newSuspenseNode.rects = measureInstance(newInstance);
3032
3184
}
3033
3185
insertChild(newInstance);
3034
3186
if (__DEBUG__) {
@@ -3058,6 +3210,10 @@ export function attach(
3058
3210
newInstance = createFilteredFiberInstance(fiber);
3059
3211
if (fiber.tag === SuspenseComponent) {
3060
3212
newSuspenseNode = createSuspenseNode(newInstance);
3213
+ // Measure this Suspense node. In general we shouldn't do this until we have
3214
+ // inserted the new children but since we know this is a FiberInstance we'll
3215
+ // just use the Fiber anyway.
3216
+ newSuspenseNode.rects = measureInstance(newInstance);
3061
3217
}
3062
3218
insertChild(newInstance);
3063
3219
if (__DEBUG__) {
@@ -4084,6 +4240,23 @@ export function attach(
4084
4240
) {
4085
4241
shouldResetChildren = true;
4086
4242
}
4243
+ } else if (
4244
+ nextFiber.memoizedState === null &&
4245
+ fiberInstance.suspenseNode !== null
4246
+ ) {
4247
+ if (!isInDisconnectedSubtree) {
4248
+ // Measure this Suspense node in case it changed. We don't update the rect while
4249
+ // we're inside a disconnected subtree nor if we are the Suspense boundary that
4250
+ // is suspended. This lets us keep the rectangle of the displayed content while
4251
+ // we're suspended to visualize the resulting state.
4252
+ const suspenseNode = fiberInstance.suspenseNode;
4253
+ const prevRects = suspenseNode.rects;
4254
+ const nextRects = measureInstance(fiberInstance);
4255
+ if (!areEqualRects(prevRects, nextRects)) {
4256
+ suspenseNode.rects = nextRects;
4257
+ recordSuspenseResize(suspenseNode);
4258
+ }
4259
+ }
4087
4260
}
4088
4261
} else {
4089
4262
// Common case: Primary -> Primary.
@@ -4179,6 +4352,21 @@ export function attach(
4179
4352
previouslyReconciledSibling = stashedPrevious;
4180
4353
remainingReconcilingChildren = stashedRemaining;
4181
4354
if (shouldPopSuspenseNode) {
4355
+ if (
4356
+ !isInDisconnectedSubtree &&
4357
+ reconcilingParentSuspenseNode !== null
4358
+ ) {
4359
+ // Measure this Suspense node in case it changed. We don't update the rect
4360
+ // while we're inside a disconnected subtree so that we keep the outline
4361
+ // as it was before we hid the parent.
4362
+ const suspenseNode = reconcilingParentSuspenseNode;
4363
+ const prevRects = suspenseNode.rects;
4364
+ const nextRects = measureInstance(fiberInstance);
4365
+ if (!areEqualRects(prevRects, nextRects)) {
4366
+ suspenseNode.rects = nextRects;
4367
+ recordSuspenseResize(suspenseNode);
4368
+ }
4369
+ }
4182
4370
reconcilingParentSuspenseNode = stashedSuspenseParent;
4183
4371
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
4184
4372
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
0 commit comments