main
js 1,122 lines 39.2 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 import type {Instance, InstanceMeasurement, Props} from './ReactFiberConfig';
12 import type {Fiber} from './ReactInternalTypes';
13 import type {ViewTransitionState} from './ReactFiberViewTransitionComponent';
14
15 import {
16 HostComponent,
17 OffscreenComponent,
18 ViewTransitionComponent,
19 } from './ReactWorkTags';
20 import {
21 NoFlags,
22 Update,
23 ViewTransitionStatic,
24 ViewTransitionStaticParent,
25 AffectedParentLayout,
26 ViewTransitionNamedStatic,
27 } from './ReactFiberFlags';
28 import {
29 supportsMutation,
30 applyViewTransitionName,
31 restoreViewTransitionName,
32 measureInstance,
33 measureClonedInstance,
34 hasInstanceChanged,
35 hasInstanceAffectedParent,
36 wasInstanceInViewport,
37 } from './ReactFiberConfig';
38 import {
39 scheduleViewTransitionEvent,
40 scheduleGestureTransitionEvent,
41 } from './ReactFiberWorkLoop';
42 import {
43 getViewTransitionName,
44 getViewTransitionClassName,
45 } from './ReactFiberViewTransitionComponent';
46 import {trackAnimatingTask} from './ReactProfilerTimer';
47 import {
48 enableComponentPerformanceTrack,
49 enableProfilerTimer,
50 enableViewTransitionForPersistenceMode,
51 enableViewTransitionParentEnterExit,
52 } from 'shared/ReactFeatureFlags';
53
54 export let shouldStartViewTransition: boolean = false;
55
56 export function resetShouldStartViewTransition(): void {
57 shouldStartViewTransition = false;
58 }
59
60 // This tracks named ViewTransition components found in the accumulateSuspenseyCommit
61 // phase that might need to find deleted pairs in the beforeMutation phase.
62 export let appearingViewTransitions: Map<string, ViewTransitionState> | null =
63 null;
64
65 export function resetAppearingViewTransitions(): void {
66 appearingViewTransitions = null;
67 }
68
69 export function trackAppearingViewTransition(
70 name: string,
71 state: ViewTransitionState,
72 ): void {
73 if (appearingViewTransitions === null) {
74 appearingViewTransitions = new Map();
75 }
76 appearingViewTransitions.set(name, state);
77 }
78
79 export function trackEnterViewTransitions(placement: Fiber): void {
80 if (
81 placement.tag === ViewTransitionComponent ||
82 (placement.subtreeFlags & ViewTransitionStatic) !== NoFlags
83 ) {
84 // If an inserted or appearing Fiber is a ViewTransition component or has one as
85 // an immediate child, then that will trigger as an "Enter" in future passes.
86 // We don't do anything else for that case in the "before mutation" phase but we
87 // still have to mark it as needing to call startViewTransition if nothing else
88 // updates.
89 shouldStartViewTransition = true;
90 }
91 }
92
93 // We can't cancel view transition children until we know that their parent also
94 // don't need to transition.
95 export let viewTransitionCancelableChildren: null | Array<
96 Instance | string | Props,
97 > = null; // tupled array where each entry is [instance: Instance, oldName: string, props: Props]
98
99 export function pushViewTransitionCancelableScope(): null | Array<
100 Instance | string | Props,
101 > {
102 const prevChildren = viewTransitionCancelableChildren;
103 viewTransitionCancelableChildren = null;
104 return prevChildren;
105 }
106
107 export function popViewTransitionCancelableScope(
108 prevChildren: null | Array<Instance | string | Props>,
109 ): void {
110 viewTransitionCancelableChildren = prevChildren;
111 }
112
113 let viewTransitionHostInstanceIdx = 0;
114
115 function applyViewTransitionToHostInstances(
116 fiber: Fiber,
117 name: string,
118 className: ?string,
119 collectMeasurements: null | Array<InstanceMeasurement>,
120 stopAtNestedViewTransitions: boolean,
121 ): boolean {
122 viewTransitionHostInstanceIdx = 0;
123 const inViewport = applyViewTransitionToHostInstancesRecursive(
124 fiber.child,
125 name,
126 className,
127 collectMeasurements,
128 stopAtNestedViewTransitions,
129 );
130 if (enableProfilerTimer && enableComponentPerformanceTrack && inViewport) {
131 if (fiber._debugTask != null) {
132 trackAnimatingTask(fiber._debugTask);
133 }
134 }
135 return inViewport;
136 }
137
138 function applyViewTransitionToHostInstancesRecursive(
139 child: null | Fiber,
140 name: string,
141 className: ?string,
142 collectMeasurements: null | Array<InstanceMeasurement>,
143 stopAtNestedViewTransitions: boolean,
144 ): boolean {
145 // $FlowFixMe[constant-condition]
146 if (!supportsMutation) {
147 if (enableViewTransitionForPersistenceMode) {
148 while (child !== null) {
149 if (child.tag === HostComponent) {
150 const instance: Instance = child.stateNode;
151 // TODO: calculate whether component is in viewport
152 shouldStartViewTransition = true;
153 applyViewTransitionName(
154 instance,
155 viewTransitionHostInstanceIdx === 0
156 ? name
157 : name + '_' + viewTransitionHostInstanceIdx,
158 className,
159 );
160 viewTransitionHostInstanceIdx++;
161 } else if (
162 child.tag === OffscreenComponent &&
163 child.memoizedState !== null
164 ) {
165 // Skip any hidden subtrees. They were or are effectively not there.
166 } else if (
167 child.tag === ViewTransitionComponent &&
168 stopAtNestedViewTransitions
169 ) {
170 // Skip any nested view transitions for updates since in that case the
171 // inner most one is the one that handles the update.
172 } else {
173 applyViewTransitionToHostInstancesRecursive(
174 child.child,
175 name,
176 className,
177 collectMeasurements,
178 stopAtNestedViewTransitions,
179 );
180 }
181 child = child.sibling;
182 }
183 return true;
184 } else {
185 return false;
186 }
187 }
188 let inViewport = false;
189 while (child !== null) {
190 if (child.tag === HostComponent) {
191 const instance: Instance = child.stateNode;
192 if (collectMeasurements !== null) {
193 const measurement = measureInstance(instance);
194 collectMeasurements.push(measurement);
195 if (wasInstanceInViewport(measurement)) {
196 inViewport = true;
197 }
198 } else if (!inViewport) {
199 if (wasInstanceInViewport(measureInstance(instance))) {
200 inViewport = true;
201 }
202 }
203 shouldStartViewTransition = true;
204 applyViewTransitionName(
205 instance,
206 viewTransitionHostInstanceIdx === 0
207 ? name
208 : // If we have multiple Host Instances below, we add a suffix to the name to give
209 // each one a unique name.
210 name + '_' + viewTransitionHostInstanceIdx,
211 className,
212 );
213 viewTransitionHostInstanceIdx++;
214 } else if (
215 child.tag === OffscreenComponent &&
216 child.memoizedState !== null
217 ) {
218 // Skip any hidden subtrees. They were or are effectively not there.
219 } else if (
220 child.tag === ViewTransitionComponent &&
221 stopAtNestedViewTransitions
222 ) {
223 // Skip any nested view transitions for updates since in that case the
224 // inner most one is the one that handles the update.
225 } else {
226 if (
227 applyViewTransitionToHostInstancesRecursive(
228 child.child,
229 name,
230 className,
231 collectMeasurements,
232 stopAtNestedViewTransitions,
233 )
234 ) {
235 inViewport = true;
236 }
237 }
238 child = child.sibling;
239 }
240 return inViewport;
241 }
242
243 function restoreViewTransitionOnHostInstances(
244 child: null | Fiber,
245 stopAtNestedViewTransitions: boolean,
246 ): void {
247 // $FlowFixMe[constant-condition]
248 if (!supportsMutation) {
249 return;
250 }
251 while (child !== null) {
252 if (child.tag === HostComponent) {
253 const instance: Instance = child.stateNode;
254 restoreViewTransitionName(instance, child.memoizedProps);
255 } else if (
256 child.tag === OffscreenComponent &&
257 child.memoizedState !== null
258 ) {
259 // Skip any hidden subtrees. They were or are effectively not there.
260 } else if (
261 child.tag === ViewTransitionComponent &&
262 stopAtNestedViewTransitions
263 ) {
264 // Skip any nested view transitions for updates since in that case the
265 // inner most one is the one that handles the update.
266 } else {
267 restoreViewTransitionOnHostInstances(
268 child.child,
269 stopAtNestedViewTransitions,
270 );
271 }
272 child = child.sibling;
273 }
274 }
275
276 function commitAppearingPairViewTransitions(placement: Fiber): void {
277 if ((placement.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
278 // This has no named view transitions in its subtree.
279 return;
280 }
281 let child = placement.child;
282 while (child !== null) {
283 if (child.tag === OffscreenComponent && child.memoizedState !== null) {
284 // This tree was already hidden so we skip it.
285 } else {
286 commitAppearingPairViewTransitions(child);
287 if (
288 child.tag === ViewTransitionComponent &&
289 (child.flags & ViewTransitionNamedStatic) !== NoFlags
290 ) {
291 const instance: ViewTransitionState = child.stateNode;
292 if (instance.paired) {
293 const props: ViewTransitionProps = child.memoizedProps;
294 if (props.name == null || props.name === 'auto') {
295 throw new Error(
296 'Found a pair with an auto name. This is a bug in React.',
297 );
298 }
299 const name = props.name;
300 const className: ?string = getViewTransitionClassName(
301 props.default,
302 props.share,
303 );
304 if (className !== 'none') {
305 // We found a new appearing view transition with the same name as this deletion.
306 // We'll transition between them.
307 const inViewport = applyViewTransitionToHostInstances(
308 child,
309 name,
310 className,
311 null,
312 false,
313 );
314 if (!inViewport) {
315 // This boundary is exiting within the viewport but is going to leave the viewport.
316 // Instead, we treat this as an exit of the previous entry by reverting the new name.
317 // Ideally we could undo the old transition but it's now too late. It's also on its
318 // on snapshot. We have know was for it to paint onto the original group.
319 // TODO: This will lead to things unexpectedly having exit animations that normally
320 // wouldn't happen. Consider if we should just let this fly off the screen instead.
321 restoreViewTransitionOnHostInstances(child.child, false);
322 }
323 }
324 }
325 }
326 }
327 child = child.sibling;
328 }
329 }
330
331 export function commitParentEnterViewTransitions(
332 parent: Fiber,
333 gesture: boolean,
334 ): void {
335 let child = parent.child;
336 while (child !== null) {
337 if (child.tag === OffscreenComponent && child.memoizedState !== null) {
338 // Skip hidden subtrees.
339 } else if (child.tag === ViewTransitionComponent) {
340 const props: ViewTransitionProps = child.memoizedProps;
341 const hasParentClass = props.parentEnter !== undefined;
342 const hasParentHandler = gesture
343 ? props.onGestureParentEnter != null
344 : props.onParentEnter != null;
345 if (hasParentClass || hasParentHandler) {
346 let relay = true;
347 if (hasParentClass) {
348 const state: ViewTransitionState = child.stateNode;
349 const name = getViewTransitionName(props, state);
350 const className: ?string = getViewTransitionClassName(
351 props.default,
352 props.parentEnter,
353 );
354 if (className === 'none') {
355 relay = false;
356 } else {
357 applyViewTransitionToHostInstances(
358 child,
359 name,
360 className,
361 null,
362 false,
363 );
364 if (hasParentHandler) {
365 if (gesture) {
366 scheduleGestureTransitionEvent(
367 child,
368 props.onGestureParentEnter,
369 );
370 } else {
371 scheduleViewTransitionEvent(child, props.onParentEnter);
372 }
373 }
374 }
375 } else {
376 if (gesture) {
377 scheduleGestureTransitionEvent(child, props.onGestureParentEnter);
378 } else {
379 scheduleViewTransitionEvent(child, props.onParentEnter);
380 }
381 }
382 if (relay) {
383 commitParentEnterViewTransitions(child, gesture);
384 }
385 }
386 } else if ((child.subtreeFlags & ViewTransitionStaticParent) !== NoFlags) {
387 commitParentEnterViewTransitions(child, gesture);
388 }
389 child = child.sibling;
390 }
391 }
392
393 export function commitParentExitViewTransitions(
394 parent: Fiber,
395 gesture: boolean,
396 ): void {
397 let child = parent.child;
398 while (child !== null) {
399 if (child.tag === OffscreenComponent && child.memoizedState !== null) {
400 // Skip hidden subtrees.
401 } else if (child.tag === ViewTransitionComponent) {
402 const props: ViewTransitionProps = child.memoizedProps;
403 const hasParentClass = props.parentExit !== undefined;
404 const hasParentHandler = gesture
405 ? props.onGestureParentExit != null
406 : props.onParentExit != null;
407 if (hasParentClass || hasParentHandler) {
408 let relay = true;
409 if (hasParentClass) {
410 const state: ViewTransitionState = child.stateNode;
411 const name = getViewTransitionName(props, state);
412 const className: ?string = getViewTransitionClassName(
413 props.default,
414 props.parentExit,
415 );
416 if (className === 'none') {
417 relay = false;
418 } else {
419 applyViewTransitionToHostInstances(
420 child,
421 name,
422 className,
423 null,
424 false,
425 );
426 if (hasParentHandler) {
427 if (gesture) {
428 scheduleGestureTransitionEvent(
429 child,
430 props.onGestureParentExit,
431 );
432 } else {
433 scheduleViewTransitionEvent(child, props.onParentExit);
434 }
435 }
436 }
437 } else {
438 if (gesture) {
439 scheduleGestureTransitionEvent(child, props.onGestureParentExit);
440 } else {
441 scheduleViewTransitionEvent(child, props.onParentExit);
442 }
443 }
444 if (relay) {
445 commitParentExitViewTransitions(child, gesture);
446 }
447 }
448 } else if ((child.subtreeFlags & ViewTransitionStaticParent) !== NoFlags) {
449 commitParentExitViewTransitions(child, gesture);
450 }
451 child = child.sibling;
452 }
453 }
454
455 function restoreParentEnterOrExitViewTransitions(parent: Fiber): void {
456 let child = parent.child;
457 while (child !== null) {
458 if (child.tag === OffscreenComponent && child.memoizedState !== null) {
459 // Skip hidden subtrees.
460 } else if (child.tag === ViewTransitionComponent) {
461 const props: ViewTransitionProps = child.memoizedProps;
462 const hasParentClass =
463 props.parentEnter !== undefined || props.parentExit !== undefined;
464 const hasParentHandler =
465 props.onParentEnter != null ||
466 props.onParentExit != null ||
467 props.onGestureParentEnter != null ||
468 props.onGestureParentExit != null;
469 if (hasParentClass) {
470 restoreViewTransitionOnHostInstances(child.child, false);
471 }
472 if (hasParentClass || hasParentHandler) {
473 restoreParentEnterOrExitViewTransitions(child);
474 }
475 } else if ((child.subtreeFlags & ViewTransitionStaticParent) !== NoFlags) {
476 restoreParentEnterOrExitViewTransitions(child);
477 }
478 child = child.sibling;
479 }
480 }
481
482 export function commitEnterViewTransitions(
483 placement: Fiber,
484 gesture: boolean,
485 ): void {
486 if (placement.tag === ViewTransitionComponent) {
487 const state: ViewTransitionState = placement.stateNode;
488 const props: ViewTransitionProps = placement.memoizedProps;
489 const name = getViewTransitionName(props, state);
490 const className: ?string = getViewTransitionClassName(
491 props.default,
492 state.paired ? props.share : props.enter,
493 );
494 if (className !== 'none') {
495 const inViewport = applyViewTransitionToHostInstances(
496 placement,
497 name,
498 className,
499 null,
500 false,
501 );
502 if (!inViewport) {
503 // TODO: If this was part of a pair we will still run the onShare callback.
504 // Revert the transition names. This boundary is not in the viewport
505 // so we won't bother animating it.
506 restoreViewTransitionOnHostInstances(placement.child, false);
507 // TODO: Should we still visit the children in case a named one was in the viewport?
508 } else {
509 commitAppearingPairViewTransitions(placement);
510
511 if (!state.paired) {
512 if (gesture) {
513 scheduleGestureTransitionEvent(placement, props.onGestureEnter);
514 } else {
515 scheduleViewTransitionEvent(placement, props.onEnter);
516 }
517 if (enableViewTransitionParentEnterExit) {
518 commitParentEnterViewTransitions(placement, gesture);
519 }
520 }
521 }
522 } else {
523 commitAppearingPairViewTransitions(placement);
524 }
525 } else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
526 let child = placement.child;
527 while (child !== null) {
528 commitEnterViewTransitions(child, gesture);
529 child = child.sibling;
530 }
531 } else {
532 commitAppearingPairViewTransitions(placement);
533 }
534 }
535
536 function commitDeletedPairViewTransitions(deletion: Fiber): void {
537 if (
538 appearingViewTransitions === null ||
539 appearingViewTransitions.size === 0
540 ) {
541 // We've found all.
542 return;
543 }
544 const pairs = appearingViewTransitions;
545 if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
546 // This has no named view transitions in its subtree.
547 return;
548 }
549 let child = deletion.child;
550 while (child !== null) {
551 if (child.tag === OffscreenComponent && child.memoizedState !== null) {
552 // This tree was already hidden so we skip it.
553 } else {
554 if (
555 child.tag === ViewTransitionComponent &&
556 (child.flags & ViewTransitionNamedStatic) !== NoFlags
557 ) {
558 const props: ViewTransitionProps = child.memoizedProps;
559 const name = props.name;
560 if (name != null && name !== 'auto') {
561 const pair = pairs.get(name);
562 if (pair !== undefined) {
563 const className: ?string = getViewTransitionClassName(
564 props.default,
565 props.share,
566 );
567 if (className !== 'none') {
568 // We found a new appearing view transition with the same name as this deletion.
569 const inViewport = applyViewTransitionToHostInstances(
570 child,
571 name,
572 className,
573 null,
574 false,
575 );
576 if (!inViewport) {
577 // This boundary is not in the viewport so we won't treat it as a matched pair.
578 // Revert the transition names. This avoids it flying onto the screen which can
579 // be disruptive and doesn't really preserve any continuity anyway.
580 restoreViewTransitionOnHostInstances(child.child, false);
581 } else {
582 // We'll transition between them.
583 const oldInstance: ViewTransitionState = child.stateNode;
584 const newInstance: ViewTransitionState = pair;
585 newInstance.paired = oldInstance;
586 oldInstance.paired = newInstance;
587 // Note: If the other side ends up outside the viewport, we'll still run this.
588 // Therefore it's possible for onShare to be called with only an old snapshot.
589 scheduleViewTransitionEvent(child, props.onShare);
590 }
591 }
592 // Delete the entry so that we know when we've found all of them
593 // and can stop searching (size reaches zero).
594 pairs.delete(name);
595 if (pairs.size === 0) {
596 break;
597 }
598 }
599 }
600 }
601 commitDeletedPairViewTransitions(child);
602 }
603 child = child.sibling;
604 }
605 }
606
607 export function commitExitViewTransitions(deletion: Fiber): void {
608 if (deletion.tag === ViewTransitionComponent) {
609 const props: ViewTransitionProps = deletion.memoizedProps;
610 const name = getViewTransitionName(props, deletion.stateNode);
611 const pair =
612 appearingViewTransitions !== null
613 ? appearingViewTransitions.get(name)
614 : undefined;
615 const className: ?string = getViewTransitionClassName(
616 props.default,
617 pair !== undefined ? props.share : props.exit,
618 );
619 if (className !== 'none') {
620 const inViewport = applyViewTransitionToHostInstances(
621 deletion,
622 name,
623 className,
624 null,
625 false,
626 );
627 if (!inViewport) {
628 // Revert the transition names. This boundary is not in the viewport
629 // so we won't bother animating it.
630 restoreViewTransitionOnHostInstances(deletion.child, false);
631 // TODO: Should we still visit the children in case a named one was in the viewport?
632 } else if (pair !== undefined) {
633 // We found a new appearing view transition with the same name as this deletion.
634 // We'll transition between them instead of running the normal exit.
635 const oldInstance: ViewTransitionState = deletion.stateNode;
636 const newInstance: ViewTransitionState = pair;
637 newInstance.paired = oldInstance;
638 oldInstance.paired = newInstance;
639 // Delete the entry so that we know when we've found all of them
640 // and can stop searching (size reaches zero).
641 // $FlowFixMe[incompatible-use]: Refined by the pair.
642 appearingViewTransitions.delete(name);
643 // Note: If the other side ends up outside the viewport, we'll still run this.
644 // Therefore it's possible for onShare to be called with only an old snapshot.
645 scheduleViewTransitionEvent(deletion, props.onShare);
646 } else {
647 scheduleViewTransitionEvent(deletion, props.onExit);
648 if (enableViewTransitionParentEnterExit) {
649 commitParentExitViewTransitions(deletion, false);
650 }
651 }
652 }
653 if (appearingViewTransitions !== null) {
654 // Look for more pairs deeper in the tree.
655 commitDeletedPairViewTransitions(deletion);
656 }
657 } else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
658 let child = deletion.child;
659 while (child !== null) {
660 commitExitViewTransitions(child);
661 child = child.sibling;
662 }
663 } else {
664 if (appearingViewTransitions !== null) {
665 commitDeletedPairViewTransitions(deletion);
666 }
667 }
668 }
669
670 export function commitBeforeUpdateViewTransition(
671 current: Fiber,
672 finishedWork: Fiber,
673 ): void {
674 // The way we deal with multiple HostInstances as children of a View Transition in an
675 // update can get tricky. The important bit is that if you swap out n HostInstances
676 // from n HostInstances then they match up in order. Similarly, if you don't swap
677 // any HostInstances each instance just transitions as is.
678 //
679 // We call this function twice. First we apply the view transition names on the
680 // "current" tree in the snapshot phase. Then in the mutation phase we apply view
681 // transition names to the "finishedWork" tree.
682 //
683 // This means that if there were insertions or deletions before an updated Instance
684 // that same Instance might get different names in the "old" and the "new" state.
685 // For example if you swap two HostInstances inside a ViewTransition they don't
686 // animate to swap position but rather cross-fade into the other instance. This might
687 // be unexpected but it is in line with the semantics that the ViewTransition is its
688 // own layer that cross-fades its content when it updates. If you want to reorder then
689 // each child needs its own ViewTransition.
690 const oldProps: ViewTransitionProps = current.memoizedProps;
691 const oldName = getViewTransitionName(oldProps, current.stateNode);
692 const newProps: ViewTransitionProps = finishedWork.memoizedProps;
693 // This className applies only if there are fewer child DOM nodes than
694 // before or if this update should've been cancelled but we ended up with
695 // a parent animating so we need to animate the child too.
696 // For example, if update="foo" layout="none" and it turns out this was
697 // a layout only change, then the "foo" class will be applied even though
698 // it was not actually an update. Which is a bug.
699 const className: ?string = getViewTransitionClassName(
700 newProps.default,
701 newProps.update,
702 );
703 if (className === 'none') {
704 // If update is "none" then we don't have to apply a name. Since we won't animate this boundary.
705 return;
706 }
707 applyViewTransitionToHostInstances(
708 current,
709 oldName,
710 className,
711 (current.memoizedState = []),
712 true,
713 );
714 }
715
716 export function commitNestedViewTransitions(changedParent: Fiber): void {
717 let child = changedParent.child;
718 while (child !== null) {
719 if (child.tag === ViewTransitionComponent) {
720 // In this case the outer ViewTransition component wins but if there
721 // was an update through this component then the inner one wins.
722 const props: ViewTransitionProps = child.memoizedProps;
723 const name = getViewTransitionName(props, child.stateNode);
724 const className: ?string = getViewTransitionClassName(
725 props.default,
726 props.update,
727 );
728 // "Nested" view transitions are in subtrees that didn't update so
729 // this is a "current". We normally clear this upon rerendering
730 // but we use this flag to track changes from layout in the commit.
731 // So we need it to be cleared before we do that.
732 // TODO: Use some other temporary state to track this.
733 child.flags &= ~Update;
734 if (className !== 'none') {
735 applyViewTransitionToHostInstances(
736 child,
737 name,
738 className,
739 (child.memoizedState = []),
740 false,
741 );
742 }
743 } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
744 commitNestedViewTransitions(child);
745 }
746 child = child.sibling;
747 }
748 }
749
750 function restorePairedViewTransitions(parent: Fiber): void {
751 if ((parent.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
752 // This has no named view transitions in its subtree.
753 return;
754 }
755 let child = parent.child;
756 while (child !== null) {
757 if (child.tag === OffscreenComponent && child.memoizedState !== null) {
758 // This tree was already hidden so we skip it.
759 } else {
760 if (
761 child.tag === ViewTransitionComponent &&
762 (child.flags & ViewTransitionNamedStatic) !== NoFlags
763 ) {
764 const instance: ViewTransitionState = child.stateNode;
765 if (instance.paired !== null) {
766 instance.paired = null;
767 restoreViewTransitionOnHostInstances(child.child, false);
768 }
769 }
770 restorePairedViewTransitions(child);
771 }
772 child = child.sibling;
773 }
774 }
775
776 export function restoreEnterOrExitViewTransitions(fiber: Fiber): void {
777 if (fiber.tag === ViewTransitionComponent) {
778 const instance: ViewTransitionState = fiber.stateNode;
779 instance.paired = null;
780 restoreViewTransitionOnHostInstances(fiber.child, false);
781 if (enableViewTransitionParentEnterExit) {
782 restoreParentEnterOrExitViewTransitions(fiber);
783 }
784 restorePairedViewTransitions(fiber);
785 } else if ((fiber.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
786 let child = fiber.child;
787 while (child !== null) {
788 restoreEnterOrExitViewTransitions(child);
789 child = child.sibling;
790 }
791 } else {
792 restorePairedViewTransitions(fiber);
793 }
794 }
795
796 export function restoreUpdateViewTransition(
797 current: Fiber,
798 finishedWork: Fiber,
799 ): void {
800 restoreViewTransitionOnHostInstances(current.child, true);
801 restoreViewTransitionOnHostInstances(finishedWork.child, true);
802 }
803
804 export function restoreUpdateViewTransitionForGesture(
805 current: Fiber,
806 finishedWork: Fiber,
807 ): void {
808 // For gestures we don't need to reset "finishedWork" because those would
809 // have all been clones that got deleted.
810 restoreViewTransitionOnHostInstances(current.child, true);
811 }
812
813 export function restoreNestedViewTransitions(changedParent: Fiber): void {
814 let child = changedParent.child;
815 while (child !== null) {
816 if (child.tag === ViewTransitionComponent) {
817 restoreViewTransitionOnHostInstances(child.child, false);
818 } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
819 restoreNestedViewTransitions(child);
820 }
821 child = child.sibling;
822 }
823 }
824
825 export function measureViewTransitionHostInstances(
826 parentViewTransition: Fiber,
827 child: null | Fiber,
828 newName: string,
829 oldName: string,
830 className: ?string,
831 previousMeasurements: null | Array<InstanceMeasurement>,
832 stopAtNestedViewTransitions: boolean,
833 ): boolean {
834 viewTransitionHostInstanceIdx = 0;
835 return measureViewTransitionHostInstancesRecursive(
836 parentViewTransition,
837 child,
838 newName,
839 oldName,
840 className,
841 previousMeasurements,
842 stopAtNestedViewTransitions,
843 );
844 }
845
846 function measureViewTransitionHostInstancesRecursive(
847 parentViewTransition: Fiber,
848 child: null | Fiber,
849 newName: string,
850 oldName: string,
851 className: ?string,
852 previousMeasurements: null | Array<InstanceMeasurement>,
853 stopAtNestedViewTransitions: boolean,
854 ): boolean {
855 // $FlowFixMe[constant-condition]
856 if (!supportsMutation) {
857 if (enableViewTransitionForPersistenceMode) {
858 while (child !== null) {
859 if (child.tag === HostComponent) {
860 const instance: Instance = child.stateNode;
861 if (
862 previousMeasurements == null ||
863 viewTransitionHostInstanceIdx >= previousMeasurements.length
864 ) {
865 // If there was an insertion of extra nodes, we have to assume they affected the parent.
866 // It should have already been marked as an Update due to the mutation.
867 parentViewTransition.flags |= AffectedParentLayout;
868 }
869 // TODO: check if instance is out of viewport
870 if ((parentViewTransition.flags & Update) !== NoFlags) {
871 applyViewTransitionName(
872 instance,
873 viewTransitionHostInstanceIdx === 0
874 ? newName
875 : newName + '_' + viewTransitionHostInstanceIdx,
876 className,
877 );
878 }
879 // TODO: cancel transition by pushing into viewTransitionCancelableChildren
880 viewTransitionHostInstanceIdx++;
881 } else if (
882 child.tag === OffscreenComponent &&
883 child.memoizedState !== null
884 ) {
885 // Skip any hidden subtrees. They were or are effectively not there.
886 } else if (
887 child.tag === ViewTransitionComponent &&
888 stopAtNestedViewTransitions
889 ) {
890 // Skip any nested view transitions for updates since in that case the
891 // inner most one is the one that handles the update.
892 // If this inner boundary resized we need to bubble that information up.
893 parentViewTransition.flags |= child.flags & AffectedParentLayout;
894 } else {
895 measureViewTransitionHostInstancesRecursive(
896 parentViewTransition,
897 child.child,
898 newName,
899 oldName,
900 className,
901 previousMeasurements,
902 stopAtNestedViewTransitions,
903 );
904 }
905 child = child.sibling;
906 }
907 return true;
908 } else {
909 return false;
910 }
911 }
912 let inViewport = false;
913 while (child !== null) {
914 if (child.tag === HostComponent) {
915 const instance: Instance = child.stateNode;
916 if (
917 previousMeasurements !== null &&
918 viewTransitionHostInstanceIdx < previousMeasurements.length
919 ) {
920 // The previous measurement of the Instance in this location within the ViewTransition.
921 // Note that this might not be the same exact Instance if the Instances within the
922 // ViewTransition changed.
923 const previousMeasurement =
924 previousMeasurements[viewTransitionHostInstanceIdx];
925 const nextMeasurement = measureInstance(instance);
926 if (
927 wasInstanceInViewport(previousMeasurement) ||
928 wasInstanceInViewport(nextMeasurement)
929 ) {
930 // If either the old or new state was within the viewport we have to animate this.
931 // But if it turns out that none of them were we'll be able to skip it.
932 inViewport = true;
933 }
934 if (
935 (parentViewTransition.flags & Update) === NoFlags &&
936 hasInstanceChanged(previousMeasurement, nextMeasurement)
937 ) {
938 parentViewTransition.flags |= Update;
939 }
940 if (hasInstanceAffectedParent(previousMeasurement, nextMeasurement)) {
941 // If this instance size within its parent has changed it might have caused the
942 // parent to relayout which needs a cross fade.
943 parentViewTransition.flags |= AffectedParentLayout;
944 }
945 } else {
946 // If there was an insertion of extra nodes, we have to assume they affected the parent.
947 // It should have already been marked as an Update due to the mutation.
948 parentViewTransition.flags |= AffectedParentLayout;
949 }
950 if ((parentViewTransition.flags & Update) !== NoFlags) {
951 // We might update this node so we need to apply its new name for the new state.
952 // Additionally in the ApplyGesture case we also need to do this because the clone
953 // will have the name but this one won't.
954 applyViewTransitionName(
955 instance,
956 viewTransitionHostInstanceIdx === 0
957 ? newName
958 : // If we have multiple Host Instances below, we add a suffix to the name to give
959 // each one a unique name.
960 newName + '_' + viewTransitionHostInstanceIdx,
961 className,
962 );
963 }
964 if (!inViewport || (parentViewTransition.flags & Update) === NoFlags) {
965 // It turns out that we had no other deeper mutations, the child transitions didn't
966 // affect the parent layout and this instance hasn't changed size. So we can skip
967 // animating it. However, in the current model this only works if the parent also
968 // doesn't animate. So we have to queue these and wait until we complete the parent
969 // to cancel them.
970 if (viewTransitionCancelableChildren === null) {
971 viewTransitionCancelableChildren = [];
972 }
973 viewTransitionCancelableChildren.push(
974 instance,
975 viewTransitionHostInstanceIdx === 0
976 ? oldName
977 : // If we have multiple Host Instances below, we add a suffix to the name to give
978 // each one a unique name.
979 oldName + '_' + viewTransitionHostInstanceIdx,
980 child.memoizedProps,
981 );
982 }
983 viewTransitionHostInstanceIdx++;
984 } else if (
985 child.tag === OffscreenComponent &&
986 child.memoizedState !== null
987 ) {
988 // Skip any hidden subtrees. They were or are effectively not there.
989 } else if (
990 child.tag === ViewTransitionComponent &&
991 stopAtNestedViewTransitions
992 ) {
993 // Skip any nested view transitions for updates since in that case the
994 // inner most one is the one that handles the update.
995 // If this inner boundary resized we need to bubble that information up.
996 parentViewTransition.flags |= child.flags & AffectedParentLayout;
997 } else {
998 if (
999 measureViewTransitionHostInstancesRecursive(
1000 parentViewTransition,
1001 child.child,
1002 newName,
1003 oldName,
1004 className,
1005 previousMeasurements,
1006 stopAtNestedViewTransitions,
1007 )
1008 ) {
1009 inViewport = true;
1010 }
1011 }
1012 child = child.sibling;
1013 }
1014 return inViewport;
1015 }
1016
1017 export function measureUpdateViewTransition(
1018 current: Fiber,
1019 finishedWork: Fiber,
1020 gesture: boolean,
1021 ): boolean {
1022 // If this was a gesture then which Fiber was used for the "old" vs "new" state is reversed.
1023 // We still need to treat "finishedWork" as the Fiber that contains the flags for this commmit.
1024 const oldFiber = gesture ? finishedWork : current;
1025 const newFiber = gesture ? current : finishedWork;
1026 const props: ViewTransitionProps = newFiber.memoizedProps;
1027 const state: ViewTransitionState = newFiber.stateNode;
1028 const newName = getViewTransitionName(props, state);
1029 const oldName = getViewTransitionName(oldFiber.memoizedProps, state);
1030 // Whether it ends up having been updated or relayout we apply the update class name.
1031 const className: ?string = getViewTransitionClassName(
1032 props.default,
1033 props.update,
1034 );
1035 if (className === 'none') {
1036 // If update is "none" then we don't have to apply a name. Since we won't animate this boundary.
1037 return false;
1038 }
1039 // If nothing changed due to a mutation, or children changing size
1040 // and the measurements end up unchanged, we should restore it to not animate.
1041 let previousMeasurements: null | Array<InstanceMeasurement>;
1042 if (gesture) {
1043 const clones = state.clones;
1044 if (clones === null) {
1045 previousMeasurements = null;
1046 } else {
1047 previousMeasurements = clones.map(measureClonedInstance);
1048 }
1049 } else {
1050 previousMeasurements = oldFiber.memoizedState;
1051 oldFiber.memoizedState = null; // Clear it. We won't need it anymore.
1052 }
1053 const inViewport = measureViewTransitionHostInstances(
1054 finishedWork, // This is always finishedWork since it's used to assign flags.
1055 newFiber.child, // This either current or finishedWork depending on if was a gesture.
1056 newName,
1057 oldName,
1058 className,
1059 previousMeasurements,
1060 true,
1061 );
1062 const previousCount =
1063 previousMeasurements === null ? 0 : previousMeasurements.length;
1064 if (viewTransitionHostInstanceIdx !== previousCount) {
1065 // If we found a different number of child DOM nodes we need to assume that
1066 // the parent layout may have changed as a result. This is not necessarily
1067 // true if those nodes were absolutely positioned.
1068 finishedWork.flags |= AffectedParentLayout;
1069 }
1070 return inViewport;
1071 }
1072
1073 export function measureNestedViewTransitions(
1074 changedParent: Fiber,
1075 gesture: boolean,
1076 ): void {
1077 let child = changedParent.child;
1078 while (child !== null) {
1079 if (child.tag === ViewTransitionComponent) {
1080 const props: ViewTransitionProps = child.memoizedProps;
1081 const state: ViewTransitionState = child.stateNode;
1082 const name = getViewTransitionName(props, state);
1083 const className: ?string = getViewTransitionClassName(
1084 props.default,
1085 props.update,
1086 );
1087 let previousMeasurements: null | Array<InstanceMeasurement>;
1088 if (gesture) {
1089 const clones = state.clones;
1090 if (clones === null) {
1091 previousMeasurements = null;
1092 } else {
1093 previousMeasurements = clones.map(measureClonedInstance);
1094 }
1095 } else {
1096 previousMeasurements = child.memoizedState;
1097 child.memoizedState = null; // Clear it. We won't need it anymore.
1098 }
1099 const inViewport = measureViewTransitionHostInstances(
1100 child,
1101 child.child,
1102 name,
1103 name, // Since this is unchanged, new and old name is the same.
1104 className,
1105 previousMeasurements,
1106 false,
1107 );
1108 if ((child.flags & Update) === NoFlags || !inViewport) {
1109 // Nothing changed.
1110 } else {
1111 if (gesture) {
1112 scheduleGestureTransitionEvent(child, props.onGestureUpdate);
1113 } else {
1114 scheduleViewTransitionEvent(child, props.onUpdate);
1115 }
1116 }
1117 } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
1118 measureNestedViewTransitions(child, gesture);
1119 }
1120 child = child.sibling;
1121 }
1122 }