From 0bf1f39ec6906c666011c0c57aa56aa34a262daf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 10 Jan 2025 11:51:37 -0500 Subject: [PATCH] View Transition Refs (#32038) This adds refs to View Transition that can resolve to an instance of: ```js type ViewTransitionRef = { name: string, group: Animatable, imagePair: Animatable, old: Animatable, new: Animatable, } ``` Animatable is a type that has `animate(keyframes, options)` and `getAnimations()` on it. It's the interface that exists on Element that lets you start animations on it. These ones are like that but for the four pseudo-elements created by the view transition. If a name changes, then a new ref is created. That way if you hold onto a ref during an exit animation spawned by the name change, you can keep calling functions on it. It will keep referring to the old name rather than the new name. This allows imperative control over the animations instead of using CSS for this. ```js const viewTransition = ref.current; const groupAnimation = viewTransition.group.animate(keyframes, options); const imagePairAnimation = viewTransition.imagePair.animate(keyframes, options); const oldAnimation = viewTransition.old.animate(keyframes, options); const newAnimation = viewTransition.new.animate(keyframes, options); ``` The downside of using this API is that it doesn't work with SSR so for SSR rendered animations they'll fallback to the CSS. You could use this for progressive enhancement though. Note: In this PR the ref only controls one DOM node child but there can be more than one DOM node in the ViewTransition fragment and they are just left to their defaults. We could try something like making the `animate()` function apply to multiple children but that could lead to some weird consequences and the return value would be difficult to merge. We could try to maintain an array of Animatable that updates with how ever many things are currently animating but that makes the API more complicated to use for the simple case. Conceptually this should be like a fragment so we would ideally combine the multiple children into a single isolate if we could. Maybe one day the same name could be applied to multiple children to create a single isolate. For now I think I'll just leave it like this and you're really expect to just use it with one DOM node. If you have more than one they just get the default animations from CSS. Using this is a little tricky due timing. In this fixture I just use a layout effect plus rAF to get into the right timing after the startViewTransition is ready. In the future I'll add an event that fires when View Transitions heuristics fire with the right timing. --- .eslintrc.js | 5 ++ .../view-transition/src/components/Page.js | 16 +++- packages/react-art/src/ReactFiberConfigART.js | 8 ++ .../src/client/ReactFiberConfigDOM.js | 77 +++++++++++++++++++ .../src/ReactFiberConfigNative.js | 8 ++ .../src/createReactNoop.js | 6 ++ packages/react-reconciler/src/ReactFiber.js | 5 +- .../src/ReactFiberBeginWork.js | 10 ++- .../src/ReactFiberCommitEffects.js | 35 ++++++--- .../src/ReactFiberCommitWork.js | 65 ++++++++++++---- .../src/ReactFiberCompleteWork.js | 4 +- .../src/ReactFiberConfigWithNoMutation.js | 2 + .../src/ReactFiberViewTransitionComponent.js | 10 ++- .../src/ReactFiberWorkLoop.js | 10 +-- .../src/forks/ReactFiberConfig.custom.js | 3 + .../src/ReactFiberConfigTestHost.js | 8 ++ 16 files changed, 232 insertions(+), 40 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index ccd162593f5ca..280f57221ce76 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -589,6 +589,11 @@ module.exports = { WheelEventHandler: 'readonly', FinalizationRegistry: 'readonly', Omit: 'readonly', + Keyframe: 'readonly', + PropertyIndexedKeyframes: 'readonly', + KeyframeAnimationOptions: 'readonly', + GetAnimationsOptions: 'readonly', + Animatable: 'readonly', spyOnDev: 'readonly', spyOnDevAndProd: 'readonly', diff --git a/fixtures/view-transition/src/components/Page.js b/fixtures/view-transition/src/components/Page.js index 4e2e806055615..8a3638b01f4b3 100644 --- a/fixtures/view-transition/src/components/Page.js +++ b/fixtures/view-transition/src/components/Page.js @@ -1,6 +1,8 @@ import React, { unstable_ViewTransition as ViewTransition, unstable_Activity as Activity, + useRef, + useLayoutEffect, } from 'react'; import './Page.css'; @@ -35,7 +37,19 @@ function Component() { } export default function Page({url, navigate}) { + const ref = useRef(); const show = url === '/?b'; + useLayoutEffect(() => { + const viewTransition = ref.current; + requestAnimationFrame(() => { + const keyframes = [ + {rotate: '0deg', transformOrigin: '30px 8px'}, + {rotate: '360deg', transformOrigin: '30px 8px'}, + ]; + viewTransition.old.animate(keyframes, 300); + viewTransition.new.animate(keyframes, 300); + }); + }, [show]); const exclamation = ( ! @@ -62,7 +76,7 @@ export default function Page({url, navigate}) { {a} )} - + {show ?
hello{exclamation}
:
Loading
}

scroll me

diff --git a/packages/react-art/src/ReactFiberConfigART.js b/packages/react-art/src/ReactFiberConfigART.js index 7ad2fe0e2bb38..95acc9e541e99 100644 --- a/packages/react-art/src/ReactFiberConfigART.js +++ b/packages/react-art/src/ReactFiberConfigART.js @@ -500,6 +500,14 @@ export function startViewTransition() { return false; } +export type ViewTransitionInstance = null | {name: string, ...}; + +export function createViewTransitionInstance( + name: string, +): ViewTransitionInstance { + return null; +} + 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 9bb20a56df67e..af93760ccfcc9 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -187,6 +187,14 @@ export type RendererInspectionConfig = $ReadOnly<{}>; export type TransitionStatus = FormStatus; +export type ViewTransitionInstance = { + name: string, + group: Animatable, + imagePair: Animatable, + old: Animatable, + new: Animatable, +}; + type SelectionInformation = { focusedElem: null | HTMLElement, selectionRange: mixed, @@ -1323,6 +1331,75 @@ export function startViewTransition( } } +interface ViewTransitionPseudoElementType extends Animatable { + _scope: HTMLElement; + _selector: string; +} + +function ViewTransitionPseudoElement( + this: ViewTransitionPseudoElementType, + pseudo: string, + name: string, +) { + // TODO: Get the owner document from the root container. + this._scope = (document.documentElement: any); + this._selector = '::view-transition-' + pseudo + '(' + name + ')'; +} +// $FlowFixMe[prop-missing] +ViewTransitionPseudoElement.prototype.animate = function ( + this: ViewTransitionPseudoElementType, + keyframes: Keyframe[] | PropertyIndexedKeyframes | null, + options?: number | KeyframeAnimationOptions, +): Animation { + const opts: any = + typeof options === 'number' + ? { + duration: options, + } + : Object.assign(({}: KeyframeAnimationOptions), options); + opts.pseudoElement = this._selector; + // TODO: Handle multiple child instances. + return this._scope.animate(keyframes, opts); +}; +// $FlowFixMe[prop-missing] +ViewTransitionPseudoElement.prototype.getAnimations = function ( + this: ViewTransitionPseudoElementType, + options?: GetAnimationsOptions, +): Animation[] { + const scope = this._scope; + const selector = this._selector; + const animations = scope.getAnimations({subtree: true}); + const result = []; + for (let i = 0; i < animations.length; i++) { + const effect: null | { + target?: Element, + pseudoElement?: string, + ... + } = (animations[i].effect: any); + // TODO: Handle multiple child instances. + if ( + effect !== null && + effect.target === scope && + effect.pseudoElement === selector + ) { + result.push(animations[i]); + } + } + return result; +}; + +export function createViewTransitionInstance( + name: string, +): ViewTransitionInstance { + return { + name: name, + group: new (ViewTransitionPseudoElement: any)('group', name), + imagePair: new (ViewTransitionPseudoElement: any)('image-pair', name), + old: new (ViewTransitionPseudoElement: any)('old', name), + new: new (ViewTransitionPseudoElement: any)('new', name), + }; +} + export function clearContainer(container: Container): void { const nodeType = container.nodeType; if (nodeType === DOCUMENT_NODE) { diff --git a/packages/react-native-renderer/src/ReactFiberConfigNative.js b/packages/react-native-renderer/src/ReactFiberConfigNative.js index 55699f0973b76..9bbe8b962a65a 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigNative.js +++ b/packages/react-native-renderer/src/ReactFiberConfigNative.js @@ -591,6 +591,14 @@ export function startViewTransition( return false; } +export type ViewTransitionInstance = null | {name: string, ...}; + +export function createViewTransitionInstance( + name: string, +): ViewTransitionInstance { + return null; +} + 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-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 6536821c1e773..b1b4a3c9405fb 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -92,6 +92,8 @@ export type TransitionStatus = mixed; export type FormInstance = Instance; +export type ViewTransitionInstance = null | {name: string, ...}; + const NO_CONTEXT = {}; const UPPERCASE_CONTEXT = {}; if (__DEV__) { @@ -786,6 +788,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { return false; }, + createViewTransitionInstance(name: string): ViewTransitionInstance { + return null; + }, + resetTextContent(instance: Instance): void { instance.text = null; }, diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 0a8b88829d24c..1ebe08b76963e 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -21,7 +21,7 @@ import type { } from './ReactFiberActivityComponent'; import type { ViewTransitionProps, - ViewTransitionInstance, + ViewTransitionState, } from './ReactFiberViewTransitionComponent'; import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent'; @@ -884,9 +884,10 @@ export function createFiberFromViewTransition( const fiber = createFiber(ViewTransitionComponent, pendingProps, key, mode); fiber.elementType = REACT_VIEW_TRANSITION_TYPE; fiber.lanes = lanes; - const instance: ViewTransitionInstance = { + const instance: ViewTransitionState = { autoName: null, paired: null, + ref: null, }; fiber.stateNode = instance; return fiber; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 970be0b3ac0e8..a7bb6e6df5acd 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -30,7 +30,7 @@ import type { } from './ReactFiberActivityComponent'; import type { ViewTransitionProps, - ViewTransitionInstance, + ViewTransitionState, } from './ReactFiberViewTransitionComponent'; import {assignViewTransitionAutoName} from './ReactFiberViewTransitionComponent'; import {OffscreenDetached} from './ReactFiberActivityComponent'; @@ -3246,7 +3246,7 @@ function updateViewTransition( renderLanes: Lanes, ) { const pendingProps: ViewTransitionProps = workInProgress.pendingProps; - const instance: ViewTransitionInstance = workInProgress.stateNode; + const instance: ViewTransitionState = workInProgress.stateNode; if (pendingProps.name != null && pendingProps.name !== 'auto') { // Explicitly named boundary. We track it so that we can pair it up with another explicit // boundary if we get deleted. @@ -3264,6 +3264,12 @@ function updateViewTransition( // counter in the commit phase instead. assignViewTransitionAutoName(pendingProps, instance); } + if (current !== null && current.memoizedProps.name !== pendingProps.name) { + // If the name changes, we schedule a ref effect to create a new ref instance. + workInProgress.flags |= Ref | RefStatic; + } else { + markRef(current, workInProgress); + } const nextChildren = pendingProps.children; reconcileChildren(current, workInProgress, nextChildren, renderLanes); return workInProgress.child; diff --git a/packages/react-reconciler/src/ReactFiberCommitEffects.js b/packages/react-reconciler/src/ReactFiberCommitEffects.js index 0847f2603439a..6873bdf9e7a63 100644 --- a/packages/react-reconciler/src/ReactFiberCommitEffects.js +++ b/packages/react-reconciler/src/ReactFiberCommitEffects.js @@ -11,21 +11,26 @@ import type {Fiber} from './ReactInternalTypes'; import type {UpdateQueue} from './ReactFiberClassUpdateQueue'; import type {FunctionComponentUpdateQueue} from './ReactFiberHooks'; import type {HookFlags} from './ReactHookEffectTags'; +import { + getViewTransitionName, + type ViewTransitionState, + type ViewTransitionProps, +} from './ReactFiberViewTransitionComponent'; import { enableProfilerTimer, enableProfilerCommitHooks, enableProfilerNestedUpdatePhase, enableSchedulingProfiler, - enableScopeAPI, enableUseResourceEffectHook, + enableViewTransition, } from 'shared/ReactFeatureFlags'; import { ClassComponent, HostComponent, HostHoistable, HostSingleton, - ScopeComponent, + ViewTransitionComponent, } from './ReactWorkTags'; import {NoFlags} from './ReactFiberFlags'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; @@ -40,7 +45,10 @@ import { commitCallbacks, commitHiddenCallbacks, } from './ReactFiberClassUpdateQueue'; -import {getPublicInstance} from './ReactFiberConfig'; +import { + getPublicInstance, + createViewTransitionInstance, +} from './ReactFiberConfig'; import { captureCommitPhaseError, setIsRunningInsertionEffect, @@ -865,20 +873,27 @@ export function safelyCallComponentWillUnmount( function commitAttachRef(finishedWork: Fiber) { const ref = finishedWork.ref; if (ref !== null) { - const instance = finishedWork.stateNode; let instanceToUse; switch (finishedWork.tag) { case HostHoistable: case HostSingleton: case HostComponent: - instanceToUse = getPublicInstance(instance); + instanceToUse = getPublicInstance(finishedWork.stateNode); break; + case ViewTransitionComponent: + if (enableViewTransition) { + const instance: ViewTransitionState = finishedWork.stateNode; + const props: ViewTransitionProps = finishedWork.memoizedProps; + const name = getViewTransitionName(props, instance); + if (instance.ref === null || instance.ref.name !== name) { + instance.ref = createViewTransitionInstance(name); + } + instanceToUse = instance.ref; + break; + } + // Fallthrough default: - instanceToUse = instance; - } - // Moved outside to ensure DCE works with this flag - if (enableScopeAPI && finishedWork.tag === ScopeComponent) { - instanceToUse = instance; + instanceToUse = finishedWork.stateNode; } if (typeof ref === 'function') { if (shouldProfile(finishedWork)) { diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 73877c52738d6..8b4baead09b89 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -43,7 +43,7 @@ import type { } from './ReactFiberTracingMarkerComponent'; import type { ViewTransitionProps, - ViewTransitionInstance, + ViewTransitionState, } from './ReactFiberViewTransitionComponent'; import { @@ -283,7 +283,7 @@ export function commitBeforeMutationEffects( root: FiberRoot, firstChild: Fiber, committedLanes: Lanes, - appearingViewTransitions: Map | null, + appearingViewTransitions: Map | null, ): void { focusedInstanceHandle = prepareForCommit(root.containerInfo); shouldFireAfterActiveInstanceBlur = false; @@ -305,7 +305,7 @@ export function commitBeforeMutationEffects( function commitBeforeMutationEffects_begin( isViewTransitionEligible: boolean, - appearingViewTransitions: Map | null, + appearingViewTransitions: Map | null, ) { // If this commit is eligible for a View Transition we look into all mutated subtrees. // TODO: We could optimize this by marking these with the Snapshot subtree flag in the render phase. @@ -523,7 +523,7 @@ function commitBeforeMutationEffectsOnFiber( function commitBeforeMutationEffectsDeletion( deletion: Fiber, isViewTransitionEligible: boolean, - appearingViewTransitions: Map | null, + appearingViewTransitions: Map | null, ) { if (enableCreateEventHandleAPI) { // TODO (effects) It would be nice to avoid calling doesFiberContain() @@ -653,7 +653,7 @@ function commitAppearingPairViewTransitions(placement: Fiber): void { child.tag === ViewTransitionComponent && (child.flags & ViewTransitionNamedStatic) !== NoFlags ) { - const instance: ViewTransitionInstance = child.stateNode; + const instance: ViewTransitionState = child.stateNode; if (instance.paired) { const props: ViewTransitionProps = child.memoizedProps; if (props.name == null || props.name === 'auto') { @@ -721,7 +721,7 @@ function commitEnterViewTransitions(placement: Fiber): void { function commitDeletedPairViewTransitions( deletion: Fiber, - appearingViewTransitions: Map, + appearingViewTransitions: Map, ): void { if (appearingViewTransitions.size === 0) { // We've found all. @@ -761,8 +761,8 @@ function commitDeletedPairViewTransitions( restoreViewTransitionOnHostInstances(child.child, false); } else { // We'll transition between them. - const oldinstance: ViewTransitionInstance = child.stateNode; - const newInstance: ViewTransitionInstance = pair; + const oldinstance: ViewTransitionState = child.stateNode; + const newInstance: ViewTransitionState = pair; newInstance.paired = oldinstance; } // Delete the entry so that we know when we've found all of them @@ -782,7 +782,7 @@ function commitDeletedPairViewTransitions( function commitExitViewTransitions( deletion: Fiber, - appearingViewTransitions: Map | null, + appearingViewTransitions: Map | null, ): void { if (deletion.tag === ViewTransitionComponent) { const props: ViewTransitionProps = deletion.memoizedProps; @@ -805,8 +805,8 @@ function commitExitViewTransitions( if (pair !== undefined) { // We found a new appearing view transition with the same name as this deletion. // We'll transition between them instead of running the normal exit. - const oldinstance: ViewTransitionInstance = deletion.stateNode; - const newInstance: ViewTransitionInstance = pair; + const oldinstance: ViewTransitionState = deletion.stateNode; + const newInstance: ViewTransitionState = pair; newInstance.paired = oldinstance; // Delete the entry so that we know when we've found all of them // and can stop searching (size reaches zero). @@ -894,7 +894,7 @@ function restorePairedViewTransitions(parent: Fiber): void { child.tag === ViewTransitionComponent && (child.flags & ViewTransitionNamedStatic) !== NoFlags ) { - const instance: ViewTransitionInstance = child.stateNode; + const instance: ViewTransitionState = child.stateNode; if (instance.paired !== null) { instance.paired = null; restoreViewTransitionOnHostInstances(child.child, false); @@ -908,7 +908,7 @@ function restorePairedViewTransitions(parent: Fiber): void { function restoreEnterViewTransitions(placement: Fiber): void { if (placement.tag === ViewTransitionComponent) { - const instance: ViewTransitionInstance = placement.stateNode; + const instance: ViewTransitionState = placement.stateNode; instance.paired = null; restoreViewTransitionOnHostInstances(placement.child, false); restorePairedViewTransitions(placement); @@ -925,7 +925,7 @@ function restoreEnterViewTransitions(placement: Fiber): void { function restoreExitViewTransitions(deletion: Fiber): void { if (deletion.tag === ViewTransitionComponent) { - const instance: ViewTransitionInstance = deletion.stateNode; + const instance: ViewTransitionState = deletion.stateNode; instance.paired = null; restoreViewTransitionOnHostInstances(deletion.child, false); restorePairedViewTransitions(deletion); @@ -1345,6 +1345,20 @@ function commitLayoutEffectOnFiber( } break; } + case ViewTransitionComponent: { + if (enableViewTransition) { + recursivelyTraverseLayoutEffects( + finishedRoot, + finishedWork, + committedLanes, + ); + if (flags & Ref) { + safelyAttachRef(finishedWork, finishedWork.return); + } + break; + } + // Fallthrough + } default: { recursivelyTraverseLayoutEffects( finishedRoot, @@ -2830,6 +2844,11 @@ function commitMutationEffectsOnFiber( } case ViewTransitionComponent: if (enableViewTransition) { + if (flags & Ref) { + if (!offscreenSubtreeWasHidden && current !== null) { + safelyDetachRef(current, current.return); + } + } const prevMutationContext = pushMutationContext(); recursivelyTraverseMutationEffects(root, finishedWork, lanes); commitReconciliationEffects(finishedWork, lanes); @@ -3194,6 +3213,12 @@ export function disappearLayoutEffects(finishedWork: Fiber) { } break; } + case ViewTransitionComponent: { + if (enableViewTransition) { + safelyDetachRef(finishedWork, finishedWork.return); + } + // Fallthrough + } default: { recursivelyTraverseDisappearLayoutEffects(finishedWork); break; @@ -3368,6 +3393,18 @@ export function reappearLayoutEffects( safelyAttachRef(finishedWork, finishedWork.return); break; } + case ViewTransitionComponent: { + if (enableViewTransition) { + recursivelyTraverseReappearLayoutEffects( + finishedRoot, + finishedWork, + includeWorkInProgressEffects, + ); + safelyAttachRef(finishedWork, finishedWork.return); + break; + } + // Fallthrough + } default: { recursivelyTraverseReappearLayoutEffects( finishedRoot, diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 2066a110a1357..ca6462d7e7d5a 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -30,7 +30,7 @@ import type { } from './ReactFiberActivityComponent'; import type { ViewTransitionProps, - ViewTransitionInstance, + ViewTransitionState, } from './ReactFiberViewTransitionComponent'; import {isOffscreenManual} from './ReactFiberActivityComponent'; import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent'; @@ -965,7 +965,7 @@ function trackReappearingViewTransitions(workInProgress: Fiber): void { ) { const props: ViewTransitionProps = child.memoizedProps; if (props.name != null && props.name !== 'auto') { - const instance: ViewTransitionInstance = child.stateNode; + const instance: ViewTransitionState = child.stateNode; trackAppearingViewTransition(instance, props.name); } } diff --git a/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js b/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js index 57a7ea793a9c5..5e54757807cd8 100644 --- a/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js +++ b/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js @@ -46,3 +46,5 @@ export const wasInstanceInViewport = shim; export const hasInstanceChanged = shim; export const hasInstanceAffectedParent = shim; export const startViewTransition = shim; +export type ViewTransitionInstance = null | {name: string, ...}; +export const createViewTransitionInstance = shim; diff --git a/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js b/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js index 0fd74b5bb6c99..008a6992f275d 100644 --- a/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js +++ b/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js @@ -9,6 +9,7 @@ import type {ReactNodeList} from 'shared/ReactTypes'; import type {FiberRoot} from './ReactInternalTypes'; +import type {ViewTransitionInstance} from './ReactFiberConfig'; import {getWorkInProgressRoot} from './ReactFiberWorkLoop'; @@ -22,16 +23,17 @@ export type ViewTransitionProps = { children?: ReactNodeList, }; -export type ViewTransitionInstance = { +export type ViewTransitionState = { autoName: null | string, // the view-transition-name to use when an explicit one is not specified - paired: null | ViewTransitionInstance, // a temporary state during the commit phase if we have paired this with another instance + paired: null | ViewTransitionState, // a temporary state during the commit phase if we have paired this with another instance + ref: null | ViewTransitionInstance, // the current ref instance. This can change through the lifetime of the instance. }; let globalClientIdCounter: number = 0; export function assignViewTransitionAutoName( props: ViewTransitionProps, - instance: ViewTransitionInstance, + instance: ViewTransitionState, ): string { if (instance.autoName !== null) { return instance.autoName; @@ -61,7 +63,7 @@ export function assignViewTransitionAutoName( export function getViewTransitionName( props: ViewTransitionProps, - instance: ViewTransitionInstance, + instance: ViewTransitionState, ): string { if (props.name != null && props.name !== 'auto') { return props.name; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index c79d6d541c271..f3ba210bf9704 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -23,7 +23,7 @@ import type { import type {OffscreenInstance} from './ReactFiberActivityComponent'; import type {Resource} from './ReactFiberConfig'; import type {RootState} from './ReactFiberRoot'; -import type {ViewTransitionInstance} from './ReactFiberViewTransitionComponent'; +import type {ViewTransitionState} from './ReactFiberViewTransitionComponent'; import { enableCreateEventHandleAPI, @@ -431,7 +431,7 @@ let workInProgressRootRecoverableErrors: Array> | null = // pairs in the snapshot phase. let workInProgressAppearingViewTransitions: Map< string, - ViewTransitionInstance, + ViewTransitionState, > | null = null; // Tracks when an update occurs during the render phase. @@ -1377,7 +1377,7 @@ function commitRootWhenReady( finishedWork: Fiber, recoverableErrors: Array> | null, transitions: Array | null, - appearingViewTransitions: Map | null, + appearingViewTransitions: Map | null, didIncludeRenderPhaseUpdate: boolean, lanes: Lanes, spawnedLane: Lane, @@ -2270,7 +2270,7 @@ export function renderHasNotSuspendedYet(): boolean { } export function trackAppearingViewTransition( - instance: ViewTransitionInstance, + instance: ViewTransitionState, name: string, ): void { if (workInProgressAppearingViewTransitions === null) { @@ -3197,7 +3197,7 @@ function commitRoot( lanes: Lanes, recoverableErrors: null | Array>, transitions: Array | null, - appearingViewTransitions: Map | null, + appearingViewTransitions: Map | null, didIncludeRenderPhaseUpdate: boolean, spawnedLane: Lane, updatedLanes: Lanes, diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js index 10e419f2c08ef..5d6ab3224ee51 100644 --- a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js @@ -40,6 +40,7 @@ export opaque type NoTimeout = mixed; export opaque type RendererInspectionConfig = mixed; export opaque type TransitionStatus = mixed; export opaque type FormInstance = mixed; +export type ViewTransitionInstance = null | {name: string, ...}; export opaque type InstanceMeasurement = mixed; export type EventResponder = any; @@ -143,6 +144,8 @@ export const wasInstanceInViewport = $$$config.wasInstanceInViewport; export const hasInstanceChanged = $$$config.hasInstanceChanged; export const hasInstanceAffectedParent = $$$config.hasInstanceAffectedParent; export const startViewTransition = $$$config.startViewTransition; +export const createViewTransitionInstance = + $$$config.createViewTransitionInstance; export const clearContainer = $$$config.clearContainer; // ------------------- diff --git a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js index c05f59af160f0..a33590a32b2f0 100644 --- a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js +++ b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js @@ -373,6 +373,14 @@ export function startViewTransition( return false; } +export type ViewTransitionInstance = null | {name: string, ...}; + +export function createViewTransitionInstance( + name: string, +): ViewTransitionInstance { + return null; +} + export function getInstanceFromNode(mockNode: Object): Object | null { const instance = nodeToInstanceMap.get(mockNode); if (instance !== undefined) {