203
OffscreenDetached,
204
OffscreenPassiveEffectsConnected,
205
} from './ReactFiberActivityComponent';
206
-import {getViewTransitionName} from './ReactFiberViewTransitionComponent';
206
+import {
207
+ getViewTransitionName,
208
+ getViewTransitionClassName,
209
+} from './ReactFiberViewTransitionComponent';
210
import {
211
TransitionRoot,
212
TransitionTracingMarker,
507
// We should just stash the parent ViewTransitionComponent and continue
508
// walking the tree until we find HostComponent but to do that we need
509
// to use a stack which requires refactoring this phase.
507
- commitBeforeUpdateViewTransition(current);
510
+ commitBeforeUpdateViewTransition(current, finishedWork);
511
}
512
}
513
break;
666
'Found a pair with an auto name. This is a bug in React.',
667
);
668
}
666
- // We found a new appearing view transition with the same name as this deletion.
667
- // We'll transition between them.
668
- viewTransitionHostInstanceIdx = 0;
669
- const inViewport = applyViewTransitionToHostInstances(
670
- child.child,
671
- props.name,
669
+ const name = props.name;
670
+ const className: ?string = getViewTransitionClassName(
671
props.className,
673
- null,
674
- false,
672
+ props.share,
673
);
676
- if (!inViewport) {
677
- // This boundary is exiting within the viewport but is going to leave the viewport.
678
- // Instead, we treat this as an exit of the previous entry by reverting the new name.
679
- // Ideally we could undo the old transition but it's now too late. It's also on its
680
- // on snapshot. We have know was for it to paint onto the original group.
681
- // TODO: This will lead to things unexpectedly having exit animations that normally
682
- // wouldn't happen. Consider if we should just let this fly off the screen instead.
683
- restoreViewTransitionOnHostInstances(child.child, false);
674
+ if (className !== 'none') {
675
+ // We found a new appearing view transition with the same name as this deletion.
676
+ // We'll transition between them.
677
+ viewTransitionHostInstanceIdx = 0;
678
+ const inViewport = applyViewTransitionToHostInstances(
679
+ child.child,
680
+ name,
681
+ className,
682
+ null,
683
+ false,
684
+ );
685
+ if (!inViewport) {
686
+ // This boundary is exiting within the viewport but is going to leave the viewport.
687
+ // Instead, we treat this as an exit of the previous entry by reverting the new name.
688
+ // Ideally we could undo the old transition but it's now too late. It's also on its
689
+ // on snapshot. We have know was for it to paint onto the original group.
690
+ // TODO: This will lead to things unexpectedly having exit animations that normally
691
+ // wouldn't happen. Consider if we should just let this fly off the screen instead.
692
+ restoreViewTransitionOnHostInstances(child.child, false);
693
+ }
694
}
695
}
696
}
701
702
function commitEnterViewTransitions(placement: Fiber): void {
703
if (placement.tag === ViewTransitionComponent) {
704
+ const state: ViewTransitionState = placement.stateNode;
705
const props: ViewTransitionProps = placement.memoizedProps;
695
- const name = getViewTransitionName(props, placement.stateNode);
696
- viewTransitionHostInstanceIdx = 0;
697
- const inViewport = applyViewTransitionToHostInstances(
698
- placement.child,
699
- name,
706
+ const name = getViewTransitionName(props, state);
707
+ const className: ?string = getViewTransitionClassName(
708
props.className,
701
- null,
702
- false,
709
+ state.paired ? props.share : props.enter,
710
);
704
- if (!inViewport) {
705
- // TODO: If this was part of a pair we will still run the onShare callback.
706
- // Revert the transition names. This boundary is not in the viewport
707
- // so we won't bother animating it.
708
- restoreViewTransitionOnHostInstances(placement.child, false);
709
- // TODO: Should we still visit the children in case a named one was in the viewport?
710
- } else {
711
- commitAppearingPairViewTransitions(placement);
711
+ if (className !== 'none') {
712
+ viewTransitionHostInstanceIdx = 0;
713
+ const inViewport = applyViewTransitionToHostInstances(
714
+ placement.child,
715
+ name,
716
+ className,
717
+ null,
718
+ false,
719
+ );
720
+ if (!inViewport) {
721
+ // TODO: If this was part of a pair we will still run the onShare callback.
722
+ // Revert the transition names. This boundary is not in the viewport
723
+ // so we won't bother animating it.
724
+ restoreViewTransitionOnHostInstances(placement.child, false);
725
+ // TODO: Should we still visit the children in case a named one was in the viewport?
726
+ } else {
727
+ commitAppearingPairViewTransitions(placement);
728
713
- const state: ViewTransitionState = placement.stateNode;
714
- if (!state.paired) {
715
- scheduleViewTransitionEvent(placement, props.onEnter);
729
+ if (!state.paired) {
730
+ scheduleViewTransitionEvent(placement, props.onEnter);
731
+ }
732
}
733
+ } else {
734
+ commitAppearingPairViewTransitions(placement);
735
}
736
} else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
737
let child = placement.child;
770
if (name != null && name !== 'auto') {
771
const pair = appearingViewTransitions.get(name);
772
if (pair !== undefined) {
755
- // We found a new appearing view transition with the same name as this deletion.
756
- viewTransitionHostInstanceIdx = 0;
757
- const inViewport = applyViewTransitionToHostInstances(
758
- child.child,
759
- name,
773
+ const className: ?string = getViewTransitionClassName(
774
props.className,
761
- null,
762
- false,
775
+ props.share,
776
);
764
- if (!inViewport) {
765
- // This boundary is not in the viewport so we won't treat it as a matched pair.
766
- // Revert the transition names. This avoids it flying onto the screen which can
767
- // be disruptive and doesn't really preserve any continuity anyway.
768
- restoreViewTransitionOnHostInstances(child.child, false);
769
- } else {
770
- // We'll transition between them.
771
- const oldinstance: ViewTransitionState = child.stateNode;
772
- const newInstance: ViewTransitionState = pair;
773
- newInstance.paired = oldinstance;
774
- // Note: If the other side ends up outside the viewport, we'll still run this.
775
- // Therefore it's possible for onShare to be called with only an old snapshot.
776
- scheduleViewTransitionEvent(child, props.onShare);
777
+ if (className !== 'none') {
778
+ // We found a new appearing view transition with the same name as this deletion.
779
+ viewTransitionHostInstanceIdx = 0;
780
+ const inViewport = applyViewTransitionToHostInstances(
781
+ child.child,
782
+ name,
783
+ className,
784
+ null,
785
+ false,
786
+ );
787
+ if (!inViewport) {
788
+ // This boundary is not in the viewport so we won't treat it as a matched pair.
789
+ // Revert the transition names. This avoids it flying onto the screen which can
790
+ // be disruptive and doesn't really preserve any continuity anyway.
791
+ restoreViewTransitionOnHostInstances(child.child, false);
792
+ } else {
793
+ // We'll transition between them.
794
+ const oldinstance: ViewTransitionState = child.stateNode;
795
+ const newInstance: ViewTransitionState = pair;
796
+ newInstance.paired = oldinstance;
797
+ // Note: If the other side ends up outside the viewport, we'll still run this.
798
+ // Therefore it's possible for onShare to be called with only an old snapshot.
799
+ scheduleViewTransitionEvent(child, props.onShare);
800
+ }
801
}
802
// Delete the entry so that we know when we've found all of them
803
// and can stop searching (size reaches zero).
821
if (deletion.tag === ViewTransitionComponent) {
822
const props: ViewTransitionProps = deletion.memoizedProps;
823
const name = getViewTransitionName(props, deletion.stateNode);
800
- viewTransitionHostInstanceIdx = 0;
801
- const inViewport = applyViewTransitionToHostInstances(
802
- deletion.child,
803
- name,
824
+ const pair =
825
+ appearingViewTransitions !== null
826
+ ? appearingViewTransitions.get(name)
827
+ : undefined;
828
+ const className: ?string = getViewTransitionClassName(
829
props.className,
805
- null,
806
- false,
830
+ pair !== undefined ? props.share : props.exit,
831
);
808
- if (!inViewport) {
809
- // Revert the transition names. This boundary is not in the viewport
810
- // so we won't bother animating it.
811
- restoreViewTransitionOnHostInstances(deletion.child, false);
812
- // TODO: Should we still visit the children in case a named one was in the viewport?
813
- } else if (appearingViewTransitions !== null) {
814
- const pair = appearingViewTransitions.get(name);
815
- if (pair !== undefined) {
832
+ if (className !== 'none') {
833
+ viewTransitionHostInstanceIdx = 0;
834
+ const inViewport = applyViewTransitionToHostInstances(
835
+ deletion.child,
836
+ name,
837
+ className,
838
+ null,
839
+ false,
840
+ );
841
+ if (!inViewport) {
842
+ // Revert the transition names. This boundary is not in the viewport
843
+ // so we won't bother animating it.
844
+ restoreViewTransitionOnHostInstances(deletion.child, false);
845
+ // TODO: Should we still visit the children in case a named one was in the viewport?
846
+ } else if (pair !== undefined) {
847
// We found a new appearing view transition with the same name as this deletion.
848
// We'll transition between them instead of running the normal exit.
849
const oldinstance: ViewTransitionState = deletion.stateNode;
851
newInstance.paired = oldinstance;
852
// Delete the entry so that we know when we've found all of them
853
// and can stop searching (size reaches zero).
854
+ // $FlowFixMe[incompatible-use]: Refined by the pair.
855
appearingViewTransitions.delete(name);
856
// Note: If the other side ends up outside the viewport, we'll still run this.
857
// Therefore it's possible for onShare to be called with only an old snapshot.
859
} else {
860
scheduleViewTransitionEvent(deletion, props.onExit);
861
}
862
+ }
863
+ if (appearingViewTransitions !== null) {
864
// Look for more pairs deeper in the tree.
865
commitDeletedPairViewTransitions(deletion, appearingViewTransitions);
832
- } else {
833
- scheduleViewTransitionEvent(deletion, props.onExit);
866
}
867
} else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
868
let child = deletion.child;
877
}
878
}
879
848
-function commitBeforeUpdateViewTransition(current: Fiber): void {
880
+function commitBeforeUpdateViewTransition(
881
+ current: Fiber,
882
+ finishedWork: Fiber,
883
+): void {
884
// The way we deal with multiple HostInstances as children of a View Transition in an
885
// update can get tricky. The important bit is that if you swap out n HostInstances
886
// from n HostInstances then they match up in order. Similarly, if you don't swap
897
// be unexpected but it is in line with the semantics that the ViewTransition is its
898
// own layer that cross-fades its content when it updates. If you want to reorder then
899
// each child needs its own ViewTransition.
865
- const props: ViewTransitionProps = current.memoizedProps;
866
- const name = getViewTransitionName(props, current.stateNode);
900
+ const oldProps: ViewTransitionProps = current.memoizedProps;
901
+ const oldName = getViewTransitionName(oldProps, current.stateNode);
902
+ const newProps: ViewTransitionProps = finishedWork.memoizedProps;
903
+ // This className applies only if there are fewer child DOM nodes than
904
+ // before or if this update should've been cancelled but we ended up with
905
+ // a parent animating so we need to animate the child too.
906
+ // For example, if update="foo" layout="none" and it turns out this was
907
+ // a layout only change, then the "foo" class will be applied even though
908
+ // it was not actually an update. Which is a bug.
909
+ let className: ?string = getViewTransitionClassName(
910
+ newProps.className,
911
+ newProps.update,
912
+ );
913
+ if (className === 'none') {
914
+ className = getViewTransitionClassName(newProps.className, newProps.layout);
915
+ if (className === 'none') {
916
+ // If both update and layout are both "none" then we don't have to
917
+ // apply a name. Since we won't animate this boundary.
918
+ return;
919
+ }
920
+ }
921
viewTransitionHostInstanceIdx = 0;
922
applyViewTransitionToHostInstances(
923
current.child,
870
- name,
871
- props.className,
924
+ oldName,
925
+ className,
926
(current.memoizedState = []),
927
true,
928
);
936
// was an update through this component then the inner one wins.
937
const props: ViewTransitionProps = child.memoizedProps;
938
const name = getViewTransitionName(props, child.stateNode);
885
- viewTransitionHostInstanceIdx = 0;
886
- applyViewTransitionToHostInstances(
887
- child.child,
888
- name,
939
+ const className: ?string = getViewTransitionClassName(
940
props.className,
890
- (child.memoizedState = []),
891
- false,
941
+ props.layout,
942
);
943
+ if (className !== 'none') {
944
+ viewTransitionHostInstanceIdx = 0;
945
+ applyViewTransitionToHostInstances(
946
+ child.child,
947
+ name,
948
+ className,
949
+ (child.memoizedState = []),
950
+ false,
951
+ );
952
+ }
953
} else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
954
commitNestedViewTransitions(child);
955
}
1039
}
1040
}
1041
1042
+function cancelViewTransitionHostInstances(
1043
+ currentViewTransition: Fiber,
1044
+ child: null | Fiber,
1045
+ stopAtNestedViewTransitions: boolean,
1046
+): void {
1047
+ if (!supportsMutation) {
1048
+ return;
1049
+ }
1050
+ while (child !== null) {
1051
+ if (child.tag === HostComponent) {
1052
+ const instance: Instance = child.stateNode;
1053
+ const oldName = getViewTransitionName(
1054
+ currentViewTransition.memoizedProps,
1055
+ currentViewTransition.stateNode,
1056
+ );
1057
+ if (viewTransitionCancelableChildren === null) {
1058
+ viewTransitionCancelableChildren = [];
1059
+ }
1060
+ viewTransitionCancelableChildren.push(
1061
+ instance,
1062
+ oldName,
1063
+ child.memoizedProps,
1064
+ );
1065
+ viewTransitionHostInstanceIdx++;
1066
+ } else if (
1067
+ child.tag === OffscreenComponent &&
1068
+ child.memoizedState !== null
1069
+ ) {
1070
+ // Skip any hidden subtrees. They were or are effectively not there.
1071
+ } else if (
1072
+ child.tag === ViewTransitionComponent &&
1073
+ stopAtNestedViewTransitions
1074
+ ) {
1075
+ // Skip any nested view transitions for updates since in that case the
1076
+ // inner most one is the one that handles the update.
1077
+ } else {
1078
+ cancelViewTransitionHostInstances(
1079
+ currentViewTransition,
1080
+ child.child,
1081
+ stopAtNestedViewTransitions,
1082
+ );
1083
+ }
1084
+ child = child.sibling;
1085
+ }
1086
+}
1087
+
1088
function measureViewTransitionHostInstances(
1089
currentViewTransition: Fiber,
1090
parentViewTransition: Fiber,
1091
child: null | Fiber,
1092
+ name: string,
1093
+ className: ?string,
1094
previousMeasurements: null | Array<InstanceMeasurement>,
1095
stopAtNestedViewTransitions: boolean,
1096
): boolean {
1136
parentViewTransition.flags |= AffectedParentLayout;
1137
}
1138
if ((parentViewTransition.flags & Update) !== NoFlags) {
1031
- const props: ViewTransitionProps = parentViewTransition.memoizedProps;
1139
// We might update this node so we need to apply its new name for the new state.
1033
- const newName = getViewTransitionName(
1034
- props,
1035
- parentViewTransition.stateNode,
1036
- );
1140
applyViewTransitionName(
1141
instance,
1142
viewTransitionHostInstanceIdx === 0
1040
- ? newName
1143
+ ? name
1144
: // If we have multiple Host Instances below, we add a suffix to the name to give
1145
// each one a unique name.
1043
- newName + '_' + viewTransitionHostInstanceIdx,
1044
- props.className,
1146
+ name + '_' + viewTransitionHostInstanceIdx,
1147
+ className,
1148
);
1149
}
1150
if (!inViewport || (parentViewTransition.flags & Update) === NoFlags) {
1186
currentViewTransition,
1187
parentViewTransition,
1188
child.child,
1189
+ name,
1190
+ className,
1191
previousMeasurements,
1192
stopAtNestedViewTransitions,
1193
)
1204
current: Fiber,
1205
finishedWork: Fiber,
1206
): boolean {
1207
+ const props: ViewTransitionProps = finishedWork.memoizedProps;
1208
+ const updateClassName: ?string = getViewTransitionClassName(
1209
+ props.className,
1210
+ props.update,
1211
+ );
1212
+ const layoutClassName: ?string = getViewTransitionClassName(
1213
+ props.className,
1214
+ props.update,
1215
+ );
1216
+ let className: ?string;
1217
+ if (updateClassName === 'none') {
1218
+ if (layoutClassName === 'none') {
1219
+ // If both update and layout class name were none, then we didn't apply any
1220
+ // names in the before update phase so we shouldn't now neither.
1221
+ return false;
1222
+ }
1223
+ // We don't care if this is mutated or children layout changed, but we still
1224
+ // measure each instance to see if it moved and therefore should apply layout.
1225
+ finishedWork.flags &= ~Update;
1226
+ className = layoutClassName;
1227
+ } else if ((finishedWork.flags & Update) !== NoFlags) {
1228
+ // It was updated and we have an appropriate class name to apply.
1229
+ className = updateClassName;
1230
+ } else {
1231
+ if (layoutClassName === 'none') {
1232
+ // If we did not update, then all changes are considered a layout. We'll
1233
+ // attempt to cancel.
1234
+ viewTransitionHostInstanceIdx = 0;
1235
+ cancelViewTransitionHostInstances(current, finishedWork.child, true);
1236
+ return false;
1237
+ }
1238
+ // We didn't update but we might still apply layout so we measure each
1239
+ // instance to see if it moved or resized.
1240
+ className = layoutClassName;
1241
+ }
1242
+ const name = getViewTransitionName(props, finishedWork.stateNode);
1243
// If nothing changed due to a mutation, or children changing size
1244
// and the measurements end up unchanged, we should restore it to not animate.
1245
viewTransitionHostInstanceIdx = 0;
1248
current,
1249
finishedWork,
1250
finishedWork.child,
1251
+ name,
1252
+ className,
1253
previousMeasurements,
1254
true,
1255
);
1270
if (child.tag === ViewTransitionComponent) {
1271
const current = child.alternate;
1272
if (current !== null) {
1273
+ const props: ViewTransitionProps = child.memoizedProps;
1274
+ const name = getViewTransitionName(props, child.stateNode);
1275
+ const className: ?string = getViewTransitionClassName(
1276
+ props.className,
1277
+ props.layout,
1278
+ );
1279
viewTransitionHostInstanceIdx = 0;
1131
- measureViewTransitionHostInstances(
1280
+ const inViewport = measureViewTransitionHostInstances(
1281
current,
1282
child,
1283
child.child,
1284
+ name,
1285
+ className,
1286
child.memoizedState,
1287
false,
1288
);
1138
- const props: ViewTransitionProps = child.memoizedProps;
1139
- scheduleViewTransitionEvent(child, props.onLayout);
1289
+ if ((child.flags & Update) === NoFlags || !inViewport) {
1290
+ // Nothing changed.
1291
+ } else {
1292
+ scheduleViewTransitionEvent(child, props.onLayout);
1293
+ }
1294
}
1295
} else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
1296
measureNestedViewTransitions(child);
3164
// its size and position. We need to measure this and if not, restore it to
3165
// not animate.
3166
measureNestedViewTransitions(parentFiber);
3013
- if ((parentFiber.flags & AffectedParentLayout) !== NoFlags) {
3014
- // This boundary changed size in a way that may have caused its parent to
3015
- // relayout. We need to bubble this information up to the parent.
3016
- viewTransitionContextChanged = true;
3017
- }
3167
}
3168
}
3169