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 {