@@ -271,13 +271,17 @@ function createVirtualInstance(
271271
272272type DevToolsInstance = FiberInstance | VirtualInstance | FilteredFiberInstance ;
273273
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+
274277type SuspenseNode = {
275278 // The Instance can be a Suspense boundary, a SuspenseList Row, or HostRoot.
276279 // It can also be disconnected from the main tree if it's a Filtered Instance.
277280 instance : FiberInstance | FilteredFiberInstance ,
278281 parent : null | SuspenseNode ,
279282 firstChild : null | SuspenseNode ,
280283 nextSibling : null | SuspenseNode ,
284+ rects : null | Array < Rect > , // The bounding rects of content children.
281285 suspendedBy : Map < ReactIOInfo , Set < DevToolsInstance> > , // Tracks which data we're suspended by and the children that suspend it.
282286 // Track whether any of the items in suspendedBy are unique this this Suspense boundaries or if they're all
283287 // also in the parent sets. This determine whether this could contribute in the loading sequence.
@@ -292,6 +296,7 @@ function createSuspenseNode(
292296 parent : null ,
293297 firstChild : null ,
294298 nextSibling : null ,
299+ rects : null ,
295300 suspendedBy : new Map ( ) ,
296301 hasUniqueSuspenders : false ,
297302 } ) ;
@@ -2130,6 +2135,69 @@ export function attach(
21302135 pendingStringTableLength = 0;
21312136 }
21322137
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+
21332201 function getStringID(string: string | null): number {
21342202 if (string === null) {
21352203 return 0;
@@ -2439,6 +2507,10 @@ export function attach(
24392507 }
24402508 }
24412509
2510+ function recordSuspenseResize(suspenseNode: SuspenseNode): void {
2511+ // TODO: Notify the front end of the change.
2512+ }
2513+
24422514 // Running state of the remaining children from the previous version of this parent that
24432515 // we haven't yet added back. This should be reset anytime we change parent.
24442516 // Any remaining ones at the end will be deleted.
@@ -2768,6 +2840,79 @@ export function attach(
27682840 return false;
27692841 }
27702842
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+
27712916 function consumeSuspenseNodesOfExistingInstance(
27722917 instance: DevToolsInstance,
27732918 ): void {
@@ -2806,6 +2951,9 @@ export function attach(
28062951 previouslyReconciledSiblingSuspenseNode.nextSibling = suspenseNode;
28072952 }
28082953 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);
28092957 // Continue
28102958 suspenseNode = nextRemainingSibling;
28112959 } else if (foundOne) {
@@ -3029,6 +3177,10 @@ export function attach(
30293177 newInstance = recordMount(fiber, reconcilingParent);
30303178 if (fiber.tag === SuspenseComponent || fiber.tag === HostRoot) {
30313179 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);
30323184 }
30333185 insertChild(newInstance);
30343186 if (__DEBUG__) {
@@ -3058,6 +3210,10 @@ export function attach(
30583210 newInstance = createFilteredFiberInstance(fiber);
30593211 if (fiber.tag === SuspenseComponent) {
30603212 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);
30613217 }
30623218 insertChild(newInstance);
30633219 if (__DEBUG__) {
@@ -4084,6 +4240,23 @@ export function attach(
40844240 ) {
40854241 shouldResetChildren = true;
40864242 }
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+ }
40874260 }
40884261 } else {
40894262 // Common case: Primary -> Primary.
@@ -4179,6 +4352,21 @@ export function attach(
41794352 previouslyReconciledSibling = stashedPrevious;
41804353 remainingReconcilingChildren = stashedRemaining;
41814354 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+ }
41824370 reconcilingParentSuspenseNode = stashedSuspenseParent;
41834371 previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
41844372 remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
0 commit comments