@samitouri / QOS-React / commits / 403d4fb852

Move ViewTransitions helpers to ReactFiberCommitViewTransitions (#32462)

This doesn't change anything. It just moves some functions. This moves the view transitions helper functions into its own file. This is similar to how I already moved ReactFiberCommitEffects and ReactFiberCommitHostEffects out of ReactFiberCommitWork. This makes it a bit easier to navigate and get an overview of ReactFiberCommitWork but another motivation is also so that I can refer to these helpers from [ReactFiberApplyGesture](https://github.com/facebook/react/pull/32451/files#diff-42297cf327dee8e01d83c85314b8965953b9674e7c4615ce6c430464dcc8550b).

Sebastian Markbåge committed Feb 25, 2025 at 12:45 UTC 403d4fb852384b820a8fe405413891d8c74bbf5d
3 files changed +861 -791
packages/react-reconciler/src/ReactFiberCommitViewTransitions.js new
+831
@@ -0,0 +1,831 @@
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 {Instance, InstanceMeasurement, Props} from './ReactFiberConfig';
11 +import type {Fiber} from './ReactInternalTypes';
12 +import type {
13 + ViewTransitionProps,
14 + ViewTransitionState,
15 +} from './ReactFiberViewTransitionComponent';
16 +
17 +import {
18 + HostComponent,
19 + OffscreenComponent,
20 + ViewTransitionComponent,
21 +} from './ReactWorkTags';
22 +import {
23 + NoFlags,
24 + Update,
25 + ViewTransitionStatic,
26 + AffectedParentLayout,
27 + ViewTransitionNamedStatic,
28 +} from './ReactFiberFlags';
29 +import {
30 + supportsMutation,
31 + applyViewTransitionName,
32 + restoreViewTransitionName,
33 + measureInstance,
34 + hasInstanceChanged,
35 + hasInstanceAffectedParent,
36 + wasInstanceInViewport,
37 +} from './ReactFiberConfig';
38 +import {scheduleViewTransitionEvent} from './ReactFiberWorkLoop';
39 +import {
40 + getViewTransitionName,
41 + getViewTransitionClassName,
42 +} from './ReactFiberViewTransitionComponent';
43 +
44 +export let shouldStartViewTransition: boolean = false;
45 +
46 +export function resetShouldStartViewTransition(): void {
47 + shouldStartViewTransition = false;
48 +}
49 +
50 +// This tracks named ViewTransition components found in the accumulateSuspenseyCommit
51 +// phase that might need to find deleted pairs in the beforeMutation phase.
52 +let appearingViewTransitions: Map<string, ViewTransitionState> | null = null;
53 +
54 +export function resetAppearingViewTransitions(): void {
55 + appearingViewTransitions = null;
56 +}
57 +
58 +export function trackAppearingViewTransition(
59 + name: string,
60 + state: ViewTransitionState,
61 +): void {
62 + if (appearingViewTransitions === null) {
63 + appearingViewTransitions = new Map();
64 + }
65 + appearingViewTransitions.set(name, state);
66 +}
67 +
68 +// We can't cancel view transition children until we know that their parent also
69 +// don't need to transition.
70 +export let viewTransitionCancelableChildren: null | Array<
71 + Instance | string | Props,
72 +> = null; // tupled array where each entry is [instance: Instance, oldName: string, props: Props]
73 +
74 +export function setViewTransitionCancelableChildren(
75 + children: null | Array<Instance | string | Props>,
76 +): void {
77 + viewTransitionCancelableChildren = children;
78 +}
79 +
80 +let viewTransitionHostInstanceIdx = 0;
81 +
82 +function applyViewTransitionToHostInstances(
83 + child: null | Fiber,
84 + name: string,
85 + className: ?string,
86 + collectMeasurements: null | Array<InstanceMeasurement>,
87 + stopAtNestedViewTransitions: boolean,
88 +): boolean {
89 + if (!supportsMutation) {
90 + return false;
91 + }
92 + let inViewport = false;
93 + while (child !== null) {
94 + if (child.tag === HostComponent) {
95 + shouldStartViewTransition = true;
96 + const instance: Instance = child.stateNode;
97 + if (collectMeasurements !== null) {
98 + const measurement = measureInstance(instance);
99 + collectMeasurements.push(measurement);
100 + if (wasInstanceInViewport(measurement)) {
101 + inViewport = true;
102 + }
103 + } else if (!inViewport) {
104 + if (wasInstanceInViewport(measureInstance(instance))) {
105 + inViewport = true;
106 + }
107 + }
108 + applyViewTransitionName(
109 + instance,
110 + viewTransitionHostInstanceIdx === 0
111 + ? name
112 + : // If we have multiple Host Instances below, we add a suffix to the name to give
113 + // each one a unique name.
114 + name + '_' + viewTransitionHostInstanceIdx,
115 + className,
116 + );
117 + viewTransitionHostInstanceIdx++;
118 + } else if (
119 + child.tag === OffscreenComponent &&
120 + child.memoizedState !== null
121 + ) {
122 + // Skip any hidden subtrees. They were or are effectively not there.
123 + } else if (
124 + child.tag === ViewTransitionComponent &&
125 + stopAtNestedViewTransitions
126 + ) {
127 + // Skip any nested view transitions for updates since in that case the
128 + // inner most one is the one that handles the update.
129 + } else {
130 + if (
131 + applyViewTransitionToHostInstances(
132 + child.child,
133 + name,
134 + className,
135 + collectMeasurements,
136 + stopAtNestedViewTransitions,
137 + )
138 + ) {
139 + inViewport = true;
140 + }
141 + }
142 + child = child.sibling;
143 + }
144 + return inViewport;
145 +}
146 +
147 +function restoreViewTransitionOnHostInstances(
148 + child: null | Fiber,
149 + stopAtNestedViewTransitions: boolean,
150 +): void {
151 + if (!supportsMutation) {
152 + return;
153 + }
154 + while (child !== null) {
155 + if (child.tag === HostComponent) {
156 + const instance: Instance = child.stateNode;
157 + restoreViewTransitionName(instance, child.memoizedProps);
158 + } else if (
159 + child.tag === OffscreenComponent &&
160 + child.memoizedState !== null
161 + ) {
162 + // Skip any hidden subtrees. They were or are effectively not there.
163 + } else if (
164 + child.tag === ViewTransitionComponent &&
165 + stopAtNestedViewTransitions
166 + ) {
167 + // Skip any nested view transitions for updates since in that case the
168 + // inner most one is the one that handles the update.
169 + } else {
170 + restoreViewTransitionOnHostInstances(
171 + child.child,
172 + stopAtNestedViewTransitions,
173 + );
174 + }
175 + child = child.sibling;
176 + }
177 +}
178 +
179 +function commitAppearingPairViewTransitions(placement: Fiber): void {
180 + if ((placement.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
181 + // This has no named view transitions in its subtree.
182 + return;
183 + }
184 + let child = placement.child;
185 + while (child !== null) {
186 + if (child.tag === OffscreenComponent && child.memoizedState === null) {
187 + // This tree was already hidden so we skip it.
188 + } else {
189 + commitAppearingPairViewTransitions(child);
190 + if (
191 + child.tag === ViewTransitionComponent &&
192 + (child.flags & ViewTransitionNamedStatic) !== NoFlags
193 + ) {
194 + const instance: ViewTransitionState = child.stateNode;
195 + if (instance.paired) {
196 + const props: ViewTransitionProps = child.memoizedProps;
197 + if (props.name == null || props.name === 'auto') {
198 + throw new Error(
199 + 'Found a pair with an auto name. This is a bug in React.',
200 + );
201 + }
202 + const name = props.name;
203 + const className: ?string = getViewTransitionClassName(
204 + props.className,
205 + props.share,
206 + );
207 + if (className !== 'none') {
208 + // We found a new appearing view transition with the same name as this deletion.
209 + // We'll transition between them.
210 + viewTransitionHostInstanceIdx = 0;
211 + const inViewport = applyViewTransitionToHostInstances(
212 + child.child,
213 + name,
214 + className,
215 + null,
216 + false,
217 + );
218 + if (!inViewport) {
219 + // This boundary is exiting within the viewport but is going to leave the viewport.
220 + // Instead, we treat this as an exit of the previous entry by reverting the new name.
221 + // Ideally we could undo the old transition but it's now too late. It's also on its
222 + // on snapshot. We have know was for it to paint onto the original group.
223 + // TODO: This will lead to things unexpectedly having exit animations that normally
224 + // wouldn't happen. Consider if we should just let this fly off the screen instead.
225 + restoreViewTransitionOnHostInstances(child.child, false);
226 + }
227 + }
228 + }
229 + }
230 + }
231 + child = child.sibling;
232 + }
233 +}
234 +
235 +export function commitEnterViewTransitions(placement: Fiber): void {
236 + if (placement.tag === ViewTransitionComponent) {
237 + const state: ViewTransitionState = placement.stateNode;
238 + const props: ViewTransitionProps = placement.memoizedProps;
239 + const name = getViewTransitionName(props, state);
240 + const className: ?string = getViewTransitionClassName(
241 + props.className,
242 + state.paired ? props.share : props.enter,
243 + );
244 + if (className !== 'none') {
245 + viewTransitionHostInstanceIdx = 0;
246 + const inViewport = applyViewTransitionToHostInstances(
247 + placement.child,
248 + name,
249 + className,
250 + null,
251 + false,
252 + );
253 + if (!inViewport) {
254 + // TODO: If this was part of a pair we will still run the onShare callback.
255 + // Revert the transition names. This boundary is not in the viewport
256 + // so we won't bother animating it.
257 + restoreViewTransitionOnHostInstances(placement.child, false);
258 + // TODO: Should we still visit the children in case a named one was in the viewport?
259 + } else {
260 + commitAppearingPairViewTransitions(placement);
261 +
262 + if (!state.paired) {
263 + scheduleViewTransitionEvent(placement, props.onEnter);
264 + }
265 + }
266 + } else {
267 + commitAppearingPairViewTransitions(placement);
268 + }
269 + } else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
270 + let child = placement.child;
271 + while (child !== null) {
272 + commitEnterViewTransitions(child);
273 + child = child.sibling;
274 + }
275 + } else {
276 + commitAppearingPairViewTransitions(placement);
277 + }
278 +}
279 +
280 +function commitDeletedPairViewTransitions(deletion: Fiber): void {
281 + if (
282 + appearingViewTransitions === null ||
283 + appearingViewTransitions.size === 0
284 + ) {
285 + // We've found all.
286 + return;
287 + }
288 + const pairs = appearingViewTransitions;
289 + if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
290 + // This has no named view transitions in its subtree.
291 + return;
292 + }
293 + let child = deletion.child;
294 + while (child !== null) {
295 + if (child.tag === OffscreenComponent && child.memoizedState === null) {
296 + // This tree was already hidden so we skip it.
297 + } else {
298 + if (
299 + child.tag === ViewTransitionComponent &&
300 + (child.flags & ViewTransitionNamedStatic) !== NoFlags
301 + ) {
302 + const props: ViewTransitionProps = child.memoizedProps;
303 + const name = props.name;
304 + if (name != null && name !== 'auto') {
305 + const pair = pairs.get(name);
306 + if (pair !== undefined) {
307 + const className: ?string = getViewTransitionClassName(
308 + props.className,
309 + props.share,
310 + );
311 + if (className !== 'none') {
312 + // We found a new appearing view transition with the same name as this deletion.
313 + viewTransitionHostInstanceIdx = 0;
314 + const inViewport = applyViewTransitionToHostInstances(
315 + child.child,
316 + name,
317 + className,
318 + null,
319 + false,
320 + );
321 + if (!inViewport) {
322 + // This boundary is not in the viewport so we won't treat it as a matched pair.
323 + // Revert the transition names. This avoids it flying onto the screen which can
324 + // be disruptive and doesn't really preserve any continuity anyway.
325 + restoreViewTransitionOnHostInstances(child.child, false);
326 + } else {
327 + // We'll transition between them.
328 + const oldinstance: ViewTransitionState = child.stateNode;
329 + const newInstance: ViewTransitionState = pair;
330 + newInstance.paired = oldinstance;
331 + // Note: If the other side ends up outside the viewport, we'll still run this.
332 + // Therefore it's possible for onShare to be called with only an old snapshot.
333 + scheduleViewTransitionEvent(child, props.onShare);
334 + }
335 + }
336 + // Delete the entry so that we know when we've found all of them
337 + // and can stop searching (size reaches zero).
338 + pairs.delete(name);
339 + if (pairs.size === 0) {
340 + break;
341 + }
342 + }
343 + }
344 + }
345 + commitDeletedPairViewTransitions(child);
346 + }
347 + child = child.sibling;
348 + }
349 +}
350 +
351 +export function commitExitViewTransitions(deletion: Fiber): void {
352 + if (deletion.tag === ViewTransitionComponent) {
353 + const props: ViewTransitionProps = deletion.memoizedProps;
354 + const name = getViewTransitionName(props, deletion.stateNode);
355 + const pair =
356 + appearingViewTransitions !== null
357 + ? appearingViewTransitions.get(name)
358 + : undefined;
359 + const className: ?string = getViewTransitionClassName(
360 + props.className,
361 + pair !== undefined ? props.share : props.exit,
362 + );
363 + if (className !== 'none') {
364 + viewTransitionHostInstanceIdx = 0;
365 + const inViewport = applyViewTransitionToHostInstances(
366 + deletion.child,
367 + name,
368 + className,
369 + null,
370 + false,
371 + );
372 + if (!inViewport) {
373 + // Revert the transition names. This boundary is not in the viewport
374 + // so we won't bother animating it.
375 + restoreViewTransitionOnHostInstances(deletion.child, false);
376 + // TODO: Should we still visit the children in case a named one was in the viewport?
377 + } else if (pair !== undefined) {
378 + // We found a new appearing view transition with the same name as this deletion.
379 + // We'll transition between them instead of running the normal exit.
380 + const oldinstance: ViewTransitionState = deletion.stateNode;
381 + const newInstance: ViewTransitionState = pair;
382 + newInstance.paired = oldinstance;
383 + // Delete the entry so that we know when we've found all of them
384 + // and can stop searching (size reaches zero).
385 + // $FlowFixMe[incompatible-use]: Refined by the pair.
386 + appearingViewTransitions.delete(name);
387 + // Note: If the other side ends up outside the viewport, we'll still run this.
388 + // Therefore it's possible for onShare to be called with only an old snapshot.
389 + scheduleViewTransitionEvent(deletion, props.onShare);
390 + } else {
391 + scheduleViewTransitionEvent(deletion, props.onExit);
392 + }
393 + }
394 + if (appearingViewTransitions !== null) {
395 + // Look for more pairs deeper in the tree.
396 + commitDeletedPairViewTransitions(deletion);
397 + }
398 + } else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
399 + let child = deletion.child;
400 + while (child !== null) {
401 + commitExitViewTransitions(child);
402 + child = child.sibling;
403 + }
404 + } else {
405 + if (appearingViewTransitions !== null) {
406 + commitDeletedPairViewTransitions(deletion);
407 + }
408 + }
409 +}
410 +
411 +export function commitBeforeUpdateViewTransition(
412 + current: Fiber,
413 + finishedWork: Fiber,
414 +): void {
415 + // The way we deal with multiple HostInstances as children of a View Transition in an
416 + // update can get tricky. The important bit is that if you swap out n HostInstances
417 + // from n HostInstances then they match up in order. Similarly, if you don't swap
418 + // any HostInstances each instance just transitions as is.
419 + //
420 + // We call this function twice. First we apply the view transition names on the
421 + // "current" tree in the snapshot phase. Then in the mutation phase we apply view
422 + // transition names to the "finishedWork" tree.
423 + //
424 + // This means that if there were insertions or deletions before an updated Instance
425 + // that same Instance might get different names in the "old" and the "new" state.
426 + // For example if you swap two HostInstances inside a ViewTransition they don't
427 + // animate to swap position but rather cross-fade into the other instance. This might
428 + // be unexpected but it is in line with the semantics that the ViewTransition is its
429 + // own layer that cross-fades its content when it updates. If you want to reorder then
430 + // each child needs its own ViewTransition.
431 + const oldProps: ViewTransitionProps = current.memoizedProps;
432 + const oldName = getViewTransitionName(oldProps, current.stateNode);
433 + const newProps: ViewTransitionProps = finishedWork.memoizedProps;
434 + // This className applies only if there are fewer child DOM nodes than
435 + // before or if this update should've been cancelled but we ended up with
436 + // a parent animating so we need to animate the child too.
437 + // For example, if update="foo" layout="none" and it turns out this was
438 + // a layout only change, then the "foo" class will be applied even though
439 + // it was not actually an update. Which is a bug.
440 + let className: ?string = getViewTransitionClassName(
441 + newProps.className,
442 + newProps.update,
443 + );
444 + if (className === 'none') {
445 + className = getViewTransitionClassName(newProps.className, newProps.layout);
446 + if (className === 'none') {
447 + // If both update and layout are both "none" then we don't have to
448 + // apply a name. Since we won't animate this boundary.
449 + return;
450 + }
451 + }
452 + viewTransitionHostInstanceIdx = 0;
453 + applyViewTransitionToHostInstances(
454 + current.child,
455 + oldName,
456 + className,
457 + (current.memoizedState = []),
458 + true,
459 + );
460 +}
461 +
462 +export function commitNestedViewTransitions(changedParent: Fiber): void {
463 + let child = changedParent.child;
464 + while (child !== null) {
465 + if (child.tag === ViewTransitionComponent) {
466 + // In this case the outer ViewTransition component wins but if there
467 + // was an update through this component then the inner one wins.
468 + const props: ViewTransitionProps = child.memoizedProps;
469 + const name = getViewTransitionName(props, child.stateNode);
470 + const className: ?string = getViewTransitionClassName(
471 + props.className,
472 + props.layout,
473 + );
474 + if (className !== 'none') {
475 + viewTransitionHostInstanceIdx = 0;
476 + applyViewTransitionToHostInstances(
477 + child.child,
478 + name,
479 + className,
480 + (child.memoizedState = []),
481 + false,
482 + );
483 + }
484 + } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
485 + commitNestedViewTransitions(child);
486 + }
487 + child = child.sibling;
488 + }
489 +}
490 +
491 +function restorePairedViewTransitions(parent: Fiber): void {
492 + if ((parent.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
493 + // This has no named view transitions in its subtree.
494 + return;
495 + }
496 + let child = parent.child;
497 + while (child !== null) {
498 + if (child.tag === OffscreenComponent && child.memoizedState === null) {
499 + // This tree was already hidden so we skip it.
500 + } else {
501 + if (
502 + child.tag === ViewTransitionComponent &&
503 + (child.flags & ViewTransitionNamedStatic) !== NoFlags
504 + ) {
505 + const instance: ViewTransitionState = child.stateNode;
506 + if (instance.paired !== null) {
507 + instance.paired = null;
508 + restoreViewTransitionOnHostInstances(child.child, false);
509 + }
510 + }
511 + restorePairedViewTransitions(child);
512 + }
513 + child = child.sibling;
514 + }
515 +}
516 +
517 +export function restoreEnterViewTransitions(placement: Fiber): void {
518 + if (placement.tag === ViewTransitionComponent) {
519 + const instance: ViewTransitionState = placement.stateNode;
520 + instance.paired = null;
521 + restoreViewTransitionOnHostInstances(placement.child, false);
522 + restorePairedViewTransitions(placement);
523 + } else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
524 + let child = placement.child;
525 + while (child !== null) {
526 + restoreEnterViewTransitions(child);
527 + child = child.sibling;
528 + }
529 + } else {
530 + restorePairedViewTransitions(placement);
531 + }
532 +}
533 +
534 +export function restoreExitViewTransitions(deletion: Fiber): void {
535 + if (deletion.tag === ViewTransitionComponent) {
536 + const instance: ViewTransitionState = deletion.stateNode;
537 + instance.paired = null;
538 + restoreViewTransitionOnHostInstances(deletion.child, false);
539 + restorePairedViewTransitions(deletion);
540 + } else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
541 + let child = deletion.child;
542 + while (child !== null) {
543 + restoreExitViewTransitions(child);
544 + child = child.sibling;
545 + }
546 + } else {
547 + restorePairedViewTransitions(deletion);
548 + }
549 +}
550 +
551 +export function restoreUpdateViewTransition(
552 + current: Fiber,
553 + finishedWork: Fiber,
554 +): void {
555 + finishedWork.memoizedState = null;
556 + restoreViewTransitionOnHostInstances(current.child, true);
557 + restoreViewTransitionOnHostInstances(finishedWork.child, true);
558 +}
559 +
560 +export function restoreNestedViewTransitions(changedParent: Fiber): void {
561 + let child = changedParent.child;
562 + while (child !== null) {
563 + if (child.tag === ViewTransitionComponent) {
564 + child.memoizedState = null;
565 + restoreViewTransitionOnHostInstances(child.child, false);
566 + } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
567 + restoreNestedViewTransitions(child);
568 + }
569 + child = child.sibling;
570 + }
571 +}
572 +
573 +function cancelViewTransitionHostInstances(
574 + currentViewTransition: Fiber,
575 + child: null | Fiber,
576 + stopAtNestedViewTransitions: boolean,
577 +): void {
578 + if (!supportsMutation) {
579 + return;
580 + }
581 + while (child !== null) {
582 + if (child.tag === HostComponent) {
583 + const instance: Instance = child.stateNode;
584 + const oldName = getViewTransitionName(
585 + currentViewTransition.memoizedProps,
586 + currentViewTransition.stateNode,
587 + );
588 + if (viewTransitionCancelableChildren === null) {
589 + viewTransitionCancelableChildren = [];
590 + }
591 + viewTransitionCancelableChildren.push(
592 + instance,
593 + oldName,
594 + child.memoizedProps,
595 + );
596 + viewTransitionHostInstanceIdx++;
597 + } else if (
598 + child.tag === OffscreenComponent &&
599 + child.memoizedState !== null
600 + ) {
601 + // Skip any hidden subtrees. They were or are effectively not there.
602 + } else if (
603 + child.tag === ViewTransitionComponent &&
604 + stopAtNestedViewTransitions
605 + ) {
606 + // Skip any nested view transitions for updates since in that case the
607 + // inner most one is the one that handles the update.
608 + } else {
609 + cancelViewTransitionHostInstances(
610 + currentViewTransition,
611 + child.child,
612 + stopAtNestedViewTransitions,
613 + );
614 + }
615 + child = child.sibling;
616 + }
617 +}
618 +
619 +function measureViewTransitionHostInstances(
620 + currentViewTransition: Fiber,
621 + parentViewTransition: Fiber,
622 + child: null | Fiber,
623 + name: string,
624 + className: ?string,
625 + previousMeasurements: null | Array<InstanceMeasurement>,
626 + stopAtNestedViewTransitions: boolean,
627 +): boolean {
628 + if (!supportsMutation) {
629 + return true;
630 + }
631 + let inViewport = false;
632 + while (child !== null) {
633 + if (child.tag === HostComponent) {
634 + const instance: Instance = child.stateNode;
635 + if (
636 + previousMeasurements !== null &&
637 + viewTransitionHostInstanceIdx < previousMeasurements.length
638 + ) {
639 + // The previous measurement of the Instance in this location within the ViewTransition.
640 + // Note that this might not be the same exact Instance if the Instances within the
641 + // ViewTransition changed.
642 + const previousMeasurement =
643 + previousMeasurements[viewTransitionHostInstanceIdx];
644 + const nextMeasurement = measureInstance(instance);
645 + if (
646 + wasInstanceInViewport(previousMeasurement) ||
647 + wasInstanceInViewport(nextMeasurement)
648 + ) {
649 + // If either the old or new state was within the viewport we have to animate this.
650 + // But if it turns out that none of them were we'll be able to skip it.
651 + inViewport = true;
652 + }
653 + if (
654 + (parentViewTransition.flags & Update) === NoFlags &&
655 + hasInstanceChanged(previousMeasurement, nextMeasurement)
656 + ) {
657 + parentViewTransition.flags |= Update;
658 + }
659 + if (hasInstanceAffectedParent(previousMeasurement, nextMeasurement)) {
660 + // If this instance size within its parent has changed it might have caused the
661 + // parent to relayout which needs a cross fade.
662 + parentViewTransition.flags |= AffectedParentLayout;
663 + }
664 + } else {
665 + // If there was an insertion of extra nodes, we have to assume they affected the parent.
666 + // It should have already been marked as an Update due to the mutation.
667 + parentViewTransition.flags |= AffectedParentLayout;
668 + }
669 + if ((parentViewTransition.flags & Update) !== NoFlags) {
670 + // We might update this node so we need to apply its new name for the new state.
671 + applyViewTransitionName(
672 + instance,
673 + viewTransitionHostInstanceIdx === 0
674 + ? name
675 + : // If we have multiple Host Instances below, we add a suffix to the name to give
676 + // each one a unique name.
677 + name + '_' + viewTransitionHostInstanceIdx,
678 + className,
679 + );
680 + }
681 + if (!inViewport || (parentViewTransition.flags & Update) === NoFlags) {
682 + // It turns out that we had no other deeper mutations, the child transitions didn't
683 + // affect the parent layout and this instance hasn't changed size. So we can skip
684 + // animating it. However, in the current model this only works if the parent also
685 + // doesn't animate. So we have to queue these and wait until we complete the parent
686 + // to cancel them.
687 + const oldName = getViewTransitionName(
688 + currentViewTransition.memoizedProps,
689 + currentViewTransition.stateNode,
690 + );
691 + if (viewTransitionCancelableChildren === null) {
692 + viewTransitionCancelableChildren = [];
693 + }
694 + viewTransitionCancelableChildren.push(
695 + instance,
696 + oldName,
697 + child.memoizedProps,
698 + );
699 + }
700 + viewTransitionHostInstanceIdx++;
701 + } else if (
702 + child.tag === OffscreenComponent &&
703 + child.memoizedState !== null
704 + ) {
705 + // Skip any hidden subtrees. They were or are effectively not there.
706 + } else if (
707 + child.tag === ViewTransitionComponent &&
708 + stopAtNestedViewTransitions
709 + ) {
710 + // Skip any nested view transitions for updates since in that case the
711 + // inner most one is the one that handles the update.
712 + // If this inner boundary resized we need to bubble that information up.
713 + parentViewTransition.flags |= child.flags & AffectedParentLayout;
714 + } else {
715 + if (
716 + measureViewTransitionHostInstances(
717 + currentViewTransition,
718 + parentViewTransition,
719 + child.child,
720 + name,
721 + className,
722 + previousMeasurements,
723 + stopAtNestedViewTransitions,
724 + )
725 + ) {
726 + inViewport = true;
727 + }
728 + }
729 + child = child.sibling;
730 + }
731 + return inViewport;
732 +}
733 +
734 +export function measureUpdateViewTransition(
735 + current: Fiber,
736 + finishedWork: Fiber,
737 +): boolean {
738 + const props: ViewTransitionProps = finishedWork.memoizedProps;
739 + const updateClassName: ?string = getViewTransitionClassName(
740 + props.className,
741 + props.update,
742 + );
743 + const layoutClassName: ?string = getViewTransitionClassName(
744 + props.className,
745 + props.layout,
746 + );
747 + let className: ?string;
748 + if (updateClassName === 'none') {
749 + if (layoutClassName === 'none') {
750 + // If both update and layout class name were none, then we didn't apply any
751 + // names in the before update phase so we shouldn't now neither.
752 + return false;
753 + }
754 + // We don't care if this is mutated or children layout changed, but we still
755 + // measure each instance to see if it moved and therefore should apply layout.
756 + finishedWork.flags &= ~Update;
757 + className = layoutClassName;
758 + } else if ((finishedWork.flags & Update) !== NoFlags) {
759 + // It was updated and we have an appropriate class name to apply.
760 + className = updateClassName;
761 + } else {
762 + if (layoutClassName === 'none') {
763 + // If we did not update, then all changes are considered a layout. We'll
764 + // attempt to cancel.
765 + viewTransitionHostInstanceIdx = 0;
766 + cancelViewTransitionHostInstances(current, finishedWork.child, true);
767 + return false;
768 + }
769 + // We didn't update but we might still apply layout so we measure each
770 + // instance to see if it moved or resized.
771 + className = layoutClassName;
772 + }
773 + const name = getViewTransitionName(props, finishedWork.stateNode);
774 + // If nothing changed due to a mutation, or children changing size
775 + // and the measurements end up unchanged, we should restore it to not animate.
776 + viewTransitionHostInstanceIdx = 0;
777 + const previousMeasurements = current.memoizedState;
778 + const inViewport = measureViewTransitionHostInstances(
779 + current,
780 + finishedWork,
781 + finishedWork.child,
782 + name,
783 + className,
784 + previousMeasurements,
785 + true,
786 + );
787 + const previousCount =
788 + previousMeasurements === null ? 0 : previousMeasurements.length;
789 + if (viewTransitionHostInstanceIdx !== previousCount) {
790 + // If we found a different number of child DOM nodes we need to assume that
791 + // the parent layout may have changed as a result. This is not necessarily
792 + // true if those nodes were absolutely positioned.
793 + finishedWork.flags |= AffectedParentLayout;
794 + }
795 + return inViewport;
796 +}
797 +
798 +export function measureNestedViewTransitions(changedParent: Fiber): void {
799 + let child = changedParent.child;
800 + while (child !== null) {
801 + if (child.tag === ViewTransitionComponent) {
802 + const current = child.alternate;
803 + if (current !== null) {
804 + const props: ViewTransitionProps = child.memoizedProps;
805 + const name = getViewTransitionName(props, child.stateNode);
806 + const className: ?string = getViewTransitionClassName(
807 + props.className,
808 + props.layout,
809 + );
810 + viewTransitionHostInstanceIdx = 0;
811 + const inViewport = measureViewTransitionHostInstances(
812 + current,
813 + child,
814 + child.child,
815 + name,
816 + className,
817 + child.memoizedState,
818 + false,
819 + );
820 + if ((child.flags & Update) === NoFlags || !inViewport) {
821 + // Nothing changed.
822 + } else {
823 + scheduleViewTransitionEvent(child, props.onLayout);
824 + }
825 + }
826 + } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
827 + measureNestedViewTransitions(child);
828 + }
829 + child = child.sibling;
830 + }
831 +}
packages/react-reconciler/src/ReactFiberCommitWork.js
+29 -790
@@ -14,7 +14,6 @@ import type {
14 Container,
15 HoistableRoot,
16 FormInstance,
17 - InstanceMeasurement,
17 Props,
18 } from './ReactFiberConfig';
19 import type {Fiber, FiberRoot} from './ReactInternalTypes';
@@ -112,9 +111,7 @@ import {
111 PerformedWork,
112 ForceClientRender,
113 DidCapture,
115 - ViewTransitionStatic,
114 AffectedParentLayout,
117 - ViewTransitionNamedStatic,
115 } from './ReactFiberFlags';
116 import {
117 commitStartTime,
@@ -163,15 +160,9 @@ import {
160 suspendResource,
161 resetFormInstance,
162 registerSuspenseInstanceRetry,
166 - applyViewTransitionName,
167 - restoreViewTransitionName,
163 cancelViewTransitionName,
164 cancelRootViewTransitionName,
165 restoreRootViewTransitionName,
171 - measureInstance,
172 - hasInstanceChanged,
173 - hasInstanceAffectedParent,
174 - wasInstanceInViewport,
166 isSingletonScope,
167 } from './ReactFiberConfig';
168 import {
@@ -203,10 +194,6 @@ import {
194 OffscreenDetached,
195 OffscreenPassiveEffectsConnected,
196 } from './ReactFiberActivityComponent';
206 -import {
207 - getViewTransitionName,
208 - getViewTransitionClassName,
209 -} from './ReactFiberViewTransitionComponent';
197 import {
198 TransitionRoot,
199 TransitionTracingMarker,
@@ -249,6 +236,23 @@ import {
236 commitHostSingletonAcquisition,
237 commitHostSingletonRelease,
238 } from './ReactFiberCommitHostEffects';
239 +import {
240 + commitEnterViewTransitions,
241 + commitExitViewTransitions,
242 + commitBeforeUpdateViewTransition,
243 + commitNestedViewTransitions,
244 + restoreEnterViewTransitions,
245 + restoreExitViewTransitions,
246 + restoreUpdateViewTransition,
247 + restoreNestedViewTransitions,
248 + measureUpdateViewTransition,
249 + measureNestedViewTransitions,
250 + resetShouldStartViewTransition,
251 + resetAppearingViewTransitions,
252 + trackAppearingViewTransition,
253 + viewTransitionCancelableChildren,
254 + setViewTransitionCancelableChildren,
255 +} from './ReactFiberCommitViewTransitions';
256 import {
257 viewTransitionMutationContext,
258 pushMutationContext,
@@ -274,19 +278,9 @@ let inProgressRoot: FiberRoot | null = null;
278 let focusedInstanceHandle: null | Fiber = null;
279 export let shouldFireAfterActiveInstanceBlur: boolean = false;
280
277 -export let shouldStartViewTransition: boolean = false;
278 -
279 -// This tracks named ViewTransition components found in the accumulateSuspenseyCommit
280 -// phase that might need to find deleted pairs in the beforeMutation phase.
281 -let appearingViewTransitions: Map<string, ViewTransitionState> | null = null;
282 -
281 // Used during the commit phase to track whether a parent ViewTransition component
282 // might have been affected by any mutations / relayouts below.
283 let viewTransitionContextChanged: boolean = false;
286 -// We can't cancel view transition children until we know that their parent also
287 -// don't need to transition.
288 -let viewTransitionCancelableChildren: null | Array<Instance | string | Props> =
289 - null; // tupled array where each entry is [instance: Instance, oldName: string, props: Props]
284
285 export function commitBeforeMutationEffects(
286 root: FiberRoot,
@@ -295,7 +289,8 @@ export function commitBeforeMutationEffects(
289 ): void {
290 focusedInstanceHandle = prepareForCommit(root.containerInfo);
291 shouldFireAfterActiveInstanceBlur = false;
298 - shouldStartViewTransition = false;
292 +
293 + resetShouldStartViewTransition();
294
295 const isViewTransitionEligible =
296 enableViewTransition &&
@@ -307,7 +302,7 @@ export function commitBeforeMutationEffects(
302 // We no longer need to track the active instance fiber
303 focusedInstanceHandle = null;
304 // We've found any matched pairs and can now reset.
310 - appearingViewTransitions = null;
305 + resetAppearingViewTransitions();
306 }
307
308 function commitBeforeMutationEffects_begin(isViewTransitionEligible: boolean) {
@@ -542,759 +537,6 @@ function commitBeforeMutationEffectsDeletion(
537 }
538 }
539
545 -let viewTransitionHostInstanceIdx = 0;
546 -
547 -function applyViewTransitionToHostInstances(
548 - child: null | Fiber,
549 - name: string,
550 - className: ?string,
551 - collectMeasurements: null | Array<InstanceMeasurement>,
552 - stopAtNestedViewTransitions: boolean,
553 -): boolean {
554 - if (!supportsMutation) {
555 - return false;
556 - }
557 - let inViewport = false;
558 - while (child !== null) {
559 - if (child.tag === HostComponent) {
560 - shouldStartViewTransition = true;
561 - const instance: Instance = child.stateNode;
562 - if (collectMeasurements !== null) {
563 - const measurement = measureInstance(instance);
564 - collectMeasurements.push(measurement);
565 - if (wasInstanceInViewport(measurement)) {
566 - inViewport = true;
567 - }
568 - } else if (!inViewport) {
569 - if (wasInstanceInViewport(measureInstance(instance))) {
570 - inViewport = true;
571 - }
572 - }
573 - applyViewTransitionName(
574 - instance,
575 - viewTransitionHostInstanceIdx === 0
576 - ? name
577 - : // If we have multiple Host Instances below, we add a suffix to the name to give
578 - // each one a unique name.
579 - name + '_' + viewTransitionHostInstanceIdx,
580 - className,
581 - );
582 - viewTransitionHostInstanceIdx++;
583 - } else if (
584 - child.tag === OffscreenComponent &&
585 - child.memoizedState !== null
586 - ) {
587 - // Skip any hidden subtrees. They were or are effectively not there.
588 - } else if (
589 - child.tag === ViewTransitionComponent &&
590 - stopAtNestedViewTransitions
591 - ) {
592 - // Skip any nested view transitions for updates since in that case the
593 - // inner most one is the one that handles the update.
594 - } else {
595 - if (
596 - applyViewTransitionToHostInstances(
597 - child.child,
598 - name,
599 - className,
600 - collectMeasurements,
601 - stopAtNestedViewTransitions,
602 - )
603 - ) {
604 - inViewport = true;
605 - }
606 - }
607 - child = child.sibling;
608 - }
609 - return inViewport;
610 -}
611 -
612 -function restoreViewTransitionOnHostInstances(
613 - child: null | Fiber,
614 - stopAtNestedViewTransitions: boolean,
615 -): void {
616 - if (!supportsMutation) {
617 - return;
618 - }
619 - while (child !== null) {
620 - if (child.tag === HostComponent) {
621 - const instance: Instance = child.stateNode;
622 - restoreViewTransitionName(instance, child.memoizedProps);
623 - } else if (
624 - child.tag === OffscreenComponent &&
625 - child.memoizedState !== null
626 - ) {
627 - // Skip any hidden subtrees. They were or are effectively not there.
628 - } else if (
629 - child.tag === ViewTransitionComponent &&
630 - stopAtNestedViewTransitions
631 - ) {
632 - // Skip any nested view transitions for updates since in that case the
633 - // inner most one is the one that handles the update.
634 - } else {
635 - restoreViewTransitionOnHostInstances(
636 - child.child,
637 - stopAtNestedViewTransitions,
638 - );
639 - }
640 - child = child.sibling;
641 - }
642 -}
643 -
644 -function commitAppearingPairViewTransitions(placement: Fiber): void {
645 - if ((placement.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
646 - // This has no named view transitions in its subtree.
647 - return;
648 - }
649 - let child = placement.child;
650 - while (child !== null) {
651 - if (child.tag === OffscreenComponent && child.memoizedState === null) {
652 - // This tree was already hidden so we skip it.
653 - } else {
654 - commitAppearingPairViewTransitions(child);
655 - if (
656 - child.tag === ViewTransitionComponent &&
657 - (child.flags & ViewTransitionNamedStatic) !== NoFlags
658 - ) {
659 - const instance: ViewTransitionState = child.stateNode;
660 - if (instance.paired) {
661 - const props: ViewTransitionProps = child.memoizedProps;
662 - if (props.name == null || props.name === 'auto') {
663 - throw new Error(
664 - 'Found a pair with an auto name. This is a bug in React.',
665 - );
666 - }
667 - const name = props.name;
668 - const className: ?string = getViewTransitionClassName(
669 - props.className,
670 - props.share,
671 - );
672 - if (className !== 'none') {
673 - // We found a new appearing view transition with the same name as this deletion.
674 - // We'll transition between them.
675 - viewTransitionHostInstanceIdx = 0;
676 - const inViewport = applyViewTransitionToHostInstances(
677 - child.child,
678 - name,
679 - className,
680 - null,
681 - false,
682 - );
683 - if (!inViewport) {
684 - // This boundary is exiting within the viewport but is going to leave the viewport.
685 - // Instead, we treat this as an exit of the previous entry by reverting the new name.
686 - // Ideally we could undo the old transition but it's now too late. It's also on its
687 - // on snapshot. We have know was for it to paint onto the original group.
688 - // TODO: This will lead to things unexpectedly having exit animations that normally
689 - // wouldn't happen. Consider if we should just let this fly off the screen instead.
690 - restoreViewTransitionOnHostInstances(child.child, false);
691 - }
692 - }
693 - }
694 - }
695 - }
696 - child = child.sibling;
697 - }
698 -}
699 -
700 -function commitEnterViewTransitions(placement: Fiber): void {
701 - if (placement.tag === ViewTransitionComponent) {
702 - const state: ViewTransitionState = placement.stateNode;
703 - const props: ViewTransitionProps = placement.memoizedProps;
704 - const name = getViewTransitionName(props, state);
705 - const className: ?string = getViewTransitionClassName(
706 - props.className,
707 - state.paired ? props.share : props.enter,
708 - );
709 - if (className !== 'none') {
710 - viewTransitionHostInstanceIdx = 0;
711 - const inViewport = applyViewTransitionToHostInstances(
712 - placement.child,
713 - name,
714 - className,
715 - null,
716 - false,
717 - );
718 - if (!inViewport) {
719 - // TODO: If this was part of a pair we will still run the onShare callback.
720 - // Revert the transition names. This boundary is not in the viewport
721 - // so we won't bother animating it.
722 - restoreViewTransitionOnHostInstances(placement.child, false);
723 - // TODO: Should we still visit the children in case a named one was in the viewport?
724 - } else {
725 - commitAppearingPairViewTransitions(placement);
726 -
727 - if (!state.paired) {
728 - scheduleViewTransitionEvent(placement, props.onEnter);
729 - }
730 - }
731 - } else {
732 - commitAppearingPairViewTransitions(placement);
733 - }
734 - } else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
735 - let child = placement.child;
736 - while (child !== null) {
737 - commitEnterViewTransitions(child);
738 - child = child.sibling;
739 - }
740 - } else {
741 - commitAppearingPairViewTransitions(placement);
742 - }
743 -}
744 -
745 -function commitDeletedPairViewTransitions(deletion: Fiber): void {
746 - if (
747 - appearingViewTransitions === null ||
748 - appearingViewTransitions.size === 0
749 - ) {
750 - // We've found all.
751 - return;
752 - }
753 - const pairs = appearingViewTransitions;
754 - if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
755 - // This has no named view transitions in its subtree.
756 - return;
757 - }
758 - let child = deletion.child;
759 - while (child !== null) {
760 - if (child.tag === OffscreenComponent && child.memoizedState === null) {
761 - // This tree was already hidden so we skip it.
762 - } else {
763 - if (
764 - child.tag === ViewTransitionComponent &&
765 - (child.flags & ViewTransitionNamedStatic) !== NoFlags
766 - ) {
767 - const props: ViewTransitionProps = child.memoizedProps;
768 - const name = props.name;
769 - if (name != null && name !== 'auto') {
770 - const pair = pairs.get(name);
771 - if (pair !== undefined) {
772 - const className: ?string = getViewTransitionClassName(
773 - props.className,
774 - props.share,
775 - );
776 - if (className !== 'none') {
777 - // We found a new appearing view transition with the same name as this deletion.
778 - viewTransitionHostInstanceIdx = 0;
779 - const inViewport = applyViewTransitionToHostInstances(
780 - child.child,
781 - name,
782 - className,
783 - null,
784 - false,
785 - );
786 - if (!inViewport) {
787 - // This boundary is not in the viewport so we won't treat it as a matched pair.
788 - // Revert the transition names. This avoids it flying onto the screen which can
789 - // be disruptive and doesn't really preserve any continuity anyway.
790 - restoreViewTransitionOnHostInstances(child.child, false);
791 - } else {
792 - // We'll transition between them.
793 - const oldinstance: ViewTransitionState = child.stateNode;
794 - const newInstance: ViewTransitionState = pair;
795 - newInstance.paired = oldinstance;
796 - // Note: If the other side ends up outside the viewport, we'll still run this.
797 - // Therefore it's possible for onShare to be called with only an old snapshot.
798 - scheduleViewTransitionEvent(child, props.onShare);
799 - }
800 - }
801 - // Delete the entry so that we know when we've found all of them
802 - // and can stop searching (size reaches zero).
803 - pairs.delete(name);
804 - if (pairs.size === 0) {
805 - break;
806 - }
807 - }
808 - }
809 - }
810 - commitDeletedPairViewTransitions(child);
811 - }
812 - child = child.sibling;
813 - }
814 -}
815 -
816 -function commitExitViewTransitions(deletion: Fiber): void {
817 - if (deletion.tag === ViewTransitionComponent) {
818 - const props: ViewTransitionProps = deletion.memoizedProps;
819 - const name = getViewTransitionName(props, deletion.stateNode);
820 - const pair =
821 - appearingViewTransitions !== null
822 - ? appearingViewTransitions.get(name)
823 - : undefined;
824 - const className: ?string = getViewTransitionClassName(
825 - props.className,
826 - pair !== undefined ? props.share : props.exit,
827 - );
828 - if (className !== 'none') {
829 - viewTransitionHostInstanceIdx = 0;
830 - const inViewport = applyViewTransitionToHostInstances(
831 - deletion.child,
832 - name,
833 - className,
834 - null,
835 - false,
836 - );
837 - if (!inViewport) {
838 - // Revert the transition names. This boundary is not in the viewport
839 - // so we won't bother animating it.
840 - restoreViewTransitionOnHostInstances(deletion.child, false);
841 - // TODO: Should we still visit the children in case a named one was in the viewport?
842 - } else if (pair !== undefined) {
843 - // We found a new appearing view transition with the same name as this deletion.
844 - // We'll transition between them instead of running the normal exit.
845 - const oldinstance: ViewTransitionState = deletion.stateNode;
846 - const newInstance: ViewTransitionState = pair;
847 - newInstance.paired = oldinstance;
848 - // Delete the entry so that we know when we've found all of them
849 - // and can stop searching (size reaches zero).
850 - // $FlowFixMe[incompatible-use]: Refined by the pair.
851 - appearingViewTransitions.delete(name);
852 - // Note: If the other side ends up outside the viewport, we'll still run this.
853 - // Therefore it's possible for onShare to be called with only an old snapshot.
854 - scheduleViewTransitionEvent(deletion, props.onShare);
855 - } else {
856 - scheduleViewTransitionEvent(deletion, props.onExit);
857 - }
858 - }
859 - if (appearingViewTransitions !== null) {
860 - // Look for more pairs deeper in the tree.
861 - commitDeletedPairViewTransitions(deletion);
862 - }
863 - } else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
864 - let child = deletion.child;
865 - while (child !== null) {
866 - commitExitViewTransitions(child);
867 - child = child.sibling;
868 - }
869 - } else {
870 - if (appearingViewTransitions !== null) {
871 - commitDeletedPairViewTransitions(deletion);
872 - }
873 - }
874 -}
875 -
876 -function commitBeforeUpdateViewTransition(
877 - current: Fiber,
878 - finishedWork: Fiber,
879 -): void {
880 - // The way we deal with multiple HostInstances as children of a View Transition in an
881 - // update can get tricky. The important bit is that if you swap out n HostInstances
882 - // from n HostInstances then they match up in order. Similarly, if you don't swap
883 - // any HostInstances each instance just transitions as is.
884 - //
885 - // We call this function twice. First we apply the view transition names on the
886 - // "current" tree in the snapshot phase. Then in the mutation phase we apply view
887 - // transition names to the "finishedWork" tree.
888 - //
889 - // This means that if there were insertions or deletions before an updated Instance
890 - // that same Instance might get different names in the "old" and the "new" state.
891 - // For example if you swap two HostInstances inside a ViewTransition they don't
892 - // animate to swap position but rather cross-fade into the other instance. This might
893 - // be unexpected but it is in line with the semantics that the ViewTransition is its
894 - // own layer that cross-fades its content when it updates. If you want to reorder then
895 - // each child needs its own ViewTransition.
896 - const oldProps: ViewTransitionProps = current.memoizedProps;
897 - const oldName = getViewTransitionName(oldProps, current.stateNode);
898 - const newProps: ViewTransitionProps = finishedWork.memoizedProps;
899 - // This className applies only if there are fewer child DOM nodes than
900 - // before or if this update should've been cancelled but we ended up with
901 - // a parent animating so we need to animate the child too.
902 - // For example, if update="foo" layout="none" and it turns out this was
903 - // a layout only change, then the "foo" class will be applied even though
904 - // it was not actually an update. Which is a bug.
905 - let className: ?string = getViewTransitionClassName(
906 - newProps.className,
907 - newProps.update,
908 - );
909 - if (className === 'none') {
910 - className = getViewTransitionClassName(newProps.className, newProps.layout);
911 - if (className === 'none') {
912 - // If both update and layout are both "none" then we don't have to
913 - // apply a name. Since we won't animate this boundary.
914 - return;
915 - }
916 - }
917 - viewTransitionHostInstanceIdx = 0;
918 - applyViewTransitionToHostInstances(
919 - current.child,
920 - oldName,
921 - className,
922 - (current.memoizedState = []),
923 - true,
924 - );
925 -}
926 -
927 -function commitNestedViewTransitions(changedParent: Fiber): void {
928 - let child = changedParent.child;
929 - while (child !== null) {
930 - if (child.tag === ViewTransitionComponent) {
931 - // In this case the outer ViewTransition component wins but if there
932 - // was an update through this component then the inner one wins.
933 - const props: ViewTransitionProps = child.memoizedProps;
934 - const name = getViewTransitionName(props, child.stateNode);
935 - const className: ?string = getViewTransitionClassName(
936 - props.className,
937 - props.layout,
938 - );
939 - if (className !== 'none') {
940 - viewTransitionHostInstanceIdx = 0;
941 - applyViewTransitionToHostInstances(
942 - child.child,
943 - name,
944 - className,
945 - (child.memoizedState = []),
946 - false,
947 - );
948 - }
949 - } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
950 - commitNestedViewTransitions(child);
951 - }
952 - child = child.sibling;
953 - }
954 -}
955 -
956 -function restorePairedViewTransitions(parent: Fiber): void {
957 - if ((parent.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
958 - // This has no named view transitions in its subtree.
959 - return;
960 - }
961 - let child = parent.child;
962 - while (child !== null) {
963 - if (child.tag === OffscreenComponent && child.memoizedState === null) {
964 - // This tree was already hidden so we skip it.
965 - } else {
966 - if (
967 - child.tag === ViewTransitionComponent &&
968 - (child.flags & ViewTransitionNamedStatic) !== NoFlags
969 - ) {
970 - const instance: ViewTransitionState = child.stateNode;
971 - if (instance.paired !== null) {
972 - instance.paired = null;
973 - restoreViewTransitionOnHostInstances(child.child, false);
974 - }
975 - }
976 - restorePairedViewTransitions(child);
977 - }
978 - child = child.sibling;
979 - }
980 -}
981 -
982 -function restoreEnterViewTransitions(placement: Fiber): void {
983 - if (placement.tag === ViewTransitionComponent) {
984 - const instance: ViewTransitionState = placement.stateNode;
985 - instance.paired = null;
986 - restoreViewTransitionOnHostInstances(placement.child, false);
987 - restorePairedViewTransitions(placement);
988 - } else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
989 - let child = placement.child;
990 - while (child !== null) {
991 - restoreEnterViewTransitions(child);
992 - child = child.sibling;
993 - }
994 - } else {
995 - restorePairedViewTransitions(placement);
996 - }
997 -}
998 -
999 -function restoreExitViewTransitions(deletion: Fiber): void {
1000 - if (deletion.tag === ViewTransitionComponent) {
1001 - const instance: ViewTransitionState = deletion.stateNode;
1002 - instance.paired = null;
1003 - restoreViewTransitionOnHostInstances(deletion.child, false);
1004 - restorePairedViewTransitions(deletion);
1005 - } else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
1006 - let child = deletion.child;
1007 - while (child !== null) {
1008 - restoreExitViewTransitions(child);
1009 - child = child.sibling;
1010 - }
1011 - } else {
1012 - restorePairedViewTransitions(deletion);
1013 - }
1014 -}
1015 -
1016 -function restoreUpdateViewTransition(
1017 - current: Fiber,
1018 - finishedWork: Fiber,
1019 -): void {
1020 - finishedWork.memoizedState = null;
1021 - restoreViewTransitionOnHostInstances(current.child, true);
1022 - restoreViewTransitionOnHostInstances(finishedWork.child, true);
1023 -}
1024 -
1025 -function restoreNestedViewTransitions(changedParent: Fiber): void {
1026 - let child = changedParent.child;
1027 - while (child !== null) {
1028 - if (child.tag === ViewTransitionComponent) {
1029 - child.memoizedState = null;
1030 - restoreViewTransitionOnHostInstances(child.child, false);
1031 - } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
1032 - restoreNestedViewTransitions(child);
1033 - }
1034 - child = child.sibling;
1035 - }
1036 -}
1037 -
1038 -function cancelViewTransitionHostInstances(
1039 - currentViewTransition: Fiber,
1040 - child: null | Fiber,
1041 - stopAtNestedViewTransitions: boolean,
1042 -): void {
1043 - if (!supportsMutation) {
1044 - return;
1045 - }
1046 - while (child !== null) {
1047 - if (child.tag === HostComponent) {
1048 - const instance: Instance = child.stateNode;
1049 - const oldName = getViewTransitionName(
1050 - currentViewTransition.memoizedProps,
1051 - currentViewTransition.stateNode,
1052 - );
1053 - if (viewTransitionCancelableChildren === null) {
1054 - viewTransitionCancelableChildren = [];
1055 - }
1056 - viewTransitionCancelableChildren.push(
1057 - instance,
1058 - oldName,
1059 - child.memoizedProps,
1060 - );
1061 - viewTransitionHostInstanceIdx++;
1062 - } else if (
1063 - child.tag === OffscreenComponent &&
1064 - child.memoizedState !== null
1065 - ) {
1066 - // Skip any hidden subtrees. They were or are effectively not there.
1067 - } else if (
1068 - child.tag === ViewTransitionComponent &&
1069 - stopAtNestedViewTransitions
1070 - ) {
1071 - // Skip any nested view transitions for updates since in that case the
1072 - // inner most one is the one that handles the update.
1073 - } else {
1074 - cancelViewTransitionHostInstances(
1075 - currentViewTransition,
1076 - child.child,
1077 - stopAtNestedViewTransitions,
1078 - );
1079 - }
1080 - child = child.sibling;
1081 - }
1082 -}
1083 -
1084 -function measureViewTransitionHostInstances(
1085 - currentViewTransition: Fiber,
1086 - parentViewTransition: Fiber,
1087 - child: null | Fiber,
1088 - name: string,
1089 - className: ?string,
1090 - previousMeasurements: null | Array<InstanceMeasurement>,
1091 - stopAtNestedViewTransitions: boolean,
1092 -): boolean {
1093 - if (!supportsMutation) {
1094 - return true;
1095 - }
1096 - let inViewport = false;
1097 - while (child !== null) {
1098 - if (child.tag === HostComponent) {
1099 - const instance: Instance = child.stateNode;
1100 - if (
1101 - previousMeasurements !== null &&
1102 - viewTransitionHostInstanceIdx < previousMeasurements.length
1103 - ) {
1104 - // The previous measurement of the Instance in this location within the ViewTransition.
1105 - // Note that this might not be the same exact Instance if the Instances within the
1106 - // ViewTransition changed.
1107 - const previousMeasurement =
1108 - previousMeasurements[viewTransitionHostInstanceIdx];
1109 - const nextMeasurement = measureInstance(instance);
1110 - if (
1111 - wasInstanceInViewport(previousMeasurement) ||
1112 - wasInstanceInViewport(nextMeasurement)
1113 - ) {
1114 - // If either the old or new state was within the viewport we have to animate this.
1115 - // But if it turns out that none of them were we'll be able to skip it.
1116 - inViewport = true;
1117 - }
1118 - if (
1119 - (parentViewTransition.flags & Update) === NoFlags &&
1120 - hasInstanceChanged(previousMeasurement, nextMeasurement)
1121 - ) {
1122 - parentViewTransition.flags |= Update;
1123 - }
1124 - if (hasInstanceAffectedParent(previousMeasurement, nextMeasurement)) {
1125 - // If this instance size within its parent has changed it might have caused the
1126 - // parent to relayout which needs a cross fade.
1127 - parentViewTransition.flags |= AffectedParentLayout;
1128 - }
1129 - } else {
1130 - // If there was an insertion of extra nodes, we have to assume they affected the parent.
1131 - // It should have already been marked as an Update due to the mutation.
1132 - parentViewTransition.flags |= AffectedParentLayout;
1133 - }
1134 - if ((parentViewTransition.flags & Update) !== NoFlags) {
1135 - // We might update this node so we need to apply its new name for the new state.
1136 - applyViewTransitionName(
1137 - instance,
1138 - viewTransitionHostInstanceIdx === 0
1139 - ? name
1140 - : // If we have multiple Host Instances below, we add a suffix to the name to give
1141 - // each one a unique name.
1142 - name + '_' + viewTransitionHostInstanceIdx,
1143 - className,
1144 - );
1145 - }
1146 - if (!inViewport || (parentViewTransition.flags & Update) === NoFlags) {
1147 - // It turns out that we had no other deeper mutations, the child transitions didn't
1148 - // affect the parent layout and this instance hasn't changed size. So we can skip
1149 - // animating it. However, in the current model this only works if the parent also
1150 - // doesn't animate. So we have to queue these and wait until we complete the parent
1151 - // to cancel them.
1152 - const oldName = getViewTransitionName(
1153 - currentViewTransition.memoizedProps,
1154 - currentViewTransition.stateNode,
1155 - );
1156 - if (viewTransitionCancelableChildren === null) {
1157 - viewTransitionCancelableChildren = [];
1158 - }
1159 - viewTransitionCancelableChildren.push(
1160 - instance,
1161 - oldName,
1162 - child.memoizedProps,
1163 - );
1164 - }
1165 - viewTransitionHostInstanceIdx++;
1166 - } else if (
1167 - child.tag === OffscreenComponent &&
1168 - child.memoizedState !== null
1169 - ) {
1170 - // Skip any hidden subtrees. They were or are effectively not there.
1171 - } else if (
1172 - child.tag === ViewTransitionComponent &&
1173 - stopAtNestedViewTransitions
1174 - ) {
1175 - // Skip any nested view transitions for updates since in that case the
1176 - // inner most one is the one that handles the update.
1177 - // If this inner boundary resized we need to bubble that information up.
1178 - parentViewTransition.flags |= child.flags & AffectedParentLayout;
1179 - } else {
1180 - if (
1181 - measureViewTransitionHostInstances(
1182 - currentViewTransition,
1183 - parentViewTransition,
1184 - child.child,
1185 - name,
1186 - className,
1187 - previousMeasurements,
1188 - stopAtNestedViewTransitions,
1189 - )
1190 - ) {
1191 - inViewport = true;
1192 - }
1193 - }
1194 - child = child.sibling;
1195 - }
1196 - return inViewport;
1197 -}
1198 -
1199 -function measureUpdateViewTransition(
1200 - current: Fiber,
1201 - finishedWork: Fiber,
1202 -): boolean {
1203 - const props: ViewTransitionProps = finishedWork.memoizedProps;
1204 - const updateClassName: ?string = getViewTransitionClassName(
1205 - props.className,
1206 - props.update,
1207 - );
1208 - const layoutClassName: ?string = getViewTransitionClassName(
1209 - props.className,
1210 - props.layout,
1211 - );
1212 - let className: ?string;
1213 - if (updateClassName === 'none') {
1214 - if (layoutClassName === 'none') {
1215 - // If both update and layout class name were none, then we didn't apply any
1216 - // names in the before update phase so we shouldn't now neither.
1217 - return false;
1218 - }
1219 - // We don't care if this is mutated or children layout changed, but we still
1220 - // measure each instance to see if it moved and therefore should apply layout.
1221 - finishedWork.flags &= ~Update;
1222 - className = layoutClassName;
1223 - } else if ((finishedWork.flags & Update) !== NoFlags) {
1224 - // It was updated and we have an appropriate class name to apply.
1225 - className = updateClassName;
1226 - } else {
1227 - if (layoutClassName === 'none') {
1228 - // If we did not update, then all changes are considered a layout. We'll
1229 - // attempt to cancel.
1230 - viewTransitionHostInstanceIdx = 0;
1231 - cancelViewTransitionHostInstances(current, finishedWork.child, true);
1232 - return false;
1233 - }
1234 - // We didn't update but we might still apply layout so we measure each
1235 - // instance to see if it moved or resized.
1236 - className = layoutClassName;
1237 - }
1238 - const name = getViewTransitionName(props, finishedWork.stateNode);
1239 - // If nothing changed due to a mutation, or children changing size
1240 - // and the measurements end up unchanged, we should restore it to not animate.
1241 - viewTransitionHostInstanceIdx = 0;
1242 - const previousMeasurements = current.memoizedState;
1243 - const inViewport = measureViewTransitionHostInstances(
1244 - current,
1245 - finishedWork,
1246 - finishedWork.child,
1247 - name,
1248 - className,
1249 - previousMeasurements,
1250 - true,
1251 - );
1252 - const previousCount =
1253 - previousMeasurements === null ? 0 : previousMeasurements.length;
1254 - if (viewTransitionHostInstanceIdx !== previousCount) {
1255 - // If we found a different number of child DOM nodes we need to assume that
1256 - // the parent layout may have changed as a result. This is not necessarily
1257 - // true if those nodes were absolutely positioned.
1258 - finishedWork.flags |= AffectedParentLayout;
1259 - }
1260 - return inViewport;
1261 -}
1262 -
1263 -function measureNestedViewTransitions(changedParent: Fiber): void {
1264 - let child = changedParent.child;
1265 - while (child !== null) {
1266 - if (child.tag === ViewTransitionComponent) {
1267 - const current = child.alternate;
1268 - if (current !== null) {
1269 - const props: ViewTransitionProps = child.memoizedProps;
1270 - const name = getViewTransitionName(props, child.stateNode);
1271 - const className: ?string = getViewTransitionClassName(
1272 - props.className,
1273 - props.layout,
1274 - );
1275 - viewTransitionHostInstanceIdx = 0;
1276 - const inViewport = measureViewTransitionHostInstances(
1277 - current,
1278 - child,
1279 - child.child,
1280 - name,
1281 - className,
1282 - child.memoizedState,
1283 - false,
1284 - );
1285 - if ((child.flags & Update) === NoFlags || !inViewport) {
1286 - // Nothing changed.
1287 - } else {
1288 - scheduleViewTransitionEvent(child, props.onLayout);
1289 - }
1290 - }
1291 - } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
1292 - measureNestedViewTransitions(child);
1293 - }
1294 - child = child.sibling;
1295 - }
1296 -}
1297 -
540 function commitLayoutEffectOnFiber(
541 finishedRoot: FiberRoot,
542 current: Fiber | null,
@@ -3204,14 +2446,14 @@ function commitAfterMutationEffectsOnFiber(
2446 switch (finishedWork.tag) {
2447 case HostRoot: {
2448 viewTransitionContextChanged = false;
3207 - viewTransitionCancelableChildren = null;
2449 + setViewTransitionCancelableChildren(null);
2450 recursivelyTraverseAfterMutationEffects(root, finishedWork, lanes);
2451 if (!viewTransitionContextChanged) {
2452 // If we didn't leak any resizing out to the root, we don't have to transition
2453 // the root itself. This means that we can now safely cancel any cancellations
2454 // that bubbled all the way up.
2455 const cancelableChildren = viewTransitionCancelableChildren;
3214 - viewTransitionCancelableChildren = null;
2456 + setViewTransitionCancelableChildren(null);
2457 if (cancelableChildren !== null) {
2458 for (let i = 0; i < cancelableChildren.length; i += 3) {
2459 cancelViewTransitionName(
@@ -3264,7 +2506,7 @@ function commitAfterMutationEffectsOnFiber(
2506 const prevContextChanged = viewTransitionContextChanged;
2507 const prevCancelableChildren = viewTransitionCancelableChildren;
2508 viewTransitionContextChanged = false;
3267 - viewTransitionCancelableChildren = null;
2509 + setViewTransitionCancelableChildren(null);
2510 recursivelyTraverseAfterMutationEffects(root, finishedWork, lanes);
2511
2512 if (viewTransitionContextChanged) {
@@ -3287,7 +2529,7 @@ function commitAfterMutationEffectsOnFiber(
2529 prevCancelableChildren,
2530 viewTransitionCancelableChildren,
2531 );
3290 - viewTransitionCancelableChildren = prevCancelableChildren;
2532 + setViewTransitionCancelableChildren(prevCancelableChildren);
2533 }
2534 // TODO: If this doesn't end up canceled, because a parent animates,
2535 // then we should probably issue an event since this instance is part of it.
@@ -3301,7 +2543,7 @@ function commitAfterMutationEffectsOnFiber(
2543 );
2544
2545 // If this boundary did update, we cannot cancel its children so those are dropped.
3304 - viewTransitionCancelableChildren = prevCancelableChildren;
2546 + setViewTransitionCancelableChildren(prevCancelableChildren);
2547 }
2548
2549 if ((finishedWork.flags & AffectedParentLayout) !== NoFlags) {
@@ -4810,7 +4052,7 @@ export function commitPassiveUnmountEffects(finishedWork: Fiber): void {
4052 // pairs.
4053 let suspenseyCommitFlag = ShouldSuspendCommit;
4054 export function accumulateSuspenseyCommit(finishedWork: Fiber): void {
4813 - appearingViewTransitions = null;
4055 + resetAppearingViewTransitions();
4056 accumulateSuspenseyCommitOnFiber(finishedWork);
4057 }
4058
@@ -4897,14 +4139,11 @@ function accumulateSuspenseyCommitOnFiber(fiber: Fiber) {
4139 if (name != null && name !== 'auto') {
4140 // This is a named ViewTransition being mounted or reappearing. Let's add it to
4141 // the map so we can match it with deletions later.
4900 - if (appearingViewTransitions === null) {
4901 - appearingViewTransitions = new Map();
4902 - }
4142 + const state: ViewTransitionState = fiber.stateNode;
4143 // Reset the pair in case we didn't end up restoring the instance in previous commits.
4144 // This shouldn't really happen anymore but just in case. We could maybe add an invariant.
4905 - const instance: ViewTransitionState = fiber.stateNode;
4906 - instance.paired = null;
4907 - appearingViewTransitions.set(name, instance);
4145 + state.paired = null;
4146 + trackAppearingViewTransition(name, state);
4147 }
4148 }
4149 recursivelyAccumulateSuspenseyCommit(fiber);
packages/react-reconciler/src/ReactFiberWorkLoop.js
+1 -1
@@ -226,8 +226,8 @@ import {
226 invokeLayoutEffectUnmountInDEV,
227 invokePassiveEffectUnmountInDEV,
228 accumulateSuspenseyCommit,
229 - shouldStartViewTransition,
229 } from './ReactFiberCommitWork';
230 +import {shouldStartViewTransition} from './ReactFiberCommitViewTransitions';
231 import {enqueueUpdate} from './ReactFiberClassUpdateQueue';
232 import {resetContextDependencies} from './ReactFiberNewContext';
233 import {