@samitouri / QOS-React / commits / 0bf1f39ec6

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.

Sebastian Markbåge committed Jan 10, 2025 at 11:51 UTC 0bf1f39ec6906c666011c0c57aa56aa34a262daf
16 files changed +232 -40
.eslintrc.js
+5
@@ -589,6 +589,11 @@ module.exports = {
589 WheelEventHandler: 'readonly',
590 FinalizationRegistry: 'readonly',
591 Omit: 'readonly',
592 + Keyframe: 'readonly',
593 + PropertyIndexedKeyframes: 'readonly',
594 + KeyframeAnimationOptions: 'readonly',
595 + GetAnimationsOptions: 'readonly',
596 + Animatable: 'readonly',
597
598 spyOnDev: 'readonly',
599 spyOnDevAndProd: 'readonly',
fixtures/view-transition/src/components/Page.js
+15 -1
@@ -1,6 +1,8 @@
1 import React, {
2 unstable_ViewTransition as ViewTransition,
3 unstable_Activity as Activity,
4 + useRef,
5 + useLayoutEffect,
6 } from 'react';
7
8 import './Page.css';
@@ -35,7 +37,19 @@ function Component() {
37 }
38
39 export default function Page({url, navigate}) {
40 + const ref = useRef();
41 const show = url === '/?b';
42 + useLayoutEffect(() => {
43 + const viewTransition = ref.current;
44 + requestAnimationFrame(() => {
45 + const keyframes = [
46 + {rotate: '0deg', transformOrigin: '30px 8px'},
47 + {rotate: '360deg', transformOrigin: '30px 8px'},
48 + ];
49 + viewTransition.old.animate(keyframes, 300);
50 + viewTransition.new.animate(keyframes, 300);
51 + });
52 + }, [show]);
53 const exclamation = (
54 <ViewTransition name="exclamation">
55 <span>!</span>
@@ -62,7 +76,7 @@ export default function Page({url, navigate}) {
76 {a}
77 </div>
78 )}
65 - <ViewTransition>
79 + <ViewTransition ref={ref}>
80 {show ? <div>hello{exclamation}</div> : <section>Loading</section>}
81 </ViewTransition>
82 <p>scroll me</p>
packages/react-art/src/ReactFiberConfigART.js
+8
@@ -500,6 +500,14 @@ export function startViewTransition() {
500 return false;
501 }
502
503 +export type ViewTransitionInstance = null | {name: string, ...};
504 +
505 +export function createViewTransitionInstance(
506 + name: string,
507 +): ViewTransitionInstance {
508 + return null;
509 +}
510 +
511 export function clearContainer(container) {
512 // TODO Implement this
513 }
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+77
@@ -187,6 +187,14 @@ export type RendererInspectionConfig = $ReadOnly<{}>;
187
188 export type TransitionStatus = FormStatus;
189
190 +export type ViewTransitionInstance = {
191 + name: string,
192 + group: Animatable,
193 + imagePair: Animatable,
194 + old: Animatable,
195 + new: Animatable,
196 +};
197 +
198 type SelectionInformation = {
199 focusedElem: null | HTMLElement,
200 selectionRange: mixed,
@@ -1323,6 +1331,75 @@ export function startViewTransition(
1331 }
1332 }
1333
1334 +interface ViewTransitionPseudoElementType extends Animatable {
1335 + _scope: HTMLElement;
1336 + _selector: string;
1337 +}
1338 +
1339 +function ViewTransitionPseudoElement(
1340 + this: ViewTransitionPseudoElementType,
1341 + pseudo: string,
1342 + name: string,
1343 +) {
1344 + // TODO: Get the owner document from the root container.
1345 + this._scope = (document.documentElement: any);
1346 + this._selector = '::view-transition-' + pseudo + '(' + name + ')';
1347 +}
1348 +// $FlowFixMe[prop-missing]
1349 +ViewTransitionPseudoElement.prototype.animate = function (
1350 + this: ViewTransitionPseudoElementType,
1351 + keyframes: Keyframe[] | PropertyIndexedKeyframes | null,
1352 + options?: number | KeyframeAnimationOptions,
1353 +): Animation {
1354 + const opts: any =
1355 + typeof options === 'number'
1356 + ? {
1357 + duration: options,
1358 + }
1359 + : Object.assign(({}: KeyframeAnimationOptions), options);
1360 + opts.pseudoElement = this._selector;
1361 + // TODO: Handle multiple child instances.
1362 + return this._scope.animate(keyframes, opts);
1363 +};
1364 +// $FlowFixMe[prop-missing]
1365 +ViewTransitionPseudoElement.prototype.getAnimations = function (
1366 + this: ViewTransitionPseudoElementType,
1367 + options?: GetAnimationsOptions,
1368 +): Animation[] {
1369 + const scope = this._scope;
1370 + const selector = this._selector;
1371 + const animations = scope.getAnimations({subtree: true});
1372 + const result = [];
1373 + for (let i = 0; i < animations.length; i++) {
1374 + const effect: null | {
1375 + target?: Element,
1376 + pseudoElement?: string,
1377 + ...
1378 + } = (animations[i].effect: any);
1379 + // TODO: Handle multiple child instances.
1380 + if (
1381 + effect !== null &&
1382 + effect.target === scope &&
1383 + effect.pseudoElement === selector
1384 + ) {
1385 + result.push(animations[i]);
1386 + }
1387 + }
1388 + return result;
1389 +};
1390 +
1391 +export function createViewTransitionInstance(
1392 + name: string,
1393 +): ViewTransitionInstance {
1394 + return {
1395 + name: name,
1396 + group: new (ViewTransitionPseudoElement: any)('group', name),
1397 + imagePair: new (ViewTransitionPseudoElement: any)('image-pair', name),
1398 + old: new (ViewTransitionPseudoElement: any)('old', name),
1399 + new: new (ViewTransitionPseudoElement: any)('new', name),
1400 + };
1401 +}
1402 +
1403 export function clearContainer(container: Container): void {
1404 const nodeType = container.nodeType;
1405 if (nodeType === DOCUMENT_NODE) {
packages/react-native-renderer/src/ReactFiberConfigNative.js
+8
@@ -591,6 +591,14 @@ export function startViewTransition(
591 return false;
592 }
593
594 +export type ViewTransitionInstance = null | {name: string, ...};
595 +
596 +export function createViewTransitionInstance(
597 + name: string,
598 +): ViewTransitionInstance {
599 + return null;
600 +}
601 +
602 export function clearContainer(container: Container): void {
603 // TODO Implement this for React Native
604 // UIManager does not expose a "remove all" type method.
packages/react-noop-renderer/src/createReactNoop.js
+6
@@ -92,6 +92,8 @@ export type TransitionStatus = mixed;
92
93 export type FormInstance = Instance;
94
95 +export type ViewTransitionInstance = null | {name: string, ...};
96 +
97 const NO_CONTEXT = {};
98 const UPPERCASE_CONTEXT = {};
99 if (__DEV__) {
@@ -786,6 +788,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
788 return false;
789 },
790
791 + createViewTransitionInstance(name: string): ViewTransitionInstance {
792 + return null;
793 + },
794 +
795 resetTextContent(instance: Instance): void {
796 instance.text = null;
797 },
packages/react-reconciler/src/ReactFiber.js
+3 -2
@@ -21,7 +21,7 @@ import type {
21 } from './ReactFiberActivityComponent';
22 import type {
23 ViewTransitionProps,
24 - ViewTransitionInstance,
24 + ViewTransitionState,
25 } from './ReactFiberViewTransitionComponent';
26 import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent';
27
@@ -884,9 +884,10 @@ export function createFiberFromViewTransition(
884 const fiber = createFiber(ViewTransitionComponent, pendingProps, key, mode);
885 fiber.elementType = REACT_VIEW_TRANSITION_TYPE;
886 fiber.lanes = lanes;
887 - const instance: ViewTransitionInstance = {
887 + const instance: ViewTransitionState = {
888 autoName: null,
889 paired: null,
890 + ref: null,
891 };
892 fiber.stateNode = instance;
893 return fiber;
packages/react-reconciler/src/ReactFiberBeginWork.js
+8 -2
@@ -30,7 +30,7 @@ import type {
30 } from './ReactFiberActivityComponent';
31 import type {
32 ViewTransitionProps,
33 - ViewTransitionInstance,
33 + ViewTransitionState,
34 } from './ReactFiberViewTransitionComponent';
35 import {assignViewTransitionAutoName} from './ReactFiberViewTransitionComponent';
36 import {OffscreenDetached} from './ReactFiberActivityComponent';
@@ -3246,7 +3246,7 @@ function updateViewTransition(
3246 renderLanes: Lanes,
3247 ) {
3248 const pendingProps: ViewTransitionProps = workInProgress.pendingProps;
3249 - const instance: ViewTransitionInstance = workInProgress.stateNode;
3249 + const instance: ViewTransitionState = workInProgress.stateNode;
3250 if (pendingProps.name != null && pendingProps.name !== 'auto') {
3251 // Explicitly named boundary. We track it so that we can pair it up with another explicit
3252 // boundary if we get deleted.
@@ -3264,6 +3264,12 @@ function updateViewTransition(
3264 // counter in the commit phase instead.
3265 assignViewTransitionAutoName(pendingProps, instance);
3266 }
3267 + if (current !== null && current.memoizedProps.name !== pendingProps.name) {
3268 + // If the name changes, we schedule a ref effect to create a new ref instance.
3269 + workInProgress.flags |= Ref | RefStatic;
3270 + } else {
3271 + markRef(current, workInProgress);
3272 + }
3273 const nextChildren = pendingProps.children;
3274 reconcileChildren(current, workInProgress, nextChildren, renderLanes);
3275 return workInProgress.child;
packages/react-reconciler/src/ReactFiberCommitEffects.js
+25 -10
@@ -11,21 +11,26 @@ import type {Fiber} from './ReactInternalTypes';
11 import type {UpdateQueue} from './ReactFiberClassUpdateQueue';
12 import type {FunctionComponentUpdateQueue} from './ReactFiberHooks';
13 import type {HookFlags} from './ReactHookEffectTags';
14 +import {
15 + getViewTransitionName,
16 + type ViewTransitionState,
17 + type ViewTransitionProps,
18 +} from './ReactFiberViewTransitionComponent';
19
20 import {
21 enableProfilerTimer,
22 enableProfilerCommitHooks,
23 enableProfilerNestedUpdatePhase,
24 enableSchedulingProfiler,
20 - enableScopeAPI,
25 enableUseResourceEffectHook,
26 + enableViewTransition,
27 } from 'shared/ReactFeatureFlags';
28 import {
29 ClassComponent,
30 HostComponent,
31 HostHoistable,
32 HostSingleton,
28 - ScopeComponent,
33 + ViewTransitionComponent,
34 } from './ReactWorkTags';
35 import {NoFlags} from './ReactFiberFlags';
36 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
@@ -40,7 +45,10 @@ import {
45 commitCallbacks,
46 commitHiddenCallbacks,
47 } from './ReactFiberClassUpdateQueue';
43 -import {getPublicInstance} from './ReactFiberConfig';
48 +import {
49 + getPublicInstance,
50 + createViewTransitionInstance,
51 +} from './ReactFiberConfig';
52 import {
53 captureCommitPhaseError,
54 setIsRunningInsertionEffect,
@@ -865,20 +873,27 @@ export function safelyCallComponentWillUnmount(
873 function commitAttachRef(finishedWork: Fiber) {
874 const ref = finishedWork.ref;
875 if (ref !== null) {
868 - const instance = finishedWork.stateNode;
876 let instanceToUse;
877 switch (finishedWork.tag) {
878 case HostHoistable:
879 case HostSingleton:
880 case HostComponent:
874 - instanceToUse = getPublicInstance(instance);
881 + instanceToUse = getPublicInstance(finishedWork.stateNode);
882 break;
883 + case ViewTransitionComponent:
884 + if (enableViewTransition) {
885 + const instance: ViewTransitionState = finishedWork.stateNode;
886 + const props: ViewTransitionProps = finishedWork.memoizedProps;
887 + const name = getViewTransitionName(props, instance);
888 + if (instance.ref === null || instance.ref.name !== name) {
889 + instance.ref = createViewTransitionInstance(name);
890 + }
891 + instanceToUse = instance.ref;
892 + break;
893 + }
894 + // Fallthrough
895 default:
877 - instanceToUse = instance;
878 - }
879 - // Moved outside to ensure DCE works with this flag
880 - if (enableScopeAPI && finishedWork.tag === ScopeComponent) {
881 - instanceToUse = instance;
896 + instanceToUse = finishedWork.stateNode;
897 }
898 if (typeof ref === 'function') {
899 if (shouldProfile(finishedWork)) {
packages/react-reconciler/src/ReactFiberCommitWork.js
+51 -14
@@ -43,7 +43,7 @@ import type {
43 } from './ReactFiberTracingMarkerComponent';
44 import type {
45 ViewTransitionProps,
46 - ViewTransitionInstance,
46 + ViewTransitionState,
47 } from './ReactFiberViewTransitionComponent';
48
49 import {
@@ -283,7 +283,7 @@ export function commitBeforeMutationEffects(
283 root: FiberRoot,
284 firstChild: Fiber,
285 committedLanes: Lanes,
286 - appearingViewTransitions: Map<string, ViewTransitionInstance> | null,
286 + appearingViewTransitions: Map<string, ViewTransitionState> | null,
287 ): void {
288 focusedInstanceHandle = prepareForCommit(root.containerInfo);
289 shouldFireAfterActiveInstanceBlur = false;
@@ -305,7 +305,7 @@ export function commitBeforeMutationEffects(
305
306 function commitBeforeMutationEffects_begin(
307 isViewTransitionEligible: boolean,
308 - appearingViewTransitions: Map<string, ViewTransitionInstance> | null,
308 + appearingViewTransitions: Map<string, ViewTransitionState> | null,
309 ) {
310 // If this commit is eligible for a View Transition we look into all mutated subtrees.
311 // TODO: We could optimize this by marking these with the Snapshot subtree flag in the render phase.
@@ -523,7 +523,7 @@ function commitBeforeMutationEffectsOnFiber(
523 function commitBeforeMutationEffectsDeletion(
524 deletion: Fiber,
525 isViewTransitionEligible: boolean,
526 - appearingViewTransitions: Map<string, ViewTransitionInstance> | null,
526 + appearingViewTransitions: Map<string, ViewTransitionState> | null,
527 ) {
528 if (enableCreateEventHandleAPI) {
529 // TODO (effects) It would be nice to avoid calling doesFiberContain()
@@ -653,7 +653,7 @@ function commitAppearingPairViewTransitions(placement: Fiber): void {
653 child.tag === ViewTransitionComponent &&
654 (child.flags & ViewTransitionNamedStatic) !== NoFlags
655 ) {
656 - const instance: ViewTransitionInstance = child.stateNode;
656 + const instance: ViewTransitionState = child.stateNode;
657 if (instance.paired) {
658 const props: ViewTransitionProps = child.memoizedProps;
659 if (props.name == null || props.name === 'auto') {
@@ -721,7 +721,7 @@ function commitEnterViewTransitions(placement: Fiber): void {
721
722 function commitDeletedPairViewTransitions(
723 deletion: Fiber,
724 - appearingViewTransitions: Map<string, ViewTransitionInstance>,
724 + appearingViewTransitions: Map<string, ViewTransitionState>,
725 ): void {
726 if (appearingViewTransitions.size === 0) {
727 // We've found all.
@@ -761,8 +761,8 @@ function commitDeletedPairViewTransitions(
761 restoreViewTransitionOnHostInstances(child.child, false);
762 } else {
763 // We'll transition between them.
764 - const oldinstance: ViewTransitionInstance = child.stateNode;
765 - const newInstance: ViewTransitionInstance = pair;
764 + const oldinstance: ViewTransitionState = child.stateNode;
765 + const newInstance: ViewTransitionState = pair;
766 newInstance.paired = oldinstance;
767 }
768 // Delete the entry so that we know when we've found all of them
@@ -782,7 +782,7 @@ function commitDeletedPairViewTransitions(
782
783 function commitExitViewTransitions(
784 deletion: Fiber,
785 - appearingViewTransitions: Map<string, ViewTransitionInstance> | null,
785 + appearingViewTransitions: Map<string, ViewTransitionState> | null,
786 ): void {
787 if (deletion.tag === ViewTransitionComponent) {
788 const props: ViewTransitionProps = deletion.memoizedProps;
@@ -805,8 +805,8 @@ function commitExitViewTransitions(
805 if (pair !== undefined) {
806 // We found a new appearing view transition with the same name as this deletion.
807 // We'll transition between them instead of running the normal exit.
808 - const oldinstance: ViewTransitionInstance = deletion.stateNode;
809 - const newInstance: ViewTransitionInstance = pair;
808 + const oldinstance: ViewTransitionState = deletion.stateNode;
809 + const newInstance: ViewTransitionState = pair;
810 newInstance.paired = oldinstance;
811 // Delete the entry so that we know when we've found all of them
812 // and can stop searching (size reaches zero).
@@ -894,7 +894,7 @@ function restorePairedViewTransitions(parent: Fiber): void {
894 child.tag === ViewTransitionComponent &&
895 (child.flags & ViewTransitionNamedStatic) !== NoFlags
896 ) {
897 - const instance: ViewTransitionInstance = child.stateNode;
897 + const instance: ViewTransitionState = child.stateNode;
898 if (instance.paired !== null) {
899 instance.paired = null;
900 restoreViewTransitionOnHostInstances(child.child, false);
@@ -908,7 +908,7 @@ function restorePairedViewTransitions(parent: Fiber): void {
908
909 function restoreEnterViewTransitions(placement: Fiber): void {
910 if (placement.tag === ViewTransitionComponent) {
911 - const instance: ViewTransitionInstance = placement.stateNode;
911 + const instance: ViewTransitionState = placement.stateNode;
912 instance.paired = null;
913 restoreViewTransitionOnHostInstances(placement.child, false);
914 restorePairedViewTransitions(placement);
@@ -925,7 +925,7 @@ function restoreEnterViewTransitions(placement: Fiber): void {
925
926 function restoreExitViewTransitions(deletion: Fiber): void {
927 if (deletion.tag === ViewTransitionComponent) {
928 - const instance: ViewTransitionInstance = deletion.stateNode;
928 + const instance: ViewTransitionState = deletion.stateNode;
929 instance.paired = null;
930 restoreViewTransitionOnHostInstances(deletion.child, false);
931 restorePairedViewTransitions(deletion);
@@ -1345,6 +1345,20 @@ function commitLayoutEffectOnFiber(
1345 }
1346 break;
1347 }
1348 + case ViewTransitionComponent: {
1349 + if (enableViewTransition) {
1350 + recursivelyTraverseLayoutEffects(
1351 + finishedRoot,
1352 + finishedWork,
1353 + committedLanes,
1354 + );
1355 + if (flags & Ref) {
1356 + safelyAttachRef(finishedWork, finishedWork.return);
1357 + }
1358 + break;
1359 + }
1360 + // Fallthrough
1361 + }
1362 default: {
1363 recursivelyTraverseLayoutEffects(
1364 finishedRoot,
@@ -2830,6 +2844,11 @@ function commitMutationEffectsOnFiber(
2844 }
2845 case ViewTransitionComponent:
2846 if (enableViewTransition) {
2847 + if (flags & Ref) {
2848 + if (!offscreenSubtreeWasHidden && current !== null) {
2849 + safelyDetachRef(current, current.return);
2850 + }
2851 + }
2852 const prevMutationContext = pushMutationContext();
2853 recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2854 commitReconciliationEffects(finishedWork, lanes);
@@ -3194,6 +3213,12 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
3213 }
3214 break;
3215 }
3216 + case ViewTransitionComponent: {
3217 + if (enableViewTransition) {
3218 + safelyDetachRef(finishedWork, finishedWork.return);
3219 + }
3220 + // Fallthrough
3221 + }
3222 default: {
3223 recursivelyTraverseDisappearLayoutEffects(finishedWork);
3224 break;
@@ -3368,6 +3393,18 @@ export function reappearLayoutEffects(
3393 safelyAttachRef(finishedWork, finishedWork.return);
3394 break;
3395 }
3396 + case ViewTransitionComponent: {
3397 + if (enableViewTransition) {
3398 + recursivelyTraverseReappearLayoutEffects(
3399 + finishedRoot,
3400 + finishedWork,
3401 + includeWorkInProgressEffects,
3402 + );
3403 + safelyAttachRef(finishedWork, finishedWork.return);
3404 + break;
3405 + }
3406 + // Fallthrough
3407 + }
3408 default: {
3409 recursivelyTraverseReappearLayoutEffects(
3410 finishedRoot,
packages/react-reconciler/src/ReactFiberCompleteWork.js
+2 -2
@@ -30,7 +30,7 @@ import type {
30 } from './ReactFiberActivityComponent';
31 import type {
32 ViewTransitionProps,
33 - ViewTransitionInstance,
33 + ViewTransitionState,
34 } from './ReactFiberViewTransitionComponent';
35 import {isOffscreenManual} from './ReactFiberActivityComponent';
36 import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent';
@@ -965,7 +965,7 @@ function trackReappearingViewTransitions(workInProgress: Fiber): void {
965 ) {
966 const props: ViewTransitionProps = child.memoizedProps;
967 if (props.name != null && props.name !== 'auto') {
968 - const instance: ViewTransitionInstance = child.stateNode;
968 + const instance: ViewTransitionState = child.stateNode;
969 trackAppearingViewTransition(instance, props.name);
970 }
971 }
packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js
+2
@@ -46,3 +46,5 @@ export const wasInstanceInViewport = shim;
46 export const hasInstanceChanged = shim;
47 export const hasInstanceAffectedParent = shim;
48 export const startViewTransition = shim;
49 +export type ViewTransitionInstance = null | {name: string, ...};
50 +export const createViewTransitionInstance = shim;
packages/react-reconciler/src/ReactFiberViewTransitionComponent.js
+6 -4
@@ -9,6 +9,7 @@
9
10 import type {ReactNodeList} from 'shared/ReactTypes';
11 import type {FiberRoot} from './ReactInternalTypes';
12 +import type {ViewTransitionInstance} from './ReactFiberConfig';
13
14 import {getWorkInProgressRoot} from './ReactFiberWorkLoop';
15
@@ -22,16 +23,17 @@ export type ViewTransitionProps = {
23 children?: ReactNodeList,
24 };
25
25 -export type ViewTransitionInstance = {
26 +export type ViewTransitionState = {
27 autoName: null | string, // the view-transition-name to use when an explicit one is not specified
27 - paired: null | ViewTransitionInstance, // a temporary state during the commit phase if we have paired this with another instance
28 + paired: null | ViewTransitionState, // a temporary state during the commit phase if we have paired this with another instance
29 + ref: null | ViewTransitionInstance, // the current ref instance. This can change through the lifetime of the instance.
30 };
31
32 let globalClientIdCounter: number = 0;
33
34 export function assignViewTransitionAutoName(
35 props: ViewTransitionProps,
34 - instance: ViewTransitionInstance,
36 + instance: ViewTransitionState,
37 ): string {
38 if (instance.autoName !== null) {
39 return instance.autoName;
@@ -61,7 +63,7 @@ export function assignViewTransitionAutoName(
63
64 export function getViewTransitionName(
65 props: ViewTransitionProps,
64 - instance: ViewTransitionInstance,
66 + instance: ViewTransitionState,
67 ): string {
68 if (props.name != null && props.name !== 'auto') {
69 return props.name;
packages/react-reconciler/src/ReactFiberWorkLoop.js
+5 -5
@@ -23,7 +23,7 @@ import type {
23 import type {OffscreenInstance} from './ReactFiberActivityComponent';
24 import type {Resource} from './ReactFiberConfig';
25 import type {RootState} from './ReactFiberRoot';
26 -import type {ViewTransitionInstance} from './ReactFiberViewTransitionComponent';
26 +import type {ViewTransitionState} from './ReactFiberViewTransitionComponent';
27
28 import {
29 enableCreateEventHandleAPI,
@@ -431,7 +431,7 @@ let workInProgressRootRecoverableErrors: Array<CapturedValue<mixed>> | null =
431 // pairs in the snapshot phase.
432 let workInProgressAppearingViewTransitions: Map<
433 string,
434 - ViewTransitionInstance,
434 + ViewTransitionState,
435 > | null = null;
436
437 // Tracks when an update occurs during the render phase.
@@ -1377,7 +1377,7 @@ function commitRootWhenReady(
1377 finishedWork: Fiber,
1378 recoverableErrors: Array<CapturedValue<mixed>> | null,
1379 transitions: Array<Transition> | null,
1380 - appearingViewTransitions: Map<string, ViewTransitionInstance> | null,
1380 + appearingViewTransitions: Map<string, ViewTransitionState> | null,
1381 didIncludeRenderPhaseUpdate: boolean,
1382 lanes: Lanes,
1383 spawnedLane: Lane,
@@ -2270,7 +2270,7 @@ export function renderHasNotSuspendedYet(): boolean {
2270 }
2271
2272 export function trackAppearingViewTransition(
2273 - instance: ViewTransitionInstance,
2273 + instance: ViewTransitionState,
2274 name: string,
2275 ): void {
2276 if (workInProgressAppearingViewTransitions === null) {
@@ -3197,7 +3197,7 @@ function commitRoot(
3197 lanes: Lanes,
3198 recoverableErrors: null | Array<CapturedValue<mixed>>,
3199 transitions: Array<Transition> | null,
3200 - appearingViewTransitions: Map<string, ViewTransitionInstance> | null,
3200 + appearingViewTransitions: Map<string, ViewTransitionState> | null,
3201 didIncludeRenderPhaseUpdate: boolean,
3202 spawnedLane: Lane,
3203 updatedLanes: Lanes,
packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
+3
@@ -40,6 +40,7 @@ export opaque type NoTimeout = mixed;
40 export opaque type RendererInspectionConfig = mixed;
41 export opaque type TransitionStatus = mixed;
42 export opaque type FormInstance = mixed;
43 +export type ViewTransitionInstance = null | {name: string, ...};
44 export opaque type InstanceMeasurement = mixed;
45 export type EventResponder = any;
46
@@ -143,6 +144,8 @@ export const wasInstanceInViewport = $$$config.wasInstanceInViewport;
144 export const hasInstanceChanged = $$$config.hasInstanceChanged;
145 export const hasInstanceAffectedParent = $$$config.hasInstanceAffectedParent;
146 export const startViewTransition = $$$config.startViewTransition;
147 +export const createViewTransitionInstance =
148 + $$$config.createViewTransitionInstance;
149 export const clearContainer = $$$config.clearContainer;
150
151 // -------------------
packages/react-test-renderer/src/ReactFiberConfigTestHost.js
+8
@@ -373,6 +373,14 @@ export function startViewTransition(
373 return false;
374 }
375
376 +export type ViewTransitionInstance = null | {name: string, ...};
377 +
378 +export function createViewTransitionInstance(
379 + name: string,
380 +): ViewTransitionInstance {
381 + return null;
382 +}
383 +
384 export function getInstanceFromNode(mockNode: Object): Object | null {
385 const instance = nodeToInstanceMap.get(mockNode);
386 if (instance !== undefined) {