diff --git a/fixtures/view-transition/src/components/Page.js b/fixtures/view-transition/src/components/Page.js index 19313a99e372d..48288da25234f 100644 --- a/fixtures/view-transition/src/components/Page.js +++ b/fixtures/view-transition/src/components/Page.js @@ -77,9 +77,9 @@ export default function Page({url, navigate}) {
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 {