diff --git a/packages/react-art/src/ReactFiberConfigART.js b/packages/react-art/src/ReactFiberConfigART.js
index 5b8788453f6af..a168975221f34 100644
--- a/packages/react-art/src/ReactFiberConfigART.js
+++ b/packages/react-art/src/ReactFiberConfigART.js
@@ -518,6 +518,10 @@ export function measureInstance(instance) {
return null;
}
+export function measureClonedInstance(instance) {
+ return null;
+}
+
export function wasInstanceInViewport(measurement): boolean {
return true;
}
diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
index 2d34bc94005be..3615e06481629 100644
--- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
@@ -1477,10 +1477,12 @@ export type InstanceMeasurement = {
view: boolean, // is in viewport bounds
};
-export function measureInstance(instance: Instance): InstanceMeasurement {
- const ownerWindow = instance.ownerDocument.defaultView;
- const rect = instance.getBoundingClientRect();
- const computedStyle = getComputedStyle(instance);
+function createMeasurement(
+ rect: ClientRect | DOMRect,
+ computedStyle: CSSStyleDeclaration,
+ element: Element,
+): InstanceMeasurement {
+ const ownerWindow = element.ownerDocument.defaultView;
return {
rect: rect,
abs:
@@ -1508,6 +1510,26 @@ export function measureInstance(instance: Instance): InstanceMeasurement {
};
}
+export function measureInstance(instance: Instance): InstanceMeasurement {
+ const rect = instance.getBoundingClientRect();
+ const computedStyle = getComputedStyle(instance);
+ return createMeasurement(rect, computedStyle, instance);
+}
+
+export function measureClonedInstance(instance: Instance): InstanceMeasurement {
+ const measuredRect = instance.getBoundingClientRect();
+ // Adjust the DOMRect based on the translate that put it outside the viewport.
+ // TODO: This might not be completely correct if the parent also has a transform.
+ const rect = new DOMRect(
+ measuredRect.x + 20000,
+ measuredRect.y + 20000,
+ measuredRect.width,
+ measuredRect.height,
+ );
+ const computedStyle = getComputedStyle(instance);
+ return createMeasurement(rect, computedStyle, instance);
+}
+
export function wasInstanceInViewport(
measurement: InstanceMeasurement,
): boolean {
diff --git a/packages/react-native-renderer/src/ReactFiberConfigNative.js b/packages/react-native-renderer/src/ReactFiberConfigNative.js
index c08e1f0f82474..b15a6f9f76b02 100644
--- a/packages/react-native-renderer/src/ReactFiberConfigNative.js
+++ b/packages/react-native-renderer/src/ReactFiberConfigNative.js
@@ -620,6 +620,10 @@ export function measureInstance(instance: Instance): InstanceMeasurement {
return null;
}
+export function measureClonedInstance(instance: Instance): InstanceMeasurement {
+ return null;
+}
+
export function wasInstanceInViewport(
measurement: InstanceMeasurement,
): boolean {
diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js
index 2ddbea79289fa..266f860fe6d32 100644
--- a/packages/react-noop-renderer/src/createReactNoop.js
+++ b/packages/react-noop-renderer/src/createReactNoop.js
@@ -796,6 +796,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
return null;
},
+ measureClonedInstance(instance: Instance): InstanceMeasurement {
+ return null;
+ },
+
wasInstanceInViewport(measurement: InstanceMeasurement): boolean {
return true;
},
diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js
index c9b1d7688f580..ea4a0ce70ae5f 100644
--- a/packages/react-reconciler/src/ReactFiberApplyGesture.js
+++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js
@@ -9,7 +9,7 @@
import type {Fiber, FiberRoot} from './ReactInternalTypes';
-import type {Instance, TextInstance} from './ReactFiberConfig';
+import type {Instance, TextInstance, Props} from './ReactFiberConfig';
import type {OffscreenState} from './ReactFiberActivityComponent';
@@ -25,6 +25,7 @@ import {
removeRootViewTransitionClone,
cancelRootViewTransitionName,
restoreRootViewTransitionName,
+ cancelViewTransitionName,
applyViewTransitionName,
appendChild,
commitUpdate,
@@ -39,6 +40,7 @@ import {
popMutationContext,
pushMutationContext,
viewTransitionMutationContext,
+ trackHostMutation,
} from './ReactFiberMutationTracking';
import {
MutationMask,
@@ -48,6 +50,7 @@ import {
Visibility,
ViewTransitionNamedStatic,
ViewTransitionStatic,
+ AffectedParentLayout,
} from './ReactFiberFlags';
import {
HostComponent,
@@ -61,9 +64,14 @@ import {
import {
restoreEnterOrExitViewTransitions,
restoreNestedViewTransitions,
+ restoreUpdateViewTransitionForGesture,
appearingViewTransitions,
commitEnterViewTransitions,
measureNestedViewTransitions,
+ measureUpdateViewTransition,
+ viewTransitionCancelableChildren,
+ pushViewTransitionCancelableScope,
+ popViewTransitionCancelableScope,
} from './ReactFiberCommitViewTransitions';
import {
getViewTransitionName,
@@ -72,6 +80,10 @@ import {
let didWarnForRootClone = false;
+// Used during the apply phase to track whether a parent ViewTransition component
+// might have been affected by any mutations / relayouts below.
+let viewTransitionContextChanged: boolean = false;
+
function detectMutationOrInsertClones(finishedWork: Fiber): boolean {
return true;
}
@@ -421,6 +433,7 @@ function recursivelyInsertNewFiber(
// For insertions we don't need to clone. It's already new state node.
if (visitPhase !== INSERT_APPEARING_PAIR) {
appendChild(hostParentClone, instance);
+ trackHostMutation();
recursivelyInsertNew(
finishedWork,
instance,
@@ -450,6 +463,7 @@ function recursivelyInsertNewFiber(
// For insertions we don't need to clone. It's already new state node.
if (visitPhase !== INSERT_APPEARING_PAIR) {
appendChild(hostParentClone, textInstance);
+ trackHostMutation();
}
break;
}
@@ -575,6 +589,7 @@ function recursivelyInsertClonesFromExistingTree(
}
if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {
unhideInstance(clone, child.memoizedProps);
+ trackHostMutation();
}
break;
}
@@ -590,6 +605,7 @@ function recursivelyInsertClonesFromExistingTree(
appendChild(hostParentClone, clone);
if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {
unhideTextInstance(clone, child.memoizedProps);
+ trackHostMutation();
}
break;
}
@@ -679,6 +695,10 @@ function recursivelyInsertClones(
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
trackEnterViewTransitions(childToDelete);
+ // Normally we would only mark something as triggering a mutation if there was
+ // actually a HostInstance below here. If this tree didn't contain a HostInstances
+ // we shouldn't trigger a mutation even though a virtual component was deleted.
+ trackHostMutation();
}
}
@@ -801,6 +821,7 @@ function insertDestinationClonesOfFiber(
clone = cloneMutableInstance(instance, true);
if (finishedWork.flags & ContentReset) {
resetTextContent(clone);
+ trackHostMutation();
}
} else {
// If we have children we'll clone them as we walk the tree so we just
@@ -825,6 +846,7 @@ function insertDestinationClonesOfFiber(
);
appendChild(hostParentClone, clone);
unhideInstance(clone, finishedWork.memoizedProps);
+ trackHostMutation();
} else {
recursivelyInsertClones(finishedWork, clone, null, visitPhase);
appendChild(hostParentClone, clone);
@@ -851,10 +873,12 @@ function insertDestinationClonesOfFiber(
const newText: string = finishedWork.memoizedProps;
const oldText: string = current.memoizedProps;
commitTextUpdate(clone, newText, oldText);
+ trackHostMutation();
}
appendChild(hostParentClone, clone);
if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {
unhideTextInstance(clone, finishedWork.memoizedProps);
+ trackHostMutation();
}
break;
}
@@ -885,6 +909,10 @@ function insertDestinationClonesOfFiber(
} else if (current !== null && current.memoizedState === null) {
// Was previously mounted as visible but is now hidden.
trackEnterViewTransitions(current);
+ // Normally we would only mark something as triggering a mutation if there was
+ // actually a HostInstance below here. If this tree didn't contain a HostInstances
+ // we shouldn't trigger a mutation even though a virtual component was hidden.
+ trackHostMutation();
}
break;
}
@@ -991,13 +1019,6 @@ function measureExitViewTransitions(placement: Fiber): void {
}
}
-function measureUpdateViewTransition(
- current: Fiber,
- finishedWork: Fiber,
-): void {
- // TODO
-}
-
function recursivelyApplyViewTransitions(parentFiber: Fiber) {
const deletions = parentFiber.deletions;
if (deletions !== null) {
@@ -1037,15 +1058,6 @@ function applyViewTransitionsOnFiber(finishedWork: Fiber) {
// because the fiber tag is more specific. An exception is any flag related
// to reconciliation, because those can be set on all fiber types.
switch (finishedWork.tag) {
- case HostComponent: {
- // const instance: Instance = finishedWork.stateNode;
- // TODO: Apply name and measure.
- recursivelyApplyViewTransitions(finishedWork);
- break;
- }
- case HostText: {
- break;
- }
case HostPortal: {
// TODO: Consider what should happen to Portals. For now we exclude them.
break;
@@ -1063,12 +1075,59 @@ function applyViewTransitionsOnFiber(finishedWork: Fiber) {
}
break;
}
- case ViewTransitionComponent:
- measureUpdateViewTransition(current, finishedWork);
+ case ViewTransitionComponent: {
+ const prevContextChanged = viewTransitionContextChanged;
+ const prevCancelableChildren = pushViewTransitionCancelableScope();
+ viewTransitionContextChanged = false;
+ recursivelyApplyViewTransitions(finishedWork);
+
+ if (viewTransitionContextChanged) {
+ finishedWork.flags |= Update;
+ }
+
+ const inViewport = measureUpdateViewTransition(
+ current,
+ finishedWork,
+ true,
+ );
+
+ if ((finishedWork.flags & Update) === NoFlags || !inViewport) {
+ // If this boundary didn't update, then we may be able to cancel its children.
+ // We bubble them up to the parent set to be determined later if we can cancel.
+ // Similarly, if old and new state was outside the viewport, we can skip it
+ // even if it did update.
+ if (prevCancelableChildren === null) {
+ // Bubbling up this whole set to the parent.
+ } else {
+ // Merge with parent set.
+ // $FlowFixMe[method-unbinding]
+ prevCancelableChildren.push.apply(
+ prevCancelableChildren,
+ viewTransitionCancelableChildren,
+ );
+ popViewTransitionCancelableScope(prevCancelableChildren);
+ }
+ // TODO: If this doesn't end up canceled, because a parent animates,
+ // then we should probably issue an event since this instance is part of it.
+ } else {
+ // TODO: Schedule gesture events.
+ // If this boundary did update, we cannot cancel its children so those are dropped.
+ popViewTransitionCancelableScope(prevCancelableChildren);
+ }
+
+ if ((finishedWork.flags & AffectedParentLayout) !== NoFlags) {
+ // This boundary changed size in a way that may have caused its parent to
+ // relayout. We need to bubble this information up to the parent.
+ viewTransitionContextChanged = true;
+ } else {
+ // Otherwise, we restore it to whatever the parent had found so far.
+ viewTransitionContextChanged = prevContextChanged;
+ }
+
const viewTransitionState: ViewTransitionState = finishedWork.stateNode;
viewTransitionState.clones = null; // Reset
- recursivelyApplyViewTransitions(finishedWork);
break;
+ }
default: {
recursivelyApplyViewTransitions(finishedWork);
break;
@@ -1082,13 +1141,38 @@ export function applyDepartureTransitions(
finishedWork: Fiber,
): void {
// First measure and apply view-transition-names to the "new" states.
+ viewTransitionContextChanged = false;
+ pushViewTransitionCancelableScope();
+
recursivelyApplyViewTransitions(finishedWork);
+
// Then remove the clones.
const rootClone = root.gestureClone;
if (rootClone !== null) {
root.gestureClone = null;
removeRootViewTransitionClone(root.containerInfo, rootClone);
}
+
+ if (!viewTransitionContextChanged) {
+ // If we didn't leak any resizing out to the root, we don't have to transition
+ // the root itself. This means that we can now safely cancel any cancellations
+ // that bubbled all the way up.
+ const cancelableChildren = viewTransitionCancelableChildren;
+ if (cancelableChildren !== null) {
+ for (let i = 0; i < cancelableChildren.length; i += 3) {
+ cancelViewTransitionName(
+ ((cancelableChildren[i]: any): Instance),
+ ((cancelableChildren[i + 1]: any): string),
+ ((cancelableChildren[i + 2]: any): Props),
+ );
+ }
+ }
+ // We also cancel the root itself. First we restore the name to the documentElement
+ // and then we cancel it.
+ restoreRootViewTransitionName(root.containerInfo);
+ cancelRootViewTransitionName(root.containerInfo);
+ }
+ popViewTransitionCancelableScope(null);
}
function recursivelyRestoreViewTransitions(parentFiber: Fiber) {
@@ -1130,15 +1214,6 @@ function restoreViewTransitionsOnFiber(finishedWork: Fiber) {
// because the fiber tag is more specific. An exception is any flag related
// to reconciliation, because those can be set on all fiber types.
switch (finishedWork.tag) {
- case HostComponent: {
- // const instance: Instance = finishedWork.stateNode;
- // TODO: Restore the name.
- recursivelyRestoreViewTransitions(finishedWork);
- break;
- }
- case HostText: {
- break;
- }
case HostPortal: {
// TODO: Consider what should happen to Portals. For now we exclude them.
break;
@@ -1157,8 +1232,7 @@ function restoreViewTransitionsOnFiber(finishedWork: Fiber) {
break;
}
case ViewTransitionComponent:
- const viewTransitionState: ViewTransitionState = finishedWork.stateNode;
- viewTransitionState.clones = null; // Reset
+ restoreUpdateViewTransitionForGesture(current, finishedWork);
recursivelyRestoreViewTransitions(finishedWork);
break;
default: {
diff --git a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js
index 7dad8b330d7e2..2ca49f677de1f 100644
--- a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js
+++ b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js
@@ -199,6 +199,7 @@ export function commitShowHideHostTextInstance(node: Fiber, isHidden: boolean) {
unhideTextInstance(instance, node.memoizedProps);
}
}
+ trackHostMutation();
} catch (error) {
captureCommitPhaseError(node, node.return, error);
}
diff --git a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js
index 80e190181086b..c8ec019da7f07 100644
--- a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js
+++ b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js
@@ -31,6 +31,7 @@ import {
applyViewTransitionName,
restoreViewTransitionName,
measureInstance,
+ measureClonedInstance,
hasInstanceChanged,
hasInstanceAffectedParent,
wasInstanceInViewport,
@@ -564,16 +565,23 @@ export function restoreUpdateViewTransition(
current: Fiber,
finishedWork: Fiber,
): void {
- finishedWork.memoizedState = null;
restoreViewTransitionOnHostInstances(current.child, true);
restoreViewTransitionOnHostInstances(finishedWork.child, true);
}
+export function restoreUpdateViewTransitionForGesture(
+ current: Fiber,
+ finishedWork: Fiber,
+): void {
+ // For gestures we don't need to reset "finishedWork" because those would
+ // have all been clones that got deleted.
+ restoreViewTransitionOnHostInstances(current.child, true);
+}
+
export function restoreNestedViewTransitions(changedParent: Fiber): void {
let child = changedParent.child;
while (child !== null) {
if (child.tag === ViewTransitionComponent) {
- child.memoizedState = null;
restoreViewTransitionOnHostInstances(child.child, false);
} else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
restoreNestedViewTransitions(child);
@@ -774,13 +782,16 @@ function measureViewTransitionHostInstancesRecursive(
export function measureUpdateViewTransition(
current: Fiber,
finishedWork: Fiber,
+ gesture: boolean,
): boolean {
- const props: ViewTransitionProps = finishedWork.memoizedProps;
- const newName = getViewTransitionName(props, finishedWork.stateNode);
- const oldName = getViewTransitionName(
- current.memoizedProps,
- current.stateNode,
- );
+ // If this was a gesture then which Fiber was used for the "old" vs "new" state is reversed.
+ // We still need to treat "finishedWork" as the Fiber that contains the flags for this commmit.
+ const oldFiber = gesture ? finishedWork : current;
+ const newFiber = gesture ? current : finishedWork;
+ const props: ViewTransitionProps = newFiber.memoizedProps;
+ const state: ViewTransitionState = newFiber.stateNode;
+ const newName = getViewTransitionName(props, state);
+ const oldName = getViewTransitionName(oldFiber.memoizedProps, state);
const updateClassName: ?string = getViewTransitionClassName(
props.className,
props.update,
@@ -807,7 +818,9 @@ export function measureUpdateViewTransition(
if (layoutClassName === 'none') {
// If we did not update, then all changes are considered a layout. We'll
// attempt to cancel.
- cancelViewTransitionHostInstances(finishedWork.child, oldName, true);
+ // This should use the Fiber that got names applied in the snapshot phase
+ // since those are the ones we're trying to cancel.
+ cancelViewTransitionHostInstances(oldFiber.child, oldName, true);
return false;
}
// We didn't update but we might still apply layout so we measure each
@@ -816,10 +829,21 @@ export function measureUpdateViewTransition(
}
// If nothing changed due to a mutation, or children changing size
// and the measurements end up unchanged, we should restore it to not animate.
- const previousMeasurements = current.memoizedState;
+ let previousMeasurements: null | Array;
+ if (gesture) {
+ const clones = state.clones;
+ if (clones === null) {
+ previousMeasurements = null;
+ } else {
+ previousMeasurements = clones.map(measureClonedInstance);
+ }
+ } else {
+ previousMeasurements = oldFiber.memoizedState;
+ oldFiber.memoizedState = null; // Clear it. We won't need it anymore.
+ }
const inViewport = measureViewTransitionHostInstances(
- finishedWork,
- finishedWork.child,
+ finishedWork, // This is always finishedWork since it's used to assign flags.
+ newFiber.child, // This either current or finishedWork depending on if was a gesture.
newName,
oldName,
className,
@@ -857,10 +881,11 @@ export function measureNestedViewTransitions(
if (clones === null) {
previousMeasurements = null;
} else {
- previousMeasurements = clones.map(measureInstance);
+ previousMeasurements = clones.map(measureClonedInstance);
}
} else {
previousMeasurements = child.memoizedState;
+ child.memoizedState = null; // Clear it. We won't need it anymore.
}
const inViewport = measureViewTransitionHostInstances(
child,
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js
index 3e2a5153dd147..63bf7b7eb9b15 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.js
@@ -2474,7 +2474,6 @@ function commitAfterMutationEffectsOnFiber(
// the root itself. This means that we can now safely cancel any cancellations
// that bubbled all the way up.
const cancelableChildren = viewTransitionCancelableChildren;
- popViewTransitionCancelableScope(null);
if (cancelableChildren !== null) {
for (let i = 0; i < cancelableChildren.length; i += 3) {
cancelViewTransitionName(
@@ -2487,6 +2486,7 @@ function commitAfterMutationEffectsOnFiber(
// We also cancel the root itself.
cancelRootViewTransitionName(root.containerInfo);
}
+ popViewTransitionCancelableScope(null);
break;
}
case HostComponent: {
@@ -2528,7 +2528,11 @@ function commitAfterMutationEffectsOnFiber(
finishedWork.flags |= Update;
}
- const inViewport = measureUpdateViewTransition(current, finishedWork);
+ const inViewport = measureUpdateViewTransition(
+ current,
+ finishedWork,
+ false,
+ );
if ((finishedWork.flags & Update) === NoFlags || !inViewport) {
// If this boundary didn't update, then we may be able to cancel its children.
@@ -3618,15 +3622,7 @@ function commitPassiveMountOnFiber(
if (current === null) {
// This is a new mount. We should have handled this as part of the
// Placement effect or it is deeper inside a entering transition.
- } else if (
- (finishedWork.subtreeFlags &
- (Placement |
- Update |
- ChildDeletion |
- ContentReset |
- Visibility)) !==
- NoFlags
- ) {
+ } else {
// Something mutated within this subtree. This might have caused
// something to cross-fade if we didn't already cancel it.
// If not, restore it.
diff --git a/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js b/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js
index 64b67491aa8d5..74e30da88c96e 100644
--- a/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js
+++ b/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js
@@ -46,6 +46,7 @@ export const cloneRootViewTransitionContainer = shim;
export const removeRootViewTransitionClone = shim;
export type InstanceMeasurement = null;
export const measureInstance = shim;
+export const measureClonedInstance = shim;
export const wasInstanceInViewport = shim;
export const hasInstanceChanged = shim;
export const hasInstanceAffectedParent = shim;
diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
index 2be7d18b87caf..f22b6a580e7d9 100644
--- a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
+++ b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
@@ -149,6 +149,7 @@ export const cloneRootViewTransitionContainer =
export const removeRootViewTransitionClone =
$$$config.removeRootViewTransitionClone;
export const measureInstance = $$$config.measureInstance;
+export const measureClonedInstance = $$$config.measureClonedInstance;
export const wasInstanceInViewport = $$$config.wasInstanceInViewport;
export const hasInstanceChanged = $$$config.hasInstanceChanged;
export const hasInstanceAffectedParent = $$$config.hasInstanceAffectedParent;
diff --git a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js
index d9a45550fa4b2..2df5f0157195c 100644
--- a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js
+++ b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js
@@ -389,6 +389,10 @@ export function measureInstance(instance: Instance): InstanceMeasurement {
return null;
}
+export function measureClonedInstance(instance: Instance): InstanceMeasurement {
+ return null;
+}
+
export function wasInstanceInViewport(
measurement: InstanceMeasurement,
): boolean {