main
js 1,349 lines 48.1 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {ViewTransitionProps} from 'shared/ReactTypes';
11
12 import type {Fiber, FiberRoot} from './ReactInternalTypes';
13
14 import type {Instance, TextInstance, Props} from './ReactFiberConfig';
15
16 import type {OffscreenState} from './ReactFiberOffscreenComponent';
17
18 import type {ViewTransitionState} from './ReactFiberViewTransitionComponent';
19
20 import {
21 cloneMutableInstance,
22 cloneMutableTextInstance,
23 cloneRootViewTransitionContainer,
24 removeRootViewTransitionClone,
25 cancelRootViewTransitionName,
26 restoreRootViewTransitionName,
27 cancelViewTransitionName,
28 applyViewTransitionName,
29 appendChild,
30 commitUpdate,
31 commitTextUpdate,
32 resetTextContent,
33 supportsResources,
34 supportsSingletons,
35 unhideInstance,
36 unhideTextInstance,
37 } from './ReactFiberConfig';
38 import {
39 popMutationContext,
40 pushMutationContext,
41 viewTransitionMutationContext,
42 trackHostMutation,
43 } from './ReactFiberMutationTracking';
44 import {
45 MutationMask,
46 Placement,
47 Update,
48 ContentReset,
49 NoFlags,
50 Visibility,
51 ViewTransitionNamedStatic,
52 ViewTransitionStatic,
53 AffectedParentLayout,
54 } from './ReactFiberFlags';
55 import {
56 HasEffect as HookHasEffect,
57 Insertion as HookInsertion,
58 } from './ReactHookEffectTags';
59 import {
60 FunctionComponent,
61 ForwardRef,
62 MemoComponent,
63 SimpleMemoComponent,
64 HostComponent,
65 HostHoistable,
66 HostSingleton,
67 HostText,
68 HostPortal,
69 OffscreenComponent,
70 ViewTransitionComponent,
71 } from './ReactWorkTags';
72 import {
73 restoreEnterOrExitViewTransitions,
74 restoreNestedViewTransitions,
75 restoreUpdateViewTransitionForGesture,
76 appearingViewTransitions,
77 commitEnterViewTransitions,
78 commitParentExitViewTransitions,
79 measureNestedViewTransitions,
80 measureUpdateViewTransition,
81 viewTransitionCancelableChildren,
82 pushViewTransitionCancelableScope,
83 popViewTransitionCancelableScope,
84 } from './ReactFiberCommitViewTransitions';
85 import {
86 commitHookEffectListMount,
87 commitHookEffectListUnmount,
88 } from './ReactFiberCommitEffects';
89 import {
90 getViewTransitionName,
91 getViewTransitionClassName,
92 } from './ReactFiberViewTransitionComponent';
93
94 import {
95 enableProfilerTimer,
96 enableComponentPerformanceTrack,
97 enableViewTransitionParentEnterExit,
98 } from 'shared/ReactFeatureFlags';
99 import {trackAnimatingTask} from './ReactProfilerTimer';
100 import {scheduleGestureTransitionEvent} from './ReactFiberWorkLoop';
101
102 // Used during the apply phase to track whether a parent ViewTransition component
103 // might have been affected by any mutations / relayouts below.
104 let viewTransitionContextChanged: boolean = false;
105
106 function detectMutationOrInsertClones(finishedWork: Fiber): boolean {
107 return true;
108 }
109
110 const CLONE_UPDATE = 0; // Mutations in this subtree or potentially affected by layout.
111 const CLONE_EXIT = 1; // Inside a reappearing offscreen before the next ViewTransition or HostComponent.
112 const CLONE_UNHIDE = 2; // Inside a reappearing offscreen before the next HostComponent.
113 const CLONE_APPEARING_PAIR = 3; // Like UNHIDE but we're already inside the first Host Component only finding pairs.
114 const CLONE_UNCHANGED = 4; // Nothing in this tree was changed but we're still walking to clone it.
115 const INSERT_EXIT = 5; // Inside a newly mounted tree before the next ViewTransition or HostComponent.
116 const INSERT_APPEND = 6; // Inside a newly mounted tree before the next HostComponent.
117 const INSERT_APPEARING_PAIR = 7; // Inside a newly mounted tree only finding pairs.
118 type VisitPhase = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
119
120 function applyViewTransitionToClones(
121 name: string,
122 className: ?string,
123 clones: Array<Instance>,
124 fiber: Fiber,
125 ): void {
126 // This gets called when we have found a pair, but after the clone in created. The clone is
127 // created by the insertion side. If the insertion side if found before the deletion side
128 // then this is called by the deletion. If the deletion is visited first then this is called
129 // later by the insertion when the clone has been created.
130 for (let i = 0; i < clones.length; i++) {
131 applyViewTransitionName(
132 clones[i],
133 i === 0
134 ? name
135 : // If we have multiple Host Instances below, we add a suffix to the name to give
136 // each one a unique name.
137 name + '_' + i,
138 className,
139 );
140 }
141 if (enableProfilerTimer && enableComponentPerformanceTrack) {
142 if (fiber._debugTask != null) {
143 trackAnimatingTask(fiber._debugTask);
144 }
145 }
146 }
147
148 function trackDeletedPairViewTransitions(deletion: Fiber): void {
149 if (
150 appearingViewTransitions === null ||
151 appearingViewTransitions.size === 0
152 ) {
153 // We've found all.
154 return;
155 }
156 const pairs = appearingViewTransitions;
157 if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
158 // This has no named view transitions in its subtree.
159 return;
160 }
161 let child = deletion.child;
162 while (child !== null) {
163 if (child.tag === OffscreenComponent && child.memoizedState !== null) {
164 // This tree was already hidden so we skip it.
165 } else {
166 if (
167 child.tag === ViewTransitionComponent &&
168 (child.flags & ViewTransitionNamedStatic) !== NoFlags
169 ) {
170 const props: ViewTransitionProps = child.memoizedProps;
171 const name = props.name;
172 if (name != null && name !== 'auto') {
173 const pair = pairs.get(name);
174 if (pair !== undefined) {
175 // Delete the entry so that we know when we've found all of them
176 // and can stop searching (size reaches zero).
177 pairs.delete(name);
178 const className: ?string = getViewTransitionClassName(
179 props.default,
180 props.share,
181 );
182 if (className !== 'none') {
183 // TODO: Since the deleted instance already has layout we could
184 // check if it's in the viewport and if not skip the pairing.
185 // It would currently cause layout thrash though so if we did that
186 // we need to avoid inserting the root of the cloned trees until
187 // the end.
188
189 // The "old" instance is actually the one we're inserting.
190 const oldInstance: ViewTransitionState = pair;
191 // The "new" instance is the already mounted one we're deleting.
192 const newInstance: ViewTransitionState = child.stateNode;
193 oldInstance.paired = newInstance;
194 newInstance.paired = oldInstance;
195 const clones = oldInstance.clones;
196 if (clones !== null) {
197 // If we have clones that means that we've already visited this
198 // ViewTransition boundary before and we can now apply the name
199 // to those clones. Otherwise, we have to wait until we clone it.
200 applyViewTransitionToClones(name, className, clones, child);
201 }
202 }
203 if (pairs.size === 0) {
204 break;
205 }
206 }
207 }
208 }
209 trackDeletedPairViewTransitions(child);
210 }
211 child = child.sibling;
212 }
213 }
214
215 function trackEnterViewTransitions(deletion: Fiber): void {
216 if (deletion.tag === ViewTransitionComponent) {
217 const props: ViewTransitionProps = deletion.memoizedProps;
218 const name = getViewTransitionName(props, deletion.stateNode);
219 const pair =
220 appearingViewTransitions !== null
221 ? appearingViewTransitions.get(name)
222 : undefined;
223 const className: ?string = getViewTransitionClassName(
224 props.default,
225 pair !== undefined ? props.share : props.enter,
226 );
227 if (className !== 'none') {
228 if (pair !== undefined) {
229 // TODO: Since the deleted instance already has layout we could
230 // check if it's in the viewport and if not skip the pairing.
231 // It would currently cause layout thrash though so if we did that
232 // we need to avoid inserting the root of the cloned trees until
233 // the end.
234
235 // Delete the entry so that we know when we've found all of them
236 // and can stop searching (size reaches zero).
237 // $FlowFixMe[incompatible-use]: Refined by the pair.
238 appearingViewTransitions.delete(name);
239 // The "old" instance is actually the one we're inserting.
240 const oldInstance: ViewTransitionState = pair;
241 // The "new" instance is the already mounted one we're deleting.
242 const newInstance: ViewTransitionState = deletion.stateNode;
243 oldInstance.paired = newInstance;
244 newInstance.paired = oldInstance;
245 const clones = oldInstance.clones;
246 if (clones !== null) {
247 // If we have clones that means that we've already visited this
248 // ViewTransition boundary before and we can now apply the name
249 // to those clones. Otherwise, we have to wait until we clone it.
250 applyViewTransitionToClones(name, className, clones, deletion);
251 }
252 }
253 }
254 // Look for more pairs deeper in the tree.
255 trackDeletedPairViewTransitions(deletion);
256 } else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
257 let child = deletion.child;
258 while (child !== null) {
259 trackEnterViewTransitions(child);
260 child = child.sibling;
261 }
262 } else {
263 trackDeletedPairViewTransitions(deletion);
264 }
265 }
266
267 function applyAppearingPairViewTransition(child: Fiber): void {
268 // Normally these helpers do recursive calls but since insertion/offscreen is forked
269 // we call this helper from those loops instead. This must be called only on
270 // ViewTransitionComponent that has already had their clones filled.
271 if ((child.flags & ViewTransitionNamedStatic) !== NoFlags) {
272 const state: ViewTransitionState = child.stateNode;
273 // If this is not yet paired, it doesn't mean that we won't pair it later when
274 // we find the deletion side. If that's the case then we'll add the names to
275 // the clones then.
276 if (state.paired) {
277 const props: ViewTransitionProps = child.memoizedProps;
278 if (props.name == null || props.name === 'auto') {
279 throw new Error(
280 'Found a pair with an auto name. This is a bug in React.',
281 );
282 }
283 const name = props.name;
284 // Note that this class name that doesn't actually really matter because the
285 // "new" side will be the one that wins in practice.
286 const className: ?string = getViewTransitionClassName(
287 props.default,
288 props.share,
289 );
290 if (className !== 'none') {
291 const clones = state.clones;
292 // If there are no clones at this point, that should mean that there are no
293 // HostComponent children in this ViewTransition.
294 if (clones !== null) {
295 applyViewTransitionToClones(name, className, clones, child);
296 }
297 scheduleGestureTransitionEvent(child, props.onGestureShare);
298 }
299 }
300 }
301 }
302
303 function applyExitViewTransition(placement: Fiber): void {
304 // Normally these helpers do recursive calls but since insertion/offscreen is forked
305 // we call this helper from those loops instead. This must be called only on
306 // ViewTransitionComponent that has already had their clones filled.
307 const state: ViewTransitionState = placement.stateNode;
308 const props: ViewTransitionProps = placement.memoizedProps;
309 const name = getViewTransitionName(props, state);
310 const className: ?string = getViewTransitionClassName(
311 props.default,
312 // Note that just because we don't have a pair yet doesn't mean we won't find one
313 // later. However, that doesn't matter because if we do the class name that wins
314 // is the one applied by the "new" side anyway.
315 state.paired ? props.share : props.exit,
316 );
317 if (className !== 'none') {
318 // TODO: Ideally we could determine if this exit is in the viewport and
319 // exclude it otherwise but that would require waiting until we insert
320 // and layout the clones first. Currently wait until the view transition
321 // starts before reading the layout.
322 const clones = state.clones;
323 // If there are no clones at this point, that should mean that there are no
324 // HostComponent children in this ViewTransition.
325 if (clones !== null) {
326 applyViewTransitionToClones(name, className, clones, placement);
327 }
328 if (state.paired) {
329 scheduleGestureTransitionEvent(placement, props.onGestureShare);
330 } else {
331 scheduleGestureTransitionEvent(placement, props.onGestureExit);
332 if (enableViewTransitionParentEnterExit) {
333 commitParentExitViewTransitions(placement, true);
334 }
335 }
336 }
337 }
338
339 function applyNestedViewTransition(child: Fiber): void {
340 const state: ViewTransitionState = child.stateNode;
341 const props: ViewTransitionProps = child.memoizedProps;
342 const name = getViewTransitionName(props, state);
343 const className: ?string = getViewTransitionClassName(
344 props.default,
345 props.update,
346 );
347 if (className !== 'none') {
348 const clones = state.clones;
349 // If there are no clones at this point, that should mean that there are no
350 // HostComponent children in this ViewTransition.
351 if (clones !== null) {
352 applyViewTransitionToClones(name, className, clones, child);
353 }
354 }
355 }
356
357 function applyUpdateViewTransition(current: Fiber, finishedWork: Fiber): void {
358 const state: ViewTransitionState = finishedWork.stateNode;
359 // Updates can have conflicting names and classNames.
360 // Since we're doing a reverse animation the "new" state is actually the current
361 // and the "old" state is the finishedWork.
362 const newProps: ViewTransitionProps = current.memoizedProps;
363 const oldProps: ViewTransitionProps = finishedWork.memoizedProps;
364 const oldName = getViewTransitionName(oldProps, state);
365 // This className applies only if there are fewer child DOM nodes than
366 // before or if this update should've been cancelled but we ended up with
367 // a parent animating so we need to animate the child too. Otherwise
368 // the "new" state wins. Since "new" normally wins, that's usually what
369 // we would use. However, since this animation is going in reverse we actually
370 // want the props from "current" since that's the class that would've won if
371 // it was the normal direction. To preserve the same effect in either direction.
372 const className: ?string = getViewTransitionClassName(
373 newProps.default,
374 newProps.update,
375 );
376 if (className === 'none') {
377 // If update is "none" then we don't have to apply a name. Since we won't animate this boundary.
378 return;
379 }
380 const clones = state.clones;
381 // If there are no clones at this point, that should mean that there are no
382 // HostComponent children in this ViewTransition.
383 if (clones !== null) {
384 applyViewTransitionToClones(oldName, className, clones, finishedWork);
385 }
386 }
387
388 function recursivelyInsertNew(
389 parentFiber: Fiber,
390 hostParentClone: Instance,
391 parentViewTransition: null | ViewTransitionState,
392 visitPhase: VisitPhase,
393 ): void {
394 if (
395 visitPhase === INSERT_APPEARING_PAIR &&
396 parentViewTransition === null &&
397 (parentFiber.subtreeFlags & (ViewTransitionNamedStatic | Placement)) ===
398 NoFlags
399 ) {
400 // We're just searching for pairs or insertion effects but we have reached the end.
401 return;
402 }
403 let child = parentFiber.child;
404 while (child !== null) {
405 recursivelyInsertNewFiber(
406 child,
407 hostParentClone,
408 parentViewTransition,
409 visitPhase,
410 );
411 child = child.sibling;
412 }
413 }
414
415 function recursivelyInsertNewFiber(
416 finishedWork: Fiber,
417 hostParentClone: Instance,
418 parentViewTransition: null | ViewTransitionState,
419 visitPhase: VisitPhase,
420 ): void {
421 switch (finishedWork.tag) {
422 case FunctionComponent:
423 case ForwardRef:
424 case MemoComponent:
425 case SimpleMemoComponent: {
426 recursivelyInsertNew(
427 finishedWork,
428 hostParentClone,
429 parentViewTransition,
430 visitPhase,
431 );
432 if (finishedWork.flags & Update) {
433 // Insertion Effects are mounted temporarily during the rendering of the snapshot.
434 // This does not affect cloned Offscreen content since those would've been mounted
435 // while inside the offscreen tree already.
436 // Note that because we are mounting a clone of the DOM tree and the previous DOM
437 // tree remains mounted during the snapshot, we can't unmount any previous insertion
438 // effects. This can lead to conflicts but that is similar to what can happen with
439 // conflicts for two mounted Activity boundaries.
440 commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
441 }
442 break;
443 }
444 case HostHoistable: {
445 // $FlowFixMe[constant-condition]
446 if (supportsResources) {
447 // TODO: Hoistables should get optimistically inserted and then removed.
448 recursivelyInsertNew(
449 finishedWork,
450 hostParentClone,
451 parentViewTransition,
452 visitPhase,
453 );
454 break;
455 }
456 // Fall through
457 }
458 case HostSingleton: {
459 // $FlowFixMe[constant-condition]
460 if (supportsSingletons) {
461 recursivelyInsertNew(
462 finishedWork,
463 hostParentClone,
464 parentViewTransition,
465 visitPhase,
466 );
467
468 if (__DEV__) {
469 // We cannot apply mutations to Host Singletons since by definition
470 // they cannot be cloned. Therefore we warn in DEV if this commit
471 // had any effect.
472 if (finishedWork.flags & Update) {
473 console.error(
474 'startGestureTransition() caused something to render a new <%s>. ' +
475 'This is not possible in the current implementation. ' +
476 "Make sure that the swipe doesn't mount any new <%s> elements.",
477 finishedWork.type,
478 finishedWork.type,
479 );
480 }
481 }
482 break;
483 }
484 // Fall through
485 }
486 case HostComponent: {
487 const instance: Instance = finishedWork.stateNode;
488 // For insertions we don't need to clone. It's already new state node.
489 if (visitPhase !== INSERT_APPEARING_PAIR) {
490 appendChild(hostParentClone, instance);
491 trackHostMutation();
492 recursivelyInsertNew(
493 finishedWork,
494 instance,
495 null,
496 INSERT_APPEARING_PAIR,
497 );
498 } else {
499 recursivelyInsertNew(finishedWork, instance, null, visitPhase);
500 }
501 if (parentViewTransition !== null) {
502 if (parentViewTransition.clones === null) {
503 parentViewTransition.clones = [instance];
504 } else {
505 parentViewTransition.clones.push(instance);
506 }
507 }
508 break;
509 }
510 case HostText: {
511 const textInstance: TextInstance = finishedWork.stateNode;
512 // $FlowFixMe[invalid-compare]
513 if (textInstance === null) {
514 throw new Error(
515 'This should have a text node initialized. This error is likely ' +
516 'caused by a bug in React. Please file an issue.',
517 );
518 }
519 // For insertions we don't need to clone. It's already new state node.
520 if (visitPhase !== INSERT_APPEARING_PAIR) {
521 appendChild(hostParentClone, textInstance);
522 trackHostMutation();
523 }
524 break;
525 }
526 case HostPortal: {
527 // TODO: Consider what should happen to Portals. For now we exclude them.
528 break;
529 }
530 case OffscreenComponent: {
531 const newState: OffscreenState | null = finishedWork.memoizedState;
532 const isHidden = newState !== null;
533 if (!isHidden) {
534 // Only insert nodes if this tree is going to be visible. No need to
535 // insert invisible content.
536 // Since there was no mutation to this node, it couldn't have changed
537 // visibility so we don't need to update visitPhase here.
538 recursivelyInsertNew(
539 finishedWork,
540 hostParentClone,
541 parentViewTransition,
542 visitPhase,
543 );
544 }
545 break;
546 }
547 case ViewTransitionComponent:
548 const prevMutationContext = pushMutationContext();
549 const viewTransitionState: ViewTransitionState = finishedWork.stateNode;
550 // TODO: If this was already cloned by a previous pass we can reuse those clones.
551 viewTransitionState.clones = null;
552 let nextPhase: VisitPhase;
553 if (visitPhase === INSERT_EXIT) {
554 // This was an Enter of a ViewTransition. We now move onto inserting the inner
555 // HostComponents and finding inner pairs.
556 nextPhase = INSERT_APPEND;
557 } else {
558 nextPhase = visitPhase;
559 }
560 recursivelyInsertNew(
561 finishedWork,
562 hostParentClone,
563 viewTransitionState,
564 nextPhase,
565 );
566 // After we've inserted the new nodes into the "clones" set we can apply share
567 // or exit transitions to them.
568 if (visitPhase === INSERT_EXIT) {
569 applyExitViewTransition(finishedWork);
570 } else if (
571 visitPhase === INSERT_APPEARING_PAIR ||
572 visitPhase === INSERT_APPEND
573 ) {
574 applyAppearingPairViewTransition(finishedWork);
575 }
576 popMutationContext(prevMutationContext);
577 break;
578 default: {
579 recursivelyInsertNew(
580 finishedWork,
581 hostParentClone,
582 parentViewTransition,
583 visitPhase,
584 );
585 break;
586 }
587 }
588 }
589
590 function recursivelyInsertClonesFromExistingTree(
591 parentFiber: Fiber,
592 hostParentClone: Instance,
593 parentViewTransition: null | ViewTransitionState,
594 visitPhase: VisitPhase,
595 ): void {
596 let child = parentFiber.child;
597 while (child !== null) {
598 switch (child.tag) {
599 case HostComponent: {
600 const instance: Instance = child.stateNode;
601 let nextPhase: VisitPhase;
602 switch (visitPhase) {
603 case CLONE_EXIT:
604 case CLONE_UNHIDE:
605 case CLONE_APPEARING_PAIR:
606 // If this was an unhide, we need to keep going if there are any named
607 // pairs in this subtree, since they might need to be marked.
608 nextPhase =
609 (child.subtreeFlags & ViewTransitionNamedStatic) !== NoFlags
610 ? CLONE_APPEARING_PAIR
611 : CLONE_UNCHANGED;
612 break;
613 default:
614 // We've found any "layout" View Transitions at this point so we can bail.
615 nextPhase = CLONE_UNCHANGED;
616 }
617 let clone: Instance;
618 if (nextPhase !== CLONE_UNCHANGED) {
619 // We might need a handle on these clones, so we need to do a shallow clone
620 // and keep going.
621 clone = cloneMutableInstance(instance, false);
622 recursivelyInsertClonesFromExistingTree(
623 child,
624 clone,
625 null,
626 nextPhase,
627 );
628 } else {
629 // If we have no mutations in this subtree, and we don't need a handle on the
630 // clones, then we can do a deep clone instead and bailout.
631 clone = cloneMutableInstance(instance, true);
632 // TODO: We may need to transfer some DOM state such as scroll position
633 // for the deep clones.
634 // TODO: If there's a manual view-transition-name inside the clone we
635 // should ideally remove it from the original and then restore it in mutation
636 // phase. Otherwise it leads to duplicate names.
637 }
638 appendChild(hostParentClone, clone);
639 if (parentViewTransition !== null) {
640 if (parentViewTransition.clones === null) {
641 parentViewTransition.clones = [clone];
642 } else {
643 parentViewTransition.clones.push(clone);
644 }
645 }
646 if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {
647 unhideInstance(clone, child.memoizedProps);
648 trackHostMutation();
649 }
650 break;
651 }
652 case HostText: {
653 const textInstance: TextInstance = child.stateNode;
654 // $FlowFixMe[invalid-compare]
655 if (textInstance === null) {
656 throw new Error(
657 'This should have a text node initialized. This error is likely ' +
658 'caused by a bug in React. Please file an issue.',
659 );
660 }
661 const clone = cloneMutableTextInstance(textInstance);
662 appendChild(hostParentClone, clone);
663 if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {
664 unhideTextInstance(clone, child.memoizedProps);
665 trackHostMutation();
666 }
667 break;
668 }
669 case HostPortal: {
670 // TODO: Consider what should happen to Portals. For now we exclude them.
671 break;
672 }
673 case OffscreenComponent: {
674 const newState: OffscreenState | null = child.memoizedState;
675 const isHidden = newState !== null;
676 if (!isHidden) {
677 // Only insert clones if this tree is going to be visible. No need to
678 // clone invisible content.
679 // TODO: If this is visible but detached it should still be cloned.
680 // Since there was no mutation to this node, it couldn't have changed
681 // visibility so we don't need to update visitPhase here.
682 recursivelyInsertClonesFromExistingTree(
683 child,
684 hostParentClone,
685 parentViewTransition,
686 visitPhase,
687 );
688 }
689 break;
690 }
691 case ViewTransitionComponent:
692 const prevMutationContext = pushMutationContext();
693 const viewTransitionState: ViewTransitionState = child.stateNode;
694 // TODO: If this was already cloned by a previous pass we can reuse those clones.
695 viewTransitionState.clones = null;
696 // "Existing" view transitions are in subtrees that didn't update so
697 // this is a "current". We normally clear this upon rerendering
698 // but we use this flag to track changes from layout in the commit.
699 // So we need it to be cleared before we do that.
700 // TODO: Use some other temporary state to track this.
701 child.flags &= ~Update;
702 let nextPhase: VisitPhase;
703 if (visitPhase === CLONE_EXIT) {
704 // This was an Enter of a ViewTransition. We now move onto unhiding the inner
705 // HostComponents and finding inner pairs.
706 nextPhase = CLONE_UNHIDE;
707 // TODO: Mark the name and find a pair.
708 } else if (visitPhase === CLONE_UPDATE) {
709 // If the tree had no mutations and we've found the top most ViewTransition
710 // then this is the one we might apply the "layout" state too if it has changed
711 // position. After we've found its HostComponents we can bail out.
712 nextPhase = CLONE_UNCHANGED;
713 } else {
714 nextPhase = visitPhase;
715 }
716 recursivelyInsertClonesFromExistingTree(
717 child,
718 hostParentClone,
719 viewTransitionState,
720 nextPhase,
721 );
722 // After we've collected the cloned instances, we can apply exit or share transitions
723 // to them.
724 if (visitPhase === CLONE_EXIT) {
725 applyExitViewTransition(child);
726 } else if (
727 visitPhase === CLONE_APPEARING_PAIR ||
728 visitPhase === CLONE_UNHIDE
729 ) {
730 applyAppearingPairViewTransition(child);
731 } else if (visitPhase === CLONE_UPDATE) {
732 applyNestedViewTransition(child);
733 }
734 popMutationContext(prevMutationContext);
735 break;
736 default: {
737 recursivelyInsertClonesFromExistingTree(
738 child,
739 hostParentClone,
740 parentViewTransition,
741 visitPhase,
742 );
743 break;
744 }
745 }
746 child = child.sibling;
747 }
748 }
749
750 function recursivelyInsertClones(
751 parentFiber: Fiber,
752 hostParentClone: Instance,
753 parentViewTransition: null | ViewTransitionState,
754 visitPhase: VisitPhase,
755 ) {
756 const deletions = parentFiber.deletions;
757 if (deletions !== null) {
758 for (let i = 0; i < deletions.length; i++) {
759 const childToDelete = deletions[i];
760 trackEnterViewTransitions(childToDelete);
761 // Normally we would only mark something as triggering a mutation if there was
762 // actually a HostInstance below here. If this tree didn't contain a HostInstances
763 // we shouldn't trigger a mutation even though a virtual component was deleted.
764 trackHostMutation();
765 }
766 }
767
768 if (
769 parentFiber.alternate === null ||
770 (parentFiber.subtreeFlags & MutationMask) !== NoFlags
771 ) {
772 // If we have mutations or if this is a newly inserted tree, clone as we go.
773 let child = parentFiber.child;
774 while (child !== null) {
775 insertDestinationClonesOfFiber(
776 child,
777 hostParentClone,
778 parentViewTransition,
779 visitPhase,
780 );
781 child = child.sibling;
782 }
783 } else {
784 // Once we reach a subtree with no more mutations we can bail out.
785 // However, we must still insert deep clones of the HostComponents.
786 recursivelyInsertClonesFromExistingTree(
787 parentFiber,
788 hostParentClone,
789 parentViewTransition,
790 visitPhase,
791 );
792 }
793 }
794
795 function insertDestinationClonesOfFiber(
796 finishedWork: Fiber,
797 hostParentClone: Instance,
798 parentViewTransition: null | ViewTransitionState,
799 visitPhase: VisitPhase,
800 ) {
801 const current = finishedWork.alternate;
802 if (current === null) {
803 // This is a newly mounted subtree. Insert any HostComponents and trigger
804 // Enter transitions.
805 recursivelyInsertNewFiber(
806 finishedWork,
807 hostParentClone,
808 parentViewTransition,
809 INSERT_EXIT,
810 );
811 return;
812 }
813
814 const flags = finishedWork.flags;
815 // The effect flag should be checked *after* we refine the type of fiber,
816 // because the fiber tag is more specific. An exception is any flag related
817 // to reconciliation, because those can be set on all fiber types.
818 switch (finishedWork.tag) {
819 case HostHoistable: {
820 // $FlowFixMe[constant-condition]
821 if (supportsResources) {
822 // TODO: Hoistables should get optimistically inserted and then removed.
823 recursivelyInsertClones(
824 finishedWork,
825 hostParentClone,
826 parentViewTransition,
827 visitPhase,
828 );
829 break;
830 }
831 // Fall through
832 }
833 case HostSingleton: {
834 // $FlowFixMe[constant-condition]
835 if (supportsSingletons) {
836 recursivelyInsertClones(
837 finishedWork,
838 hostParentClone,
839 parentViewTransition,
840 visitPhase,
841 );
842 if (__DEV__) {
843 // We cannot apply mutations to Host Singletons since by definition
844 // they cannot be cloned. Therefore we warn in DEV if this commit
845 // had any effect.
846 if (flags & Update) {
847 const newProps = finishedWork.memoizedProps;
848 const oldProps = current.memoizedProps;
849 const instance = finishedWork.stateNode;
850 const type = finishedWork.type;
851 const prev = pushMutationContext();
852
853 try {
854 // Since we currently don't have a separate diffing algorithm for
855 // individual properties, the Update flag can be a false positive.
856 // We have to apply the new props first o detect any mutations and
857 // then revert them.
858 commitUpdate(instance, type, oldProps, newProps, finishedWork);
859 if (viewTransitionMutationContext) {
860 console.error(
861 'startGestureTransition() caused something to mutate <%s>. ' +
862 'This is not possible in the current implementation. ' +
863 "Make sure that the swipe doesn't update any state which " +
864 'causes <%s> to change.',
865 finishedWork.type,
866 finishedWork.type,
867 );
868 }
869 // Revert
870 commitUpdate(instance, type, newProps, oldProps, finishedWork);
871 } finally {
872 popMutationContext(prev);
873 }
874 }
875 }
876 break;
877 }
878 // Fall through
879 }
880 case HostComponent: {
881 const instance: Instance = finishedWork.stateNode;
882 let clone: Instance;
883 if (finishedWork.child === null) {
884 // This node is terminal. We still do a deep clone in case this has user
885 // inserted content, text content or dangerouslySetInnerHTML.
886 clone = cloneMutableInstance(instance, true);
887 if (finishedWork.flags & ContentReset) {
888 resetTextContent(clone);
889 trackHostMutation();
890 }
891 } else {
892 // If we have children we'll clone them as we walk the tree so we just
893 // do a shallow clone here.
894 clone = cloneMutableInstance(instance, false);
895 }
896
897 if (flags & Update) {
898 const newProps = finishedWork.memoizedProps;
899 const oldProps = current.memoizedProps;
900 const type = finishedWork.type;
901 // Apply the delta to the clone.
902 commitUpdate(clone, type, oldProps, newProps, finishedWork);
903 }
904
905 if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {
906 appendChild(hostParentClone, clone);
907 unhideInstance(clone, finishedWork.memoizedProps);
908 recursivelyInsertClones(
909 finishedWork,
910 clone,
911 null,
912 CLONE_APPEARING_PAIR,
913 );
914 trackHostMutation();
915 } else {
916 appendChild(hostParentClone, clone);
917 recursivelyInsertClones(finishedWork, clone, null, visitPhase);
918 }
919 if (parentViewTransition !== null) {
920 if (parentViewTransition.clones === null) {
921 parentViewTransition.clones = [clone];
922 } else {
923 parentViewTransition.clones.push(clone);
924 }
925 }
926 break;
927 }
928 case HostText: {
929 const textInstance: TextInstance = finishedWork.stateNode;
930 // $FlowFixMe[invalid-compare]
931 if (textInstance === null) {
932 throw new Error(
933 'This should have a text node initialized. This error is likely ' +
934 'caused by a bug in React. Please file an issue.',
935 );
936 }
937 const clone = cloneMutableTextInstance(textInstance);
938 if (flags & Update) {
939 const newText: string = finishedWork.memoizedProps;
940 const oldText: string = current.memoizedProps;
941 commitTextUpdate(clone, newText, oldText);
942 trackHostMutation();
943 }
944 appendChild(hostParentClone, clone);
945 if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {
946 unhideTextInstance(clone, finishedWork.memoizedProps);
947 trackHostMutation();
948 }
949 break;
950 }
951 case HostPortal: {
952 // TODO: Consider what should happen to Portals. For now we exclude them.
953 break;
954 }
955 case OffscreenComponent: {
956 const newState: OffscreenState | null = finishedWork.memoizedState;
957 const isHidden = newState !== null;
958 if (!isHidden) {
959 // Only insert clones if this tree is going to be visible. No need to
960 // clone invisible content.
961 // TODO: If this is visible but detached it should still be cloned.
962 let nextPhase: VisitPhase;
963 if (visitPhase === CLONE_UPDATE && (flags & Visibility) !== NoFlags) {
964 // This is the root of an appear. We need to trigger Enter transitions.
965 nextPhase = CLONE_EXIT;
966 } else {
967 nextPhase = visitPhase;
968 }
969 recursivelyInsertClones(
970 finishedWork,
971 hostParentClone,
972 parentViewTransition,
973 nextPhase,
974 );
975 // $FlowFixMe[invalid-compare]
976 } else if (current !== null && current.memoizedState === null) {
977 // Was previously mounted as visible but is now hidden.
978 trackEnterViewTransitions(current);
979 // Normally we would only mark something as triggering a mutation if there was
980 // actually a HostInstance below here. If this tree didn't contain a HostInstances
981 // we shouldn't trigger a mutation even though a virtual component was hidden.
982 trackHostMutation();
983 }
984 break;
985 }
986 case ViewTransitionComponent:
987 const prevMutationContext = pushMutationContext();
988 const viewTransitionState: ViewTransitionState = finishedWork.stateNode;
989 // TODO: If this was already cloned by a previous pass we can reuse those clones.
990 viewTransitionState.clones = null;
991 let nextPhase: VisitPhase;
992 if (visitPhase === CLONE_EXIT) {
993 // This was an Enter of a ViewTransition. We now move onto unhiding the inner
994 // HostComponents and finding inner pairs.
995 nextPhase = CLONE_UNHIDE;
996 // TODO: Mark the name and find a pair.
997 } else {
998 nextPhase = visitPhase;
999 }
1000 recursivelyInsertClones(
1001 finishedWork,
1002 hostParentClone,
1003 viewTransitionState,
1004 nextPhase,
1005 );
1006 if (viewTransitionMutationContext) {
1007 // Track that this boundary had a mutation and therefore needs to animate
1008 // whether it resized or not.
1009 finishedWork.flags |= Update;
1010 }
1011 // After we've collected the cloned instances, we can apply exit or share transitions
1012 // to them.
1013 if (visitPhase === CLONE_EXIT) {
1014 applyExitViewTransition(finishedWork);
1015 } else if (
1016 visitPhase === CLONE_APPEARING_PAIR ||
1017 visitPhase === CLONE_UNHIDE
1018 ) {
1019 applyAppearingPairViewTransition(finishedWork);
1020 } else if (visitPhase === CLONE_UPDATE) {
1021 applyUpdateViewTransition(current, finishedWork);
1022 }
1023 popMutationContext(prevMutationContext);
1024 break;
1025 default: {
1026 recursivelyInsertClones(
1027 finishedWork,
1028 hostParentClone,
1029 parentViewTransition,
1030 visitPhase,
1031 );
1032 break;
1033 }
1034 }
1035 }
1036
1037 // Clone View Transition boundaries that have any mutations or might have had their
1038 // layout affected by child insertions.
1039 export function insertDestinationClones(
1040 root: FiberRoot,
1041 finishedWork: Fiber,
1042 ): void {
1043 // We'll either not transition the root, or we'll transition the clone. Regardless
1044 // we cancel the root view transition name.
1045 const needsClone = detectMutationOrInsertClones(finishedWork);
1046 if (needsClone) {
1047 // Clone the whole root
1048 const rootClone = cloneRootViewTransitionContainer(root.containerInfo);
1049 root.gestureClone = rootClone;
1050 recursivelyInsertClones(finishedWork, rootClone, null, CLONE_UPDATE);
1051 } else {
1052 root.gestureClone = null;
1053 cancelRootViewTransitionName(root.containerInfo);
1054 }
1055 }
1056
1057 function measureExitViewTransitions(placement: Fiber): void {
1058 if (placement.tag === ViewTransitionComponent) {
1059 // const state: ViewTransitionState = placement.stateNode;
1060 const props: ViewTransitionProps = placement.memoizedProps;
1061 const name = props.name;
1062 if (name != null && name !== 'auto') {
1063 // TODO: Find a pair
1064 }
1065 } else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
1066 // TODO: Check if this is a hidden Offscreen or a Portal.
1067 let child = placement.child;
1068 while (child !== null) {
1069 measureExitViewTransitions(child);
1070 child = child.sibling;
1071 }
1072 } else {
1073 // We don't need to find pairs here because we would've already found and
1074 // measured the pairs inside the deletion phase.
1075 }
1076 }
1077
1078 function recursivelyRestoreNew(
1079 finishedWork: Fiber,
1080 nearestMountedAncestor: Fiber,
1081 ): void {
1082 // There has to be move a Placement AND an Update flag somewhere below for this
1083 // pass to be relevant since we only apply insertion effects for new components here.
1084 if (((Placement | Update) & finishedWork.subtreeFlags) !== NoFlags) {
1085 let child = finishedWork.child;
1086 while (child !== null) {
1087 recursivelyRestoreNew(child, nearestMountedAncestor);
1088 child = child.sibling;
1089 }
1090 }
1091 switch (finishedWork.tag) {
1092 case FunctionComponent:
1093 case ForwardRef:
1094 case MemoComponent:
1095 case SimpleMemoComponent: {
1096 const current = finishedWork.alternate;
1097 if (current === null && finishedWork.flags & Update) {
1098 // Insertion Effects are mounted temporarily during the rendering of the snapshot.
1099 // We have now already takes a snapshot of the inserted state so we can now unmount
1100 // them to get back into the original state before starting the animation.
1101 commitHookEffectListUnmount(
1102 HookInsertion | HookHasEffect,
1103 finishedWork,
1104 nearestMountedAncestor,
1105 );
1106 }
1107 break;
1108 }
1109 }
1110 }
1111
1112 function recursivelyApplyViewTransitions(parentFiber: Fiber) {
1113 const deletions = parentFiber.deletions;
1114 if (deletions !== null) {
1115 for (let i = 0; i < deletions.length; i++) {
1116 const childToDelete = deletions[i];
1117 commitEnterViewTransitions(childToDelete, true);
1118 }
1119 }
1120
1121 if (
1122 parentFiber.alternate === null ||
1123 (parentFiber.subtreeFlags & MutationMask) !== NoFlags
1124 ) {
1125 // If we have mutations or if this is a newly inserted tree, clone as we go.
1126 let child = parentFiber.child;
1127 while (child !== null) {
1128 const current = child.alternate;
1129 if (current === null) {
1130 measureExitViewTransitions(child);
1131 recursivelyRestoreNew(child, parentFiber);
1132 } else {
1133 applyViewTransitionsOnFiber(child, current);
1134 }
1135 child = child.sibling;
1136 }
1137 } else {
1138 // Nothing has changed in this subtree, but the parent may have still affected
1139 // its size and position. We need to measure the old and new state to see if
1140 // we should animate its size and position.
1141 measureNestedViewTransitions(parentFiber, true);
1142 }
1143 }
1144
1145 function applyViewTransitionsOnFiber(finishedWork: Fiber, current: Fiber) {
1146 // The effect flag should be checked *after* we refine the type of fiber,
1147 // because the fiber tag is more specific. An exception is any flag related
1148 // to reconciliation, because those can be set on all fiber types.
1149 switch (finishedWork.tag) {
1150 case HostPortal: {
1151 // TODO: Consider what should happen to Portals. For now we exclude them.
1152 break;
1153 }
1154 case OffscreenComponent: {
1155 const newState: OffscreenState | null = finishedWork.memoizedState;
1156 const isHidden = newState !== null;
1157 const wasHidden = current.memoizedState !== null;
1158 if (!isHidden) {
1159 if (wasHidden) {
1160 measureExitViewTransitions(finishedWork);
1161 recursivelyRestoreNew(finishedWork, finishedWork);
1162 } else {
1163 recursivelyApplyViewTransitions(finishedWork);
1164 }
1165 } else {
1166 if (!wasHidden) {
1167 // Was previously mounted as visible but is now hidden.
1168 commitEnterViewTransitions(current, true);
1169 }
1170 }
1171 break;
1172 }
1173 case ViewTransitionComponent: {
1174 const prevContextChanged = viewTransitionContextChanged;
1175 const prevCancelableChildren = pushViewTransitionCancelableScope();
1176 viewTransitionContextChanged = false;
1177 recursivelyApplyViewTransitions(finishedWork);
1178
1179 if (viewTransitionContextChanged) {
1180 finishedWork.flags |= Update;
1181 }
1182
1183 const inViewport = measureUpdateViewTransition(
1184 current,
1185 finishedWork,
1186 true,
1187 );
1188
1189 if ((finishedWork.flags & Update) === NoFlags || !inViewport) {
1190 // If this boundary didn't update, then we may be able to cancel its children.
1191 // We bubble them up to the parent set to be determined later if we can cancel.
1192 // Similarly, if old and new state was outside the viewport, we can skip it
1193 // even if it did update.
1194 if (prevCancelableChildren === null) {
1195 // Bubbling up this whole set to the parent.
1196 } else {
1197 // Merge with parent set.
1198 // $FlowFixMe[method-unbinding]
1199 prevCancelableChildren.push.apply(
1200 prevCancelableChildren,
1201 viewTransitionCancelableChildren,
1202 );
1203 popViewTransitionCancelableScope(prevCancelableChildren);
1204 }
1205 // TODO: If this doesn't end up canceled, because a parent animates,
1206 // then we should probably issue an event since this instance is part of it.
1207 } else {
1208 const props: ViewTransitionProps = finishedWork.memoizedProps;
1209 scheduleGestureTransitionEvent(finishedWork, props.onGestureUpdate);
1210 // If this boundary did update, we cannot cancel its children so those are dropped.
1211 popViewTransitionCancelableScope(prevCancelableChildren);
1212 }
1213
1214 if ((finishedWork.flags & AffectedParentLayout) !== NoFlags) {
1215 // This boundary changed size in a way that may have caused its parent to
1216 // relayout. We need to bubble this information up to the parent.
1217 viewTransitionContextChanged = true;
1218 } else {
1219 // Otherwise, we restore it to whatever the parent had found so far.
1220 viewTransitionContextChanged = prevContextChanged;
1221 }
1222
1223 const viewTransitionState: ViewTransitionState = finishedWork.stateNode;
1224 viewTransitionState.clones = null; // Reset
1225 break;
1226 }
1227 default: {
1228 recursivelyApplyViewTransitions(finishedWork);
1229 break;
1230 }
1231 }
1232 }
1233
1234 // Revert insertions and apply view transition names to the "new" (current) state.
1235 export function applyDepartureTransitions(
1236 root: FiberRoot,
1237 finishedWork: Fiber,
1238 ): void {
1239 // First measure and apply view-transition-names to the "new" states.
1240 viewTransitionContextChanged = false;
1241 pushViewTransitionCancelableScope();
1242
1243 recursivelyApplyViewTransitions(finishedWork);
1244
1245 // Then remove the clones.
1246 const rootClone = root.gestureClone;
1247 if (rootClone !== null) {
1248 root.gestureClone = null;
1249 removeRootViewTransitionClone(root.containerInfo, rootClone);
1250 }
1251
1252 if (!viewTransitionContextChanged) {
1253 // If we didn't leak any resizing out to the root, we don't have to transition
1254 // the root itself. This means that we can now safely cancel any cancellations
1255 // that bubbled all the way up.
1256 const cancelableChildren = viewTransitionCancelableChildren;
1257 if (cancelableChildren !== null) {
1258 for (let i = 0; i < cancelableChildren.length; i += 3) {
1259 cancelViewTransitionName(
1260 cancelableChildren[i] as any as Instance,
1261 cancelableChildren[i + 1] as any as string,
1262 cancelableChildren[i + 2] as any as Props,
1263 );
1264 }
1265 }
1266 // We also cancel the root itself. First we restore the name to the documentElement
1267 // and then we cancel it.
1268 restoreRootViewTransitionName(root.containerInfo);
1269 cancelRootViewTransitionName(root.containerInfo);
1270 }
1271 popViewTransitionCancelableScope(null);
1272 }
1273
1274 function recursivelyRestoreViewTransitions(parentFiber: Fiber) {
1275 const deletions = parentFiber.deletions;
1276 if (deletions !== null) {
1277 for (let i = 0; i < deletions.length; i++) {
1278 const childToDelete = deletions[i];
1279 restoreEnterOrExitViewTransitions(childToDelete);
1280 }
1281 }
1282
1283 if (
1284 parentFiber.alternate === null ||
1285 (parentFiber.subtreeFlags & MutationMask) !== NoFlags
1286 ) {
1287 // If we have mutations or if this is a newly inserted tree, clone as we go.
1288 let child = parentFiber.child;
1289 while (child !== null) {
1290 restoreViewTransitionsOnFiber(child);
1291 child = child.sibling;
1292 }
1293 } else {
1294 // Nothing has changed in this subtree, but the parent may have still affected
1295 // its size and position. We need to measure the old and new state to see if
1296 // we should animate its size and position.
1297 restoreNestedViewTransitions(parentFiber);
1298 }
1299 }
1300
1301 function restoreViewTransitionsOnFiber(finishedWork: Fiber) {
1302 const current = finishedWork.alternate;
1303 if (current === null) {
1304 restoreEnterOrExitViewTransitions(finishedWork);
1305 return;
1306 }
1307
1308 const flags = finishedWork.flags;
1309 // The effect flag should be checked *after* we refine the type of fiber,
1310 // because the fiber tag is more specific. An exception is any flag related
1311 // to reconciliation, because those can be set on all fiber types.
1312 switch (finishedWork.tag) {
1313 case HostPortal: {
1314 // TODO: Consider what should happen to Portals. For now we exclude them.
1315 break;
1316 }
1317 case OffscreenComponent: {
1318 if (flags & Visibility) {
1319 const newState: OffscreenState | null = finishedWork.memoizedState;
1320 const isHidden = newState !== null;
1321 if (!isHidden) {
1322 restoreEnterOrExitViewTransitions(finishedWork);
1323 // $FlowFixMe[invalid-compare]
1324 } else if (current !== null && current.memoizedState === null) {
1325 // Was previously mounted as visible but is now hidden.
1326 restoreEnterOrExitViewTransitions(current);
1327 }
1328 }
1329 break;
1330 }
1331 case ViewTransitionComponent:
1332 restoreUpdateViewTransitionForGesture(current, finishedWork);
1333 recursivelyRestoreViewTransitions(finishedWork);
1334 break;
1335 default: {
1336 recursivelyRestoreViewTransitions(finishedWork);
1337 break;
1338 }
1339 }
1340 }
1341
1342 // Revert transition names and start/adjust animations on the started View Transition.
1343 export function startGestureAnimations(
1344 root: FiberRoot,
1345 finishedWork: Fiber,
1346 ): void {
1347 restoreViewTransitionsOnFiber(finishedWork);
1348 restoreRootViewTransitionName(root.containerInfo);
1349 }