diff --git a/packages/react-art/src/ReactFiberConfigART.js b/packages/react-art/src/ReactFiberConfigART.js index 50873af6da..1ed9b04fa8 100644 --- a/packages/react-art/src/ReactFiberConfigART.js +++ b/packages/react-art/src/ReactFiberConfigART.js @@ -249,6 +249,7 @@ function applyTextProps(instance, props, prevProps = {}) { } } +export * from 'react-reconciler/src/ReactFiberConfigWithNoViewTransition'; export * from 'react-reconciler/src/ReactFiberConfigWithNoPersistence'; export * from 'react-reconciler/src/ReactFiberConfigWithNoHydration'; export * from 'react-reconciler/src/ReactFiberConfigWithNoScopes'; @@ -484,92 +485,6 @@ export function unhideTextInstance(textInstance, text): void { // Noop } -export function applyViewTransitionName(instance, name, className) { - // Noop -} - -export function restoreViewTransitionName(instance, props) { - // Noop -} - -export function cancelViewTransitionName(instance, name, props) { - // Noop -} - -export function cancelRootViewTransitionName(rootContainer) { - // Noop -} - -export function restoreRootViewTransitionName(rootContainer) { - // Noop -} - -export function cloneRootViewTransitionContainer(rootContainer) { - throw new Error('Not implemented.'); -} - -export function removeRootViewTransitionClone(rootContainer, clone) { - throw new Error('Not implemented.'); -} - -export type InstanceMeasurement = null; - -export function measureInstance(instance) { - return null; -} - -export function measureClonedInstance(instance) { - return null; -} - -export function wasInstanceInViewport(measurement): boolean { - return true; -} - -export function hasInstanceChanged(oldMeasurement, newMeasurement): boolean { - return false; -} - -export function hasInstanceAffectedParent( - oldMeasurement, - newMeasurement, -): boolean { - return false; -} - -export function startViewTransition() { - return null; -} - -export type RunningViewTransition = null; - -export function startGestureTransition() { - return null; -} - -export function stopViewTransition(transition: RunningViewTransition) {} - -export function addViewTransitionFinishedListener( - transition: RunningViewTransition, - callback: () => void, -) { - callback(); -} - -export type ViewTransitionInstance = null | {name: string, ...}; - -export function createViewTransitionInstance( - name: string, -): ViewTransitionInstance { - return null; -} - -export type GestureTimeline = null; - -export function getCurrentGestureOffset(provider: GestureTimeline): number { - throw new Error('startGestureTransition is not yet supported in react-art.'); -} - export function clearContainer(container) { // TODO Implement this } diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 4cb4e8e427..eb111c2bfd 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -877,6 +877,7 @@ function handleErrorInNextTick(error: any) { // ------------------- export const supportsMutation = true; +export const supportsViewTransition = true; export function commitMount( domElement: Instance, @@ -1497,6 +1498,14 @@ function countClientRects(rects: Array): number { return count; } +export function finalizeViewTransitionChild( + type: string, + props: Props, +): Props { + // No-op for DOM. View flattening is a React Native concept. + return props; +} + export function applyViewTransitionName( instance: Instance, name: string, diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index 18c4eaddd9..533b20fa6d 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -166,6 +166,7 @@ export * from 'react-reconciler/src/ReactFiberConfigWithNoScopes'; export * from 'react-reconciler/src/ReactFiberConfigWithNoTestSelectors'; export * from 'react-reconciler/src/ReactFiberConfigWithNoResources'; export * from 'react-reconciler/src/ReactFiberConfigWithNoSingletons'; +export * from './ReactFiberConfigFabricWithViewTransition'; export function appendInitialChild( parentInstance: Instance, diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js new file mode 100644 index 0000000000..6027e6e4fe --- /dev/null +++ b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js @@ -0,0 +1,272 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {TransitionTypes} from 'react/src/ReactTransitionType'; +import type { + Instance, + Props, + Container, + SuspendedState, + GestureTimeline, +} from './ReactFiberConfigFabric'; + +const { + measureInstance: fabricMeasureInstance, + applyViewTransitionName: fabricApplyViewTransitionName, + startViewTransition: fabricStartViewTransition, + restoreViewTransitionName: fabricRestoreViewTransitionName, + cancelViewTransitionName: fabricCancelViewTransitionName, +} = nativeFabricUIManager; + +export const supportsViewTransition = true; + +export type InstanceMeasurement = { + rect: {x: number, y: number, width: number, height: number}, + abs: boolean, + clip: boolean, + view: boolean, +}; + +export type RunningViewTransition = { + skipTransition(): void, + finished: Promise, + ready: Promise, + ... +}; + +interface ViewTransitionPseudoElementType extends mixin$Animatable { + _scope: HTMLElement; + _selector: string; + getComputedStyle(): CSSStyleDeclaration; +} + +function ViewTransitionPseudoElement( + this: ViewTransitionPseudoElementType, + pseudo: string, + name: string, +) { + // TODO: Get the owner document from the root container. + this._pseudo = pseudo; + this._name = name; +} + +export type ViewTransitionInstance = null | { + name: string, + old: mixin$Animatable, + new: mixin$Animatable, + ... +}; + +export function restoreViewTransitionName( + instance: Instance, + props: Props, +): void { + fabricRestoreViewTransitionName(instance.node); +} + +// Cancel the old and new snapshots of viewTransitionName +export function cancelViewTransitionName( + instance: Instance, + oldName: string, + props: Props, +): void { + fabricCancelViewTransitionName(instance.node, oldName); +} + +export function cancelRootViewTransitionName(rootContainer: Container): void { + if (__DEV__) { + console.warn('cancelRootViewTransitionName is not implemented'); + } +} + +export function restoreRootViewTransitionName(rootContainer: Container): void { + if (__DEV__) { + console.warn('restoreRootViewTransitionName is not implemented'); + } +} + +export function cloneRootViewTransitionContainer( + rootContainer: Container, +): Instance { + if (__DEV__) { + console.warn('cloneRootViewTransitionContainer is not implemented'); + } + // $FlowFixMe[incompatible-return] Return empty stub + return null; +} + +export function removeRootViewTransitionClone( + rootContainer: Container, + clone: Instance, +): void { + if (__DEV__) { + console.warn('removeRootViewTransitionClone is not implemented'); + } +} + +export function measureInstance(instance: Instance): InstanceMeasurement { + const measurement = fabricMeasureInstance(instance.node); + return { + rect: { + x: measurement.x, + y: measurement.y, + width: measurement.width, + height: measurement.height, + }, + abs: false, + clip: false, + view: true, + }; +} + +export function measureClonedInstance(instance: Instance): InstanceMeasurement { + if (__DEV__) { + console.warn('measureClonedInstance is not implemented'); + } + return { + rect: {x: 0, y: 0, width: 0, height: 0}, + abs: false, + clip: false, + view: true, + }; +} + +export function wasInstanceInViewport( + measurement: InstanceMeasurement, +): boolean { + return measurement.view; +} + +export function hasInstanceChanged( + oldMeasurement: InstanceMeasurement, + newMeasurement: InstanceMeasurement, +): boolean { + if (__DEV__) { + console.warn('hasInstanceChanged is not implemented'); + } + return false; +} + +export function hasInstanceAffectedParent( + oldMeasurement: InstanceMeasurement, + newMeasurement: InstanceMeasurement, +): boolean { + if (__DEV__) { + console.warn('hasInstanceAffectedParent is not implemented'); + } + return false; +} + +export function startGestureTransition( + suspendedState: null | SuspendedState, + rootContainer: Container, + timeline: GestureTimeline, + rangeStart: number, + rangeEnd: number, + transitionTypes: null | TransitionTypes, + mutationCallback: () => void, + animateCallback: () => void, + errorCallback: (error: mixed) => void, + finishedAnimation: () => void, +): RunningViewTransition { + if (__DEV__) { + console.warn('startGestureTransition is not implemented'); + } + return null; +} + +export function stopViewTransition(transition: RunningViewTransition): void { + if (__DEV__) { + console.warn('stopViewTransition is not implemented'); + } +} + +export function addViewTransitionFinishedListener( + transition: RunningViewTransition, + callback: () => void, +): void { + callback(); +} + +export function createViewTransitionInstance( + name: string, +): ViewTransitionInstance { + return { + name, + old: new (ViewTransitionPseudoElement: any)('old', name), + new: new (ViewTransitionPseudoElement: any)('new', name), + }; +} + +export function finalizeViewTransitionChild( + type: string, + props: Props, +): Props { + // Prevent view flattening for direct host children of ViewTransition. + // Without this, Fabric's native-side optimization may remove the view + // from the platform hierarchy, breaking view transition animations. + return Object.assign({}, props, {collapsable: false}); +} + +export function applyViewTransitionName( + instance: Instance, + name: string, + className: ?string, +): void { + // add view-transition-name to things that might animate for browser + fabricApplyViewTransitionName(instance.node, name, className); +} + +export function startViewTransition( + suspendedState: null | SuspendedState, + rootContainer: Container, + transitionTypes: null | TransitionTypes, + mutationCallback: () => void, + layoutCallback: () => void, + afterMutationCallback: () => void, + spawnedWorkCallback: () => void, + passiveCallback: () => mixed, + errorCallback: (error: mixed) => void, + blockedCallback: (name: string) => void, + finishedAnimation: () => void, +): null | RunningViewTransition { + const transition = fabricStartViewTransition( + // mutation + () => { + mutationCallback(); // completeRoot should run here + layoutCallback(); + afterMutationCallback(); + }, + ); + + if (transition == null) { + if (__DEV__) { + console.warn( + "startViewTransition didn't kick off transition in Fabric, the ViewTransition ReactNativeFeatureFlag might not be enabled.", + ); + } + // Flush remaining work synchronously. + mutationCallback(); + layoutCallback(); + // Skip afterMutationCallback(). We don't need it since we're not animating. + spawnedWorkCallback(); + // Skip passiveCallback(). Spawned work will schedule a task. + return null; + } + + transition.ready.then(() => { + spawnedWorkCallback(); + }); + + transition.finished.finally(() => { + passiveCallback(); + }); + + return transition; +} diff --git a/packages/react-native-renderer/src/ReactFiberConfigNative.js b/packages/react-native-renderer/src/ReactFiberConfigNative.js index 404ae7a54a..b5e086493a 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigNative.js +++ b/packages/react-native-renderer/src/ReactFiberConfigNative.js @@ -8,7 +8,6 @@ */ import type {InspectorData, TouchedViewDataAtPoint} from './ReactNativeTypes'; -import type {TransitionTypes} from 'react/src/ReactTransitionType'; // Modules provided by RN: import { @@ -35,8 +34,6 @@ import { } from 'react-reconciler/src/ReactEventPriorities'; import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; -import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; - import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; import type {ReactContext} from 'shared/ReactTypes'; @@ -112,6 +109,7 @@ function recursivelyUncacheFiberNode(node: Instance | TextInstance) { } } +export * from 'react-reconciler/src/ReactFiberConfigWithNoViewTransition'; export * from 'react-reconciler/src/ReactFiberConfigWithNoPersistence'; export * from 'react-reconciler/src/ReactFiberConfigWithNoHydration'; export * from 'react-reconciler/src/ReactFiberConfigWithNoScopes'; @@ -591,152 +589,6 @@ export function unhideInstance(instance: Instance, props: Props): void { ); } -export function applyViewTransitionName( - instance: Instance, - name: string, - className: ?string, -): void { - // Not yet implemented -} - -export function restoreViewTransitionName( - instance: Instance, - props: Props, -): void { - // Not yet implemented -} - -export function cancelViewTransitionName( - instance: Instance, - name: string, - props: Props, -): void { - // Not yet implemented -} - -export function cancelRootViewTransitionName(rootContainer: Container): void { - // Not yet implemented -} - -export function restoreRootViewTransitionName(rootContainer: Container): void { - // Not yet implemented -} - -export function cloneRootViewTransitionContainer( - rootContainer: Container, -): Instance { - throw new Error('Not implemented.'); -} - -export function removeRootViewTransitionClone( - rootContainer: Container, - clone: Instance, -): void { - throw new Error('Not implemented.'); -} - -export type InstanceMeasurement = null; - -export function measureInstance(instance: Instance): InstanceMeasurement { - // This heuristic is better implemented at the native layer. - return null; -} - -export function measureClonedInstance(instance: Instance): InstanceMeasurement { - return null; -} - -export function wasInstanceInViewport( - measurement: InstanceMeasurement, -): boolean { - return true; -} - -export function hasInstanceChanged( - oldMeasurement: InstanceMeasurement, - newMeasurement: InstanceMeasurement, -): boolean { - return false; -} - -export function hasInstanceAffectedParent( - oldMeasurement: InstanceMeasurement, - newMeasurement: InstanceMeasurement, -): boolean { - return false; -} - -export function startViewTransition( - suspendedState: null | SuspendedState, - rootContainer: Container, - transitionTypes: null | TransitionTypes, - mutationCallback: () => void, - layoutCallback: () => void, - afterMutationCallback: () => void, - spawnedWorkCallback: () => void, - passiveCallback: () => mixed, - errorCallback: mixed => void, - blockedCallback: string => void, // Profiling-only - finishedAnimation: () => void, // Profiling-only -): null | RunningViewTransition { - mutationCallback(); - layoutCallback(); - // Skip afterMutationCallback(). We don't need it since we're not animating. - spawnedWorkCallback(); - if (enableProfilerTimer) { - finishedAnimation(); - } - // Skip passiveCallback(). Spawned work will schedule a task. - return null; -} - -export type RunningViewTransition = null; - -export function startGestureTransition( - suspendedState: null | SuspendedState, - rootContainer: Container, - timeline: GestureTimeline, - rangeStart: number, - rangeEnd: number, - transitionTypes: null | TransitionTypes, - mutationCallback: () => void, - animateCallback: () => void, - errorCallback: mixed => void, - finishedAnimation: () => void, // Profiling-only -): null | RunningViewTransition { - mutationCallback(); - animateCallback(); - if (enableProfilerTimer) { - finishedAnimation(); - } - return null; -} - -export function stopViewTransition(transition: RunningViewTransition) {} - -export function addViewTransitionFinishedListener( - transition: RunningViewTransition, - callback: () => void, -) { - callback(); -} - -export type ViewTransitionInstance = null | {name: string, ...}; - -export function createViewTransitionInstance( - name: string, -): ViewTransitionInstance { - return null; -} - -export type GestureTimeline = null; - -export function getCurrentGestureOffset(provider: GestureTimeline): number { - throw new Error( - 'startGestureTransition is not yet supported in React Native.', - ); -} - export function clearContainer(container: Container): void { // TODO Implement this for React Native // UIManager does not expose a "remove all" type method. diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index cf2c082367..3bba42462d 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -188,6 +188,7 @@ import {shouldError, shouldSuspend} from './ReactFiberReconciler'; import { pushHostContext, pushHostContainer, + pushViewTransitionContext, getRootHostContainer, } from './ReactFiberHostContext'; import { @@ -3571,6 +3572,10 @@ function updateViewTransition( workInProgress: Fiber, renderLanes: Lanes, ) { + // Mark direct host children as being inside a ViewTransition so the renderer + // can finalize them (e.g. prevent view flattening in React Native). + pushViewTransitionContext(workInProgress); + if (workInProgress.stateNode === null) { // We previously reset the work-in-progress. // We need to create a new ViewTransitionState instance. @@ -4157,6 +4162,13 @@ function attemptEarlyBailoutIfNoScheduledUpdate( } // Fallthrough } + case ViewTransitionComponent: { + if (enableViewTransition) { + pushViewTransitionContext(workInProgress); + break; + } + // Fallthrough + } } return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } diff --git a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js index a9edc0c84d..5a6b243f1f 100644 --- a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js +++ b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js @@ -34,6 +34,7 @@ import { hasInstanceAffectedParent, wasInstanceInViewport, } from './ReactFiberConfig'; +import {enableViewTransitionForPersistenceMode} from 'shared/ReactFeatureFlags'; import { scheduleViewTransitionEvent, scheduleGestureTransitionEvent, @@ -139,93 +140,168 @@ function applyViewTransitionToHostInstancesRecursive( collectMeasurements: null | Array, stopAtNestedViewTransitions: boolean, ): boolean { - if (!supportsMutation) { - return false; - } - let inViewport = false; - while (child !== null) { - if (child.tag === HostComponent) { - const instance: Instance = child.stateNode; - if (collectMeasurements !== null) { - const measurement = measureInstance(instance); - collectMeasurements.push(measurement); - if (wasInstanceInViewport(measurement)) { - inViewport = true; + if (supportsMutation) { + let inViewport = false; + while (child !== null) { + if (child.tag === HostComponent) { + const instance: Instance = child.stateNode; + if (collectMeasurements !== null) { + const measurement = measureInstance(instance); + collectMeasurements.push(measurement); + if (wasInstanceInViewport(measurement)) { + inViewport = true; + } + } else if (!inViewport) { + if (wasInstanceInViewport(measureInstance(instance))) { + inViewport = true; + } } - } else if (!inViewport) { - if (wasInstanceInViewport(measureInstance(instance))) { + shouldStartViewTransition = true; + applyViewTransitionName( + instance, + viewTransitionHostInstanceIdx === 0 + ? name + : // If we have multiple Host Instances below, we add a suffix to the name to give + // each one a unique name. + name + '_' + viewTransitionHostInstanceIdx, + className, + ); + viewTransitionHostInstanceIdx++; + } else if ( + child.tag === OffscreenComponent && + child.memoizedState !== null + ) { + // Skip any hidden subtrees. They were or are effectively not there. + } else if ( + child.tag === ViewTransitionComponent && + stopAtNestedViewTransitions + ) { + // Skip any nested view transitions for updates since in that case the + // inner most one is the one that handles the update. + } else { + if ( + applyViewTransitionToHostInstancesRecursive( + child.child, + name, + className, + collectMeasurements, + stopAtNestedViewTransitions, + ) + ) { inViewport = true; } } - shouldStartViewTransition = true; - applyViewTransitionName( - instance, - viewTransitionHostInstanceIdx === 0 - ? name - : // If we have multiple Host Instances below, we add a suffix to the name to give - // each one a unique name. - name + '_' + viewTransitionHostInstanceIdx, - className, - ); - viewTransitionHostInstanceIdx++; - } else if ( - child.tag === OffscreenComponent && - child.memoizedState !== null - ) { - // Skip any hidden subtrees. They were or are effectively not there. - } else if ( - child.tag === ViewTransitionComponent && - stopAtNestedViewTransitions - ) { - // Skip any nested view transitions for updates since in that case the - // inner most one is the one that handles the update. - } else { - if ( - applyViewTransitionToHostInstancesRecursive( - child.child, - name, + child = child.sibling; + } + return inViewport; + } else if (enableViewTransitionForPersistenceMode) { + let inViewport = false; + while (child !== null) { + if (child.tag === HostComponent) { + const instance: Instance = child.stateNode; + if (collectMeasurements !== null) { + const measurement = measureInstance(instance); + collectMeasurements.push(measurement); + if (wasInstanceInViewport(measurement)) { + inViewport = true; + } + } else if (!inViewport) { + if (wasInstanceInViewport(measureInstance(instance))) { + inViewport = true; + } + } + shouldStartViewTransition = true; + applyViewTransitionName( + instance, + viewTransitionHostInstanceIdx === 0 + ? name + : name + '_' + viewTransitionHostInstanceIdx, className, - collectMeasurements, - stopAtNestedViewTransitions, - ) + ); + viewTransitionHostInstanceIdx++; + } else if ( + child.tag === OffscreenComponent && + child.memoizedState !== null ) { - inViewport = true; + // Skip any hidden subtrees. They were or are effectively not there. + } else if ( + child.tag === ViewTransitionComponent && + stopAtNestedViewTransitions + ) { + // Skip any nested view transitions for updates since in that case the + // inner most one is the one that handles the update. + } else { + if ( + applyViewTransitionToHostInstancesRecursive( + child.child, + name, + className, + collectMeasurements, + stopAtNestedViewTransitions, + ) + ) { + inViewport = true; + } } + child = child.sibling; } - child = child.sibling; + return inViewport; } - return inViewport; + return false; } function restoreViewTransitionOnHostInstances( child: null | Fiber, stopAtNestedViewTransitions: boolean, ): void { - if (!supportsMutation) { - return; - } - while (child !== null) { - if (child.tag === HostComponent) { - const instance: Instance = child.stateNode; - restoreViewTransitionName(instance, child.memoizedProps); - } else if ( - child.tag === OffscreenComponent && - child.memoizedState !== null - ) { - // Skip any hidden subtrees. They were or are effectively not there. - } else if ( - child.tag === ViewTransitionComponent && - stopAtNestedViewTransitions - ) { - // Skip any nested view transitions for updates since in that case the - // inner most one is the one that handles the update. - } else { - restoreViewTransitionOnHostInstances( - child.child, - stopAtNestedViewTransitions, - ); + if (supportsMutation) { + while (child !== null) { + if (child.tag === HostComponent) { + const instance: Instance = child.stateNode; + restoreViewTransitionName(instance, child.memoizedProps); + } else if ( + child.tag === OffscreenComponent && + child.memoizedState !== null + ) { + // Skip any hidden subtrees. They were or are effectively not there. + } else if ( + child.tag === ViewTransitionComponent && + stopAtNestedViewTransitions + ) { + // Skip any nested view transitions for updates since in that case the + // inner most one is the one that handles the update. + } else { + restoreViewTransitionOnHostInstances( + child.child, + stopAtNestedViewTransitions, + ); + } + child = child.sibling; + } + } else if (enableViewTransitionForPersistenceMode) { + while (child !== null) { + if (child.tag === HostComponent) { + const instance: Instance = child.stateNode; + restoreViewTransitionName(instance, child.memoizedProps); + } else if ( + child.tag === OffscreenComponent && + child.memoizedState !== null + ) { + // Skip any hidden subtrees. They were or are effectively not there. + } else if ( + child.tag === ViewTransitionComponent && + stopAtNestedViewTransitions + ) { + // Skip any nested view transitions for updates since in that case the + // inner most one is the one that handles the update. + } else { + restoreViewTransitionOnHostInstances( + child.child, + stopAtNestedViewTransitions, + ); + } + child = child.sibling; } - child = child.sibling; } } @@ -648,112 +724,196 @@ function measureViewTransitionHostInstancesRecursive( previousMeasurements: null | Array, stopAtNestedViewTransitions: boolean, ): boolean { - if (!supportsMutation) { - return true; - } - let inViewport = false; - while (child !== null) { - if (child.tag === HostComponent) { - const instance: Instance = child.stateNode; - if ( - previousMeasurements !== null && - viewTransitionHostInstanceIdx < previousMeasurements.length + if (supportsMutation) { + let inViewport = false; + while (child !== null) { + if (child.tag === HostComponent) { + const instance: Instance = child.stateNode; + if ( + previousMeasurements !== null && + viewTransitionHostInstanceIdx < previousMeasurements.length + ) { + // The previous measurement of the Instance in this location within the ViewTransition. + // Note that this might not be the same exact Instance if the Instances within the + // ViewTransition changed. + const previousMeasurement = + previousMeasurements[viewTransitionHostInstanceIdx]; + const nextMeasurement = measureInstance(instance); + if ( + wasInstanceInViewport(previousMeasurement) || + wasInstanceInViewport(nextMeasurement) + ) { + // If either the old or new state was within the viewport we have to animate this. + // But if it turns out that none of them were we'll be able to skip it. + inViewport = true; + } + if ( + (parentViewTransition.flags & Update) === NoFlags && + hasInstanceChanged(previousMeasurement, nextMeasurement) + ) { + parentViewTransition.flags |= Update; + } + if ( + hasInstanceAffectedParent(previousMeasurement, nextMeasurement) + ) { + // If this instance size within its parent has changed it might have caused the + // parent to relayout which needs a cross fade. + parentViewTransition.flags |= AffectedParentLayout; + } + } else { + // If there was an insertion of extra nodes, we have to assume they affected the parent. + // It should have already been marked as an Update due to the mutation. + parentViewTransition.flags |= AffectedParentLayout; + } + 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 + ? newName + : // If we have multiple Host Instances below, we add a suffix to the name to give + // each one a unique name. + newName + '_' + viewTransitionHostInstanceIdx, + className, + ); + } + if (!inViewport || (parentViewTransition.flags & Update) === NoFlags) { + // It turns out that we had no other deeper mutations, the child transitions didn't + // affect the parent layout and this instance hasn't changed size. So we can skip + // animating it. However, in the current model this only works if the parent also + // doesn't animate. So we have to queue these and wait until we complete the parent + // to cancel them. + if (viewTransitionCancelableChildren === null) { + viewTransitionCancelableChildren = []; + } + viewTransitionCancelableChildren.push( + instance, + viewTransitionHostInstanceIdx === 0 + ? oldName + : // If we have multiple Host Instances below, we add a suffix to the name to give + // each one a unique name. + oldName + '_' + viewTransitionHostInstanceIdx, + child.memoizedProps, + ); + } + viewTransitionHostInstanceIdx++; + } else if ( + child.tag === OffscreenComponent && + child.memoizedState !== null ) { - // The previous measurement of the Instance in this location within the ViewTransition. - // Note that this might not be the same exact Instance if the Instances within the - // ViewTransition changed. - const previousMeasurement = - previousMeasurements[viewTransitionHostInstanceIdx]; - const nextMeasurement = measureInstance(instance); + // Skip any hidden subtrees. They were or are effectively not there. + } else if ( + child.tag === ViewTransitionComponent && + stopAtNestedViewTransitions + ) { + // Skip any nested view transitions for updates since in that case the + // inner most one is the one that handles the update. + // If this inner boundary resized we need to bubble that information up. + parentViewTransition.flags |= child.flags & AffectedParentLayout; + } else { if ( - wasInstanceInViewport(previousMeasurement) || - wasInstanceInViewport(nextMeasurement) + measureViewTransitionHostInstancesRecursive( + parentViewTransition, + child.child, + newName, + oldName, + className, + previousMeasurements, + stopAtNestedViewTransitions, + ) ) { - // If either the old or new state was within the viewport we have to animate this. - // But if it turns out that none of them were we'll be able to skip it. inViewport = true; } + } + child = child.sibling; + } + return inViewport; + } else if (enableViewTransitionForPersistenceMode) { + let inViewport = false; + while (child !== null) { + if (child.tag === HostComponent) { + const instance: Instance = child.stateNode; if ( - (parentViewTransition.flags & Update) === NoFlags && - hasInstanceChanged(previousMeasurement, nextMeasurement) + previousMeasurements !== null && + viewTransitionHostInstanceIdx < previousMeasurements.length ) { - parentViewTransition.flags |= Update; - } - if (hasInstanceAffectedParent(previousMeasurement, nextMeasurement)) { - // If this instance size within its parent has changed it might have caused the - // parent to relayout which needs a cross fade. + const previousMeasurement = + previousMeasurements[viewTransitionHostInstanceIdx]; + const nextMeasurement = measureInstance(instance); + if ( + wasInstanceInViewport(previousMeasurement) || + wasInstanceInViewport(nextMeasurement) + ) { + inViewport = true; + } + if ( + (parentViewTransition.flags & Update) === NoFlags && + hasInstanceChanged(previousMeasurement, nextMeasurement) + ) { + parentViewTransition.flags |= Update; + } + if ( + hasInstanceAffectedParent(previousMeasurement, nextMeasurement) + ) { + parentViewTransition.flags |= AffectedParentLayout; + } + } else { parentViewTransition.flags |= AffectedParentLayout; } - } else { - // If there was an insertion of extra nodes, we have to assume they affected the parent. - // It should have already been marked as an Update due to the mutation. - parentViewTransition.flags |= AffectedParentLayout; - } - 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 - ? newName - : // If we have multiple Host Instances below, we add a suffix to the name to give - // each one a unique name. - newName + '_' + viewTransitionHostInstanceIdx, - className, - ); - } - if (!inViewport || (parentViewTransition.flags & Update) === NoFlags) { - // It turns out that we had no other deeper mutations, the child transitions didn't - // affect the parent layout and this instance hasn't changed size. So we can skip - // animating it. However, in the current model this only works if the parent also - // doesn't animate. So we have to queue these and wait until we complete the parent - // to cancel them. - if (viewTransitionCancelableChildren === null) { - viewTransitionCancelableChildren = []; + if ((parentViewTransition.flags & Update) !== NoFlags) { + applyViewTransitionName( + instance, + viewTransitionHostInstanceIdx === 0 + ? newName + : newName + '_' + viewTransitionHostInstanceIdx, + className, + ); } - viewTransitionCancelableChildren.push( - instance, - viewTransitionHostInstanceIdx === 0 - ? oldName - : // If we have multiple Host Instances below, we add a suffix to the name to give - // each one a unique name. - oldName + '_' + viewTransitionHostInstanceIdx, - child.memoizedProps, - ); - } - viewTransitionHostInstanceIdx++; - } else if ( - child.tag === OffscreenComponent && - child.memoizedState !== null - ) { - // Skip any hidden subtrees. They were or are effectively not there. - } else if ( - child.tag === ViewTransitionComponent && - stopAtNestedViewTransitions - ) { - // Skip any nested view transitions for updates since in that case the - // inner most one is the one that handles the update. - // If this inner boundary resized we need to bubble that information up. - parentViewTransition.flags |= child.flags & AffectedParentLayout; - } else { - if ( - measureViewTransitionHostInstancesRecursive( - parentViewTransition, - child.child, - newName, - oldName, - className, - previousMeasurements, - stopAtNestedViewTransitions, - ) + if (!inViewport || (parentViewTransition.flags & Update) === NoFlags) { + if (viewTransitionCancelableChildren === null) { + viewTransitionCancelableChildren = []; + } + viewTransitionCancelableChildren.push( + instance, + viewTransitionHostInstanceIdx === 0 + ? oldName + : oldName + '_' + viewTransitionHostInstanceIdx, + child.memoizedProps, + ); + } + viewTransitionHostInstanceIdx++; + } else if ( + child.tag === OffscreenComponent && + child.memoizedState !== null + ) { + // Skip any hidden subtrees. They were or are effectively not there. + } else if ( + child.tag === ViewTransitionComponent && + stopAtNestedViewTransitions ) { - inViewport = true; + parentViewTransition.flags |= child.flags & AffectedParentLayout; + } else { + if ( + measureViewTransitionHostInstancesRecursive( + parentViewTransition, + child.child, + newName, + oldName, + className, + previousMeasurements, + stopAtNestedViewTransitions, + ) + ) { + inViewport = true; + } } + child = child.sibling; } - child = child.sibling; + return inViewport; } - return inViewport; + return true; } export function measureUpdateViewTransition( diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 322c858bb9..36c670108c 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -58,6 +58,7 @@ import { disableLegacyMode, enableComponentPerformanceTrack, enableViewTransition, + enableViewTransitionForPersistenceMode, enableFragmentRefs, enableEagerAlternateStateNodeCleanup, enableDefaultTransitionIndicator, @@ -158,6 +159,7 @@ import { supportsHydration, supportsResources, supportsSingletons, + supportsViewTransition, clearSuspenseBoundary, clearSuspenseBoundaryFromContainer, createContainerChildSet, @@ -3712,7 +3714,15 @@ function commitPassiveMountOnFiber( } if (isViewTransitionEligible) { - if (supportsMutation && rootViewTransitionNameCanceled) { + if ( + supportsViewTransition && + rootViewTransitionNameCanceled + ) { + restoreRootViewTransitionName(finishedRoot.containerInfo); + } else if ( + enableViewTransitionForPersistenceMode && + rootViewTransitionNameCanceled + ) { restoreRootViewTransitionName(finishedRoot.containerInfo); } } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 9b8c4a21bd..5300d93dc8 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -127,11 +127,14 @@ import { mayResourceSuspendCommit, preloadInstance, preloadResource, + finalizeViewTransitionChild, } from './ReactFiberConfig'; import { getRootHostContainer, popHostContext, getHostContext, + getIsInViewTransition, + popViewTransitionContext, popHostContainer, } from './ReactFiberHostContext'; import { @@ -1356,16 +1359,25 @@ function completeWork( case HostComponent: { popHostContext(workInProgress); const type = workInProgress.type; + + // After popping, check if this HostComponent is a direct host child + // of a ViewTransition. If so, let the renderer finalize the props + // (e.g. to prevent view flattening in React Native). + let instanceProps = newProps; + if (enableViewTransition && getIsInViewTransition()) { + instanceProps = finalizeViewTransitionChild(type, instanceProps); + } + if (current !== null && workInProgress.stateNode != null) { updateHostComponent( current, workInProgress, type, - newProps, + instanceProps, renderLanes, ); } else { - if (!newProps) { + if (!instanceProps) { if (workInProgress.stateNode === null) { throw new Error( 'We must have new props for new mounts. This error is likely ' + @@ -1397,7 +1409,7 @@ function completeWork( finalizeHydratedChildren( workInProgress.stateNode, type, - newProps, + instanceProps, currentHostContext, ) ) { @@ -1407,7 +1419,7 @@ function completeWork( const rootContainerInstance = getRootHostContainer(); const instance = createInstance( type, - newProps, + instanceProps, rootContainerInstance, currentHostContext, workInProgress, @@ -1425,7 +1437,7 @@ function completeWork( finalizeInitialChildren( instance, type, - newProps, + instanceProps, currentHostContext, ) ) { @@ -1433,6 +1445,13 @@ function completeWork( } } } + + // Ensure memoizedProps reflects the finalized props so that + // future renders diff against the correct props. + if (instanceProps !== newProps) { + workInProgress.memoizedProps = instanceProps; + } + bubbleProperties(workInProgress); if (enableViewTransition) { // Host Components act as their own View Transitions which doesn't run enter/exit animations. @@ -2056,6 +2075,7 @@ function completeWork( } case ViewTransitionComponent: { if (enableViewTransition) { + popViewTransitionContext(workInProgress); // We're a component that might need an exit transition. This flag will // bubble up to the parent tree to indicate that there's a child that // might need an exit View Transition upon unmount. diff --git a/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js b/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js index 79cf3990a7..f243805214 100644 --- a/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js +++ b/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js @@ -37,25 +37,5 @@ export const hideTextInstance = shim; export const unhideInstance = shim; export const unhideTextInstance = shim; export const clearContainer = shim; -export const applyViewTransitionName = shim; -export const restoreViewTransitionName = shim; -export const cancelViewTransitionName = shim; -export const cancelRootViewTransitionName = shim; -export const restoreRootViewTransitionName = shim; -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; -export const startViewTransition = shim; -export type RunningViewTransition = null; -export const startGestureTransition = shim; -export const stopViewTransition = shim; -export const addViewTransitionFinishedListener = shim; -export type ViewTransitionInstance = null | {name: string, ...}; -export const createViewTransitionInstance = shim; export type GestureTimeline = any; export const getCurrentGestureOffset = shim; diff --git a/packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js b/packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js new file mode 100644 index 0000000000..1f428404ae --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +// Renderers that don't support view transitions +// can re-export everything from this module. + +function shim(...args: any): empty { + throw new Error( + 'The current renderer does not support view transitions. ' + + 'This error is likely caused by a bug in React. ' + + 'Please file an issue.', + ); +} + +// View Transitions (when unsupported) +export const supportsViewTransition = false; +export const applyViewTransitionName = shim; +export const restoreViewTransitionName = shim; +export const cancelViewTransitionName = shim; +export const cancelRootViewTransitionName = shim; +export const restoreRootViewTransitionName = shim; +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; +export const startViewTransition = shim; +export type RunningViewTransition = null; +export const startGestureTransition = shim; +export const stopViewTransition = shim; +export const addViewTransitionFinishedListener = shim; +export type ViewTransitionInstance = null | {name: string, ...}; +export const createViewTransitionInstance = shim; +export const finalizeViewTransitionChild = shim; diff --git a/packages/react-reconciler/src/ReactFiberHostContext.js b/packages/react-reconciler/src/ReactFiberHostContext.js index 2d2ec4c88a..6395b16b10 100644 --- a/packages/react-reconciler/src/ReactFiberHostContext.js +++ b/packages/react-reconciler/src/ReactFiberHostContext.js @@ -23,9 +23,12 @@ import { NotPendingTransition, isPrimaryRenderer, } from './ReactFiberConfig'; +import {enableViewTransition} from 'shared/ReactFeatureFlags'; import {createCursor, push, pop} from './ReactFiberStack'; const contextStackCursor: StackCursor = createCursor(null); + +const viewTransitionCursor: StackCursor = createCursor(false); const contextFiberStackCursor: StackCursor = createCursor(null); const rootInstanceStackCursor: StackCursor = createCursor(null); @@ -93,7 +96,29 @@ function getHostContext(): HostContext { return context; } +function pushViewTransitionContext(fiber: Fiber): void { + if (enableViewTransition) { + push(viewTransitionCursor, true, fiber); + } +} + +function popViewTransitionContext(fiber: Fiber): void { + if (enableViewTransition) { + pop(viewTransitionCursor, fiber); + } +} + +function getIsInViewTransition(): boolean { + return viewTransitionCursor.current; +} + function pushHostContext(fiber: Fiber): void { + // HostComponents act as ViewTransition boundaries. Push false so that + // nested HostComponents below this one are not considered direct VT children. + if (enableViewTransition) { + push(viewTransitionCursor, false, fiber); + } + const stateHook: Hook | null = fiber.memoizedState; if (stateHook !== null) { // Propagate the current state to all the descendents. @@ -129,6 +154,10 @@ function pushHostContext(fiber: Fiber): void { } function popHostContext(fiber: Fiber): void { + if (enableViewTransition) { + pop(viewTransitionCursor, fiber); + } + if (contextFiberStackCursor.current === fiber) { // Do not pop unless this Fiber provided the current context. // pushHostContext() only pushes Fibers that provide unique contexts. @@ -159,10 +188,13 @@ function popHostContext(fiber: Fiber): void { export { getHostContext, + getIsInViewTransition, getCurrentRootHostContainer, getRootHostContainer, popHostContainer, popHostContext, pushHostContainer, pushHostContext, + pushViewTransitionContext, + popViewTransitionContext, }; diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index b2954c41f5..31cd1d8648 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -33,15 +33,21 @@ import { LegacyHiddenComponent, CacheComponent, TracingMarkerComponent, + ViewTransitionComponent, } from './ReactWorkTags'; import {DidCapture, NoFlags, ShouldCapture, Update} from './ReactFiberFlags'; import {NoMode, ProfileMode} from './ReactTypeOfMode'; import { enableProfilerTimer, enableTransitionTracing, + enableViewTransition, } from 'shared/ReactFeatureFlags'; -import {popHostContainer, popHostContext} from './ReactFiberHostContext'; +import { + popHostContainer, + popHostContext, + popViewTransitionContext, +} from './ReactFiberHostContext'; import { popSuspenseListContext, popSuspenseHandler, @@ -243,6 +249,11 @@ function unwindWork( } } return null; + case ViewTransitionComponent: + if (enableViewTransition) { + popViewTransitionContext(workInProgress); + } + return null; default: return null; } @@ -324,6 +335,11 @@ function unwindInterruptedWork( } } break; + case ViewTransitionComponent: + if (enableViewTransition) { + popViewTransitionContext(interruptedWork); + } + break; default: break; } diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js index 1785fa9aae..ccefe1c053 100644 --- a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js @@ -73,6 +73,7 @@ export const warnsIfNotActing = $$$config.warnsIfNotActing; export const supportsMutation = $$$config.supportsMutation; export const supportsPersistence = $$$config.supportsPersistence; export const supportsHydration = $$$config.supportsHydration; +export const supportsViewTransition = $$$config.supportsViewTransition; export const getInstanceFromNode = $$$config.getInstanceFromNode; export const beforeActiveInstanceBlur = $$$config.beforeActiveInstanceBlur; export const afterActiveInstanceBlur = $$$config.afterActiveInstanceBlur; @@ -167,6 +168,8 @@ export const addViewTransitionFinishedListener = export const getCurrentGestureOffset = $$$config.getCurrentGestureOffset; export const createViewTransitionInstance = $$$config.createViewTransitionInstance; +export const finalizeViewTransitionChild = + $$$config.finalizeViewTransitionChild; export const clearContainer = $$$config.clearContainer; export const createFragmentInstance = $$$config.createFragmentInstance; export const updateFragmentInstanceFiber = diff --git a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js index 6b04a36d29..417745828d 100644 --- a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js +++ b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js @@ -8,8 +8,6 @@ */ import type {ReactContext} from 'shared/ReactTypes'; -import type {TransitionTypes} from 'react/src/ReactTransitionType'; - import isArray from 'shared/isArray'; import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; import { @@ -17,7 +15,6 @@ import { NoEventPriority, type EventPriority, } from 'react-reconciler/src/ReactEventPriorities'; -import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; export {default as rendererVersion} from 'shared/ReactVersion'; // TODO: Consider exporting the react-native version. export const rendererPackageName = 'react-test-renderer'; @@ -56,6 +53,7 @@ export type EventResponder = any; export type RendererInspectionConfig = $ReadOnly<{}>; export type TransitionStatus = mixed; +export * from 'react-reconciler/src/ReactFiberConfigWithNoViewTransition'; export * from 'react-reconciler/src/ReactFiberConfigWithNoPersistence'; export * from 'react-reconciler/src/ReactFiberConfigWithNoHydration'; export * from 'react-reconciler/src/ReactFiberConfigWithNoTestSelectors'; @@ -332,148 +330,6 @@ export function unhideTextInstance( textInstance.isHidden = false; } -export function applyViewTransitionName( - instance: Instance, - name: string, - className: ?string, -): void { - // Noop -} - -export function restoreViewTransitionName( - instance: Instance, - props: Props, -): void { - // Noop -} - -export function cancelViewTransitionName( - instance: Instance, - name: string, - props: Props, -): void { - // Noop -} - -export function cancelRootViewTransitionName(rootContainer: Container): void { - // Noop -} - -export function restoreRootViewTransitionName(rootContainer: Container): void { - // Noop -} - -export function cloneRootViewTransitionContainer( - rootContainer: Container, -): Instance { - return { - type: 'ROOT', - props: {}, - isHidden: false, - children: [], - internalInstanceHandle: null, - rootContainerInstance: rootContainer, - tag: 'INSTANCE', - }; -} - -export function removeRootViewTransitionClone( - rootContainer: Container, - clone: Instance, -): void { - // Noop since it was never inserted anywhere. -} - -export type InstanceMeasurement = null; - -export function measureInstance(instance: Instance): InstanceMeasurement { - return null; -} - -export function measureClonedInstance(instance: Instance): InstanceMeasurement { - return null; -} - -export function wasInstanceInViewport( - measurement: InstanceMeasurement, -): boolean { - return true; -} - -export function hasInstanceChanged( - oldMeasurement: InstanceMeasurement, - newMeasurement: InstanceMeasurement, -): boolean { - return false; -} - -export function hasInstanceAffectedParent( - oldMeasurement: InstanceMeasurement, - newMeasurement: InstanceMeasurement, -): boolean { - return false; -} - -export function startViewTransition( - suspendedState: null | SuspendedState, - rootContainer: Container, - transitionTypes: null | TransitionTypes, - mutationCallback: () => void, - layoutCallback: () => void, - afterMutationCallback: () => void, - spawnedWorkCallback: () => void, - passiveCallback: () => mixed, - errorCallback: mixed => void, - blockedCallback: string => void, // Profiling-only - finishedAnimation: () => void, // Profiling-only -): null | RunningViewTransition { - mutationCallback(); - layoutCallback(); - // Skip afterMutationCallback(). We don't need it since we're not animating. - spawnedWorkCallback(); - // Skip passiveCallback(). Spawned work will schedule a task. - return null; -} - -export type RunningViewTransition = null; - -export function startGestureTransition( - suspendedState: null | SuspendedState, - rootContainer: Container, - timeline: GestureTimeline, - rangeStart: number, - rangeEnd: number, - transitionTypes: null | TransitionTypes, - mutationCallback: () => void, - animateCallback: () => void, - errorCallback: mixed => void, - finishedAnimation: () => void, // Profiling-only -): null | RunningViewTransition { - mutationCallback(); - animateCallback(); - if (enableProfilerTimer) { - finishedAnimation(); - } - return null; -} - -export function stopViewTransition(transition: RunningViewTransition) {} - -export function addViewTransitionFinishedListener( - transition: RunningViewTransition, - callback: () => void, -) { - callback(); -} - -export type ViewTransitionInstance = null | {name: string, ...}; - -export function createViewTransitionInstance( - name: string, -): ViewTransitionInstance { - return null; -} - export type FragmentInstanceType = null; export function createFragmentInstance( diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index ee5f22ab95..d574ad4fa1 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -80,6 +80,8 @@ export const enableTaint = __EXPERIMENTAL__; export const enableViewTransition: boolean = true; +export const enableViewTransitionForPersistenceMode: boolean = false; + export const enableGestureTransition = __EXPERIMENTAL__; export const enableScrollEndPolyfill = __EXPERIMENTAL__; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index bbb13a6eb1..bebae02dc8 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -69,6 +69,7 @@ export const transitionLaneExpirationMs = 5000; export const enableYieldingBeforePassive: boolean = false; export const enableThrottledScheduling: boolean = false; export const enableViewTransition: boolean = false; +export const enableViewTransitionForPersistenceMode: boolean = false; export const enableGestureTransition: boolean = false; export const enableScrollEndPolyfill: boolean = true; export const enableSuspenseyImages: boolean = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 6b0d934479..0a7dff59d5 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -59,6 +59,7 @@ export const enableYieldingBeforePassive: boolean = false; export const enableThrottledScheduling: boolean = false; export const enableViewTransition: boolean = true; +export const enableViewTransitionForPersistenceMode: boolean = false; export const enableGestureTransition: boolean = false; export const enableScrollEndPolyfill: boolean = true; export const enableSuspenseyImages: boolean = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 954d9d88ea..315ea3d622 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -60,6 +60,7 @@ export const enableYieldingBeforePassive: boolean = true; export const enableThrottledScheduling: boolean = false; export const enableViewTransition: boolean = true; +export const enableViewTransitionForPersistenceMode: boolean = false; export const enableGestureTransition: boolean = false; export const enableScrollEndPolyfill: boolean = true; export const enableSuspenseyImages: boolean = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index 6bc80d2b8e..70db88d0d3 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -55,6 +55,7 @@ export const transitionLaneExpirationMs = 5000; export const enableYieldingBeforePassive = false; export const enableThrottledScheduling = false; export const enableViewTransition = false; +export const enableViewTransitionForPersistenceMode = false; export const enableGestureTransition = false; export const enableScrollEndPolyfill = true; export const enableSuspenseyImages = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 91dc33b28f..c99a477caa 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -66,6 +66,7 @@ export const enableYieldingBeforePassive: boolean = false; export const enableThrottledScheduling: boolean = false; export const enableViewTransition: boolean = false; +export const enableViewTransitionForPersistenceMode: boolean = false; export const enableGestureTransition: boolean = false; export const enableScrollEndPolyfill: boolean = true; export const enableSuspenseyImages: boolean = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 76e3909cca..a07f344142 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -98,6 +98,8 @@ export const disableLegacyMode: boolean = true; export const enableEagerAlternateStateNodeCleanup: boolean = true; +export const enableViewTransitionForPersistenceMode: boolean = false; + export const enableGestureTransition: boolean = false; export const enableSuspenseyImages: boolean = false; diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 09e60d8b25..2de374c316 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -566,5 +566,6 @@ "578": "Already initialized Iterator.", "579": "Invalid data for bytes stream.", "580": "Server Function has too many bound arguments. Received %s but the limit is %s.", - "581": "BigInt is too large. Received %s digits but the limit is %s." + "581": "BigInt is too large. Received %s digits but the limit is %s.", + "582": "The current renderer does not support view transitions. This error is likely caused by a bug in React. Please file an issue." } diff --git a/scripts/flow/react-native-host-hooks.js b/scripts/flow/react-native-host-hooks.js index 227c78bca2..db8bbd9efa 100644 --- a/scripts/flow/react-native-host-hooks.js +++ b/scripts/flow/react-native-host-hooks.js @@ -301,5 +301,23 @@ declare const nativeFabricUIManager: { unstable_ContinuousEventPriority: number, unstable_IdleEventPriority: number, unstable_getCurrentEventPriority: () => number, + measureInstance: (node: Object) => { + x: number, + y: number, + width: number, + height: number, + }, + applyViewTransitionName: ( + node: Object, + name: string, + className: ?string, + ) => void, + startViewTransition: ( + mutationCallback: () => void, + onReady: () => void, + onComplete: () => void, + ) => boolean, + restoreViewTransitionName: (node: Object) => void, + cancelViewTransitionName: (node: Object, oldName: string) => void, ... };