Repository: facebook/react
Pull request: Measure and apply names for the "new" phase

Stacked on #32599 and #32611.

This is able to reuse the code from CommitViewTransitions for "enter", "shared" and "layout". The difference is that for "enter"/"shared" in the "new" phase we pass in the deletions.

For "layout" of nested boundaries we just need to measure the clones at the same time we measure the original nodes since we haven't measured them in a previous phase in the current approach.

With these updates, things move around more like expected in the fixture because we're now applying the appropriate pairs to trigger individual animations instead of just the full document cross-fade.

The "update" phase is a little more complicated and is coming soon.

Changed files:
filename: packages/react-reconciler/src/ReactFiberApplyGesture.js
patch: @@ -62,6 +62,8 @@ import {
   restoreEnterOrExitViewTransitions,
   restoreNestedViewTransitions,
   appearingViewTransitions,
+  commitEnterViewTransitions,
+  measureNestedViewTransitions,
 } from './ReactFiberCommitViewTransitions';
 import {
   getViewTransitionName,
@@ -968,53 +970,6 @@ export function insertDestinationClones(
   }
 }
 
-function applyDeletedPairViewTransitions(deletion: Fiber): void {
-  if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
-    // This has no named view transitions in its subtree.
-    return;
-  }
-  let child = deletion.child;
-  while (child !== null) {
-    if (child.tag === OffscreenComponent && child.memoizedState === null) {
-      // This tree was already hidden so we skip it.
-    } else {
-      if (
-        child.tag === ViewTransitionComponent &&
-        (child.flags & ViewTransitionNamedStatic) !== NoFlags
-      ) {
-        const props: ViewTransitionProps = child.memoizedProps;
-        const name = props.name;
-        if (name != null && name !== 'auto') {
-          // TODO: Find a pair
-        }
-      }
-      applyDeletedPairViewTransitions(child);
-    }
-    child = child.sibling;
-  }
-}
-
-function applyEnterViewTransitions(deletion: Fiber): void {
-  if (deletion.tag === ViewTransitionComponent) {
-    const props: ViewTransitionProps = deletion.memoizedProps;
-    const name = props.name;
-    if (name != null && name !== 'auto') {
-      // TODO: Find a pair
-    }
-    // Look for more pairs deeper in the tree.
-    applyDeletedPairViewTransitions(deletion);
-  } else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
-    // TODO: Check if this is a hidden Offscreen or a Portal.
-    let child = deletion.child;
-    while (child !== null) {
-      applyEnterViewTransitions(child);
-      child = child.sibling;
-    }
-  } else {
-    applyDeletedPairViewTransitions(deletion);
-  }
-}
-
 function measureExitViewTransitions(placement: Fiber): void {
   if (placement.tag === ViewTransitionComponent) {
     // const state: ViewTransitionState = placement.stateNode;
@@ -1036,24 +991,6 @@ function measureExitViewTransitions(placement: Fiber): void {
   }
 }
 
-function measureNestedViewTransitions(changedParent: Fiber): void {
-  let child = changedParent.child;
-  while (child !== null) {
-    if (child.tag === ViewTransitionComponent) {
-      const current = child.alternate;
-      if (current !== null) {
-        // const props: ViewTransitionProps = child.memoizedProps;
-        // const name = getViewTransitionName(props, child.stateNode);
-        // TODO: Measure both the old and new state and see if they're different.
-      }
-    } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
-      // TODO: Check if this is a hidden Offscreen or a Portal.
-      measureNestedViewTransitions(child);
-    }
-    child = child.sibling;
-  }
-}
-
 function measureUpdateViewTransition(
   current: Fiber,
   finishedWork: Fiber,
@@ -1066,7 +1003,7 @@ function recursivelyApplyViewTransitions(parentFiber: Fiber) {
   if (deletions !== null) {
     for (let i = 0; i < deletions.length; i++) {
       const childToDelete = deletions[i];
-      applyEnterViewTransitions(childToDelete);
+      commitEnterViewTransitions(childToDelete, true);
     }
   }
 
@@ -1084,7 +1021,7 @@ function recursivelyApplyViewTransitions(parentFiber: Fiber) {
     // Nothing has changed in this subtree, but the parent may have still affected
     // its size and position. We need to measure the old and new state to see if
     // we should animate its size and position.
-    measureNestedViewTransitions(parentFiber);
+    measureNestedViewTransitions(parentFiber, true);
   }
 }
 
@@ -1121,7 +1058,7 @@ function applyViewTransitionsOnFiber(finishedWork: Fiber) {
           measureExitViewTransitions(finishedWork);
         } else if (current !== null && current.memoizedState === null) {
           // Was previously mounted as visible but is now hidden.
-          applyEnterViewTransitions(current);
+          commitEnterViewTransitions(current, true);
         }
       }
       break;

filename: packages/react-reconciler/src/ReactFiberCommitViewTransitions.js
patch: @@ -257,7 +257,10 @@ function commitAppearingPairViewTransitions(placement: Fiber): void {
   }
 }
 
-export function commitEnterViewTransitions(placement: Fiber): void {
+export function commitEnterViewTransitions(
+  placement: Fiber,
+  gesture: boolean,
+): void {
   if (placement.tag === ViewTransitionComponent) {
     const state: ViewTransitionState = placement.stateNode;
     const props: ViewTransitionProps = placement.memoizedProps;
@@ -284,7 +287,11 @@ export function commitEnterViewTransitions(placement: Fiber): void {
         commitAppearingPairViewTransitions(placement);
 
         if (!state.paired) {
-          scheduleViewTransitionEvent(placement, props.onEnter);
+          if (gesture) {
+            // TODO: Schedule gesture events.
+          } else {
+            scheduleViewTransitionEvent(placement, props.onEnter);
+          }
         }
       }
     } else {
@@ -293,7 +300,7 @@ export function commitEnterViewTransitions(placement: Fiber): void {
   } else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
     let child = placement.child;
     while (child !== null) {
-      commitEnterViewTransitions(child);
+      commitEnterViewTransitions(child, gesture);
       child = child.sibling;
     }
   } else {
@@ -703,6 +710,8 @@ function measureViewTransitionHostInstancesRecursive(
       }
       if ((parentViewTransition.flags & Update) !== NoFlags) {
         // We might update this node so we need to apply its new name for the new state.
+        // Additionally in the ApplyGesture case we also need to do this because the clone
+        // will have the name but this one won't.
         applyViewTransitionName(
           instance,
           viewTransitionHostInstanceIdx === 0
@@ -828,32 +837,51 @@ export function measureUpdateViewTransition(
   return inViewport;
 }
 
-export function measureNestedViewTransitions(changedParent: Fiber): void {
+export function measureNestedViewTransitions(
+  changedParent: Fiber,
+  gesture: boolean,
+): void {
   let child = changedParent.child;
   while (child !== null) {
     if (child.tag === ViewTransitionComponent) {
       const props: ViewTransitionProps = child.memoizedProps;
-      const name = getViewTransitionName(props, child.stateNode);
+      const state: ViewTransitionState = child.stateNode;
+      const name = getViewTransitionName(props, state);
       const className: ?string = getViewTransitionClassName(
         props.className,
         props.layout,
       );
+      let previousMeasurements: null | Array<InstanceMeasurement>;
+      if (gesture) {
+        const clones = state.clones;
+        if (clones === null) {
+          previousMeasurements = null;
+        } else {
+          previousMeasurements = clones.map(measureInstance);
+        }
+      } else {
+        previousMeasurements = child.memoizedState;
+      }
       const inViewport = measureViewTransitionHostInstances(
         child,
         child.child,
         name,
         name, // Since this is unchanged, new and old name is the same.
         className,
-        child.memoizedState,
+        previousMeasurements,
         false,
       );
       if ((child.flags & Update) === NoFlags || !inViewport) {
         // Nothing changed.
       } else {
-        scheduleViewTransitionEvent(child, props.onLayout);
+        if (gesture) {
+          // TODO: Schedule gesture events.
+        } else {
+          scheduleViewTransitionEvent(child, props.onLayout);
+        }
       }
     } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
-      measureNestedViewTransitions(child);
+      measureNestedViewTransitions(child, gesture);
     }
     child = child.sibling;
   }

filename: packages/react-reconciler/src/ReactFiberCommitWork.js
patch: @@ -2450,7 +2450,7 @@ function recursivelyTraverseAfterMutationEffects(
     // Nothing has changed in this subtree, but the parent may have still affected
     // its size and position. We need to measure this and if not, restore it to
     // not animate.
-    measureNestedViewTransitions(parentFiber);
+    measureNestedViewTransitions(parentFiber, false);
   }
 }
 
@@ -2468,7 +2468,7 @@ function commitAfterMutationEffectsOnFiber(
     // we can just bail after we're done with the first one.
     // The first ViewTransition inside a newly mounted tree runs an enter transition
     // but other nested ones don't unless they have a named pair.
-    commitEnterViewTransitions(finishedWork);
+    commitEnterViewTransitions(finishedWork, false);
     return;
   }
 
@@ -2512,7 +2512,7 @@ function commitAfterMutationEffectsOnFiber(
           // The Offscreen tree is visible.
           const wasHidden = current.memoizedState !== null;
           if (wasHidden) {
-            commitEnterViewTransitions(finishedWork);
+            commitEnterViewTransitions(finishedWork, false);
             // If it was previous hidden then the children are treated as enter
             // not updates so we don't need to visit these children.
           } else {