Skip to content

Commit 3c3696d

Browse files
authored
Measure Updated ViewTransition Boundaries (facebook#32653)
This does the same thing for `measureUpdateViewTransition` that we did for `measureNestedViewTransitions` in facebook@e3cbaff. If a boundary hasn't mutated and didn't change in size, we mark it for cancellation. Otherwise we add names to it. The different from the CommitViewTransition path is that the "old" names are added to the clones so this is the first time the "new" names. Now we also cancel any boundaries that were unchanged. So now the root no longer animates. We still have to clone them. There are other optimizations that can avoid cloning but once we've done all the layouts we can still cancel the running animation and let them just be the regular content if they didn't change. Just like the regular fire-and-forget path. This also fixes the measurement so that we measure clones by adjusting their position back into the viewport. This actually surfaces a bug in Safari that was already in facebook#32612. It turns out that the old names aren't picked up for some reason and so in Safari they looked more like a cross-fade than what facebook#32612 was supposed to fix. However, now that bug is even more apparent because they actually just disappear in Safari. I'm not sure what that bug is but it's unrelated to this PR so will fix that separately.
1 parent 90b511e commit 3c3696d

File tree

12 files changed

+197
-61
lines changed

12 files changed

+197
-61
lines changed

fixtures/view-transition/src/components/Page.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ export default function Page({url, navigate}) {
7777
<div>
7878
<button
7979
onClick={() => {
80-
navigate(show ? '/?a' : '/?b');
80+
navigate(url === '/?b' ? '/?a' : '/?b');
8181
}}>
82-
{show ? 'A' : 'B'}
82+
{url === '/?b' ? 'A' : 'B'}
8383
</button>
8484
<ViewTransition className="none">
8585
<div>

packages/react-art/src/ReactFiberConfigART.js

+4
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,10 @@ export function measureInstance(instance) {
518518
return null;
519519
}
520520

521+
export function measureClonedInstance(instance) {
522+
return null;
523+
}
524+
521525
export function wasInstanceInViewport(measurement): boolean {
522526
return true;
523527
}

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

+26-4
Original file line numberDiff line numberDiff line change
@@ -1477,10 +1477,12 @@ export type InstanceMeasurement = {
14771477
view: boolean, // is in viewport bounds
14781478
};
14791479

1480-
export function measureInstance(instance: Instance): InstanceMeasurement {
1481-
const ownerWindow = instance.ownerDocument.defaultView;
1482-
const rect = instance.getBoundingClientRect();
1483-
const computedStyle = getComputedStyle(instance);
1480+
function createMeasurement(
1481+
rect: ClientRect | DOMRect,
1482+
computedStyle: CSSStyleDeclaration,
1483+
element: Element,
1484+
): InstanceMeasurement {
1485+
const ownerWindow = element.ownerDocument.defaultView;
14841486
return {
14851487
rect: rect,
14861488
abs:
@@ -1508,6 +1510,26 @@ export function measureInstance(instance: Instance): InstanceMeasurement {
15081510
};
15091511
}
15101512

1513+
export function measureInstance(instance: Instance): InstanceMeasurement {
1514+
const rect = instance.getBoundingClientRect();
1515+
const computedStyle = getComputedStyle(instance);
1516+
return createMeasurement(rect, computedStyle, instance);
1517+
}
1518+
1519+
export function measureClonedInstance(instance: Instance): InstanceMeasurement {
1520+
const measuredRect = instance.getBoundingClientRect();
1521+
// Adjust the DOMRect based on the translate that put it outside the viewport.
1522+
// TODO: This might not be completely correct if the parent also has a transform.
1523+
const rect = new DOMRect(
1524+
measuredRect.x + 20000,
1525+
measuredRect.y + 20000,
1526+
measuredRect.width,
1527+
measuredRect.height,
1528+
);
1529+
const computedStyle = getComputedStyle(instance);
1530+
return createMeasurement(rect, computedStyle, instance);
1531+
}
1532+
15111533
export function wasInstanceInViewport(
15121534
measurement: InstanceMeasurement,
15131535
): boolean {

packages/react-native-renderer/src/ReactFiberConfigNative.js

+4
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,10 @@ export function measureInstance(instance: Instance): InstanceMeasurement {
620620
return null;
621621
}
622622

623+
export function measureClonedInstance(instance: Instance): InstanceMeasurement {
624+
return null;
625+
}
626+
623627
export function wasInstanceInViewport(
624628
measurement: InstanceMeasurement,
625629
): boolean {

packages/react-noop-renderer/src/createReactNoop.js

+4
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
796796
return null;
797797
},
798798

799+
measureClonedInstance(instance: Instance): InstanceMeasurement {
800+
return null;
801+
},
802+
799803
wasInstanceInViewport(measurement: InstanceMeasurement): boolean {
800804
return true;
801805
},

packages/react-reconciler/src/ReactFiberApplyGesture.js

+105-31
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import type {Fiber, FiberRoot} from './ReactInternalTypes';
1111

12-
import type {Instance, TextInstance} from './ReactFiberConfig';
12+
import type {Instance, TextInstance, Props} from './ReactFiberConfig';
1313

1414
import type {OffscreenState} from './ReactFiberActivityComponent';
1515

@@ -25,6 +25,7 @@ import {
2525
removeRootViewTransitionClone,
2626
cancelRootViewTransitionName,
2727
restoreRootViewTransitionName,
28+
cancelViewTransitionName,
2829
applyViewTransitionName,
2930
appendChild,
3031
commitUpdate,
@@ -39,6 +40,7 @@ import {
3940
popMutationContext,
4041
pushMutationContext,
4142
viewTransitionMutationContext,
43+
trackHostMutation,
4244
} from './ReactFiberMutationTracking';
4345
import {
4446
MutationMask,
@@ -48,6 +50,7 @@ import {
4850
Visibility,
4951
ViewTransitionNamedStatic,
5052
ViewTransitionStatic,
53+
AffectedParentLayout,
5154
} from './ReactFiberFlags';
5255
import {
5356
HostComponent,
@@ -61,9 +64,14 @@ import {
6164
import {
6265
restoreEnterOrExitViewTransitions,
6366
restoreNestedViewTransitions,
67+
restoreUpdateViewTransitionForGesture,
6468
appearingViewTransitions,
6569
commitEnterViewTransitions,
6670
measureNestedViewTransitions,
71+
measureUpdateViewTransition,
72+
viewTransitionCancelableChildren,
73+
pushViewTransitionCancelableScope,
74+
popViewTransitionCancelableScope,
6775
} from './ReactFiberCommitViewTransitions';
6876
import {
6977
getViewTransitionName,
@@ -72,6 +80,10 @@ import {
7280

7381
let didWarnForRootClone = false;
7482

83+
// Used during the apply phase to track whether a parent ViewTransition component
84+
// might have been affected by any mutations / relayouts below.
85+
let viewTransitionContextChanged: boolean = false;
86+
7587
function detectMutationOrInsertClones(finishedWork: Fiber): boolean {
7688
return true;
7789
}
@@ -421,6 +433,7 @@ function recursivelyInsertNewFiber(
421433
// For insertions we don't need to clone. It's already new state node.
422434
if (visitPhase !== INSERT_APPEARING_PAIR) {
423435
appendChild(hostParentClone, instance);
436+
trackHostMutation();
424437
recursivelyInsertNew(
425438
finishedWork,
426439
instance,
@@ -450,6 +463,7 @@ function recursivelyInsertNewFiber(
450463
// For insertions we don't need to clone. It's already new state node.
451464
if (visitPhase !== INSERT_APPEARING_PAIR) {
452465
appendChild(hostParentClone, textInstance);
466+
trackHostMutation();
453467
}
454468
break;
455469
}
@@ -575,6 +589,7 @@ function recursivelyInsertClonesFromExistingTree(
575589
}
576590
if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {
577591
unhideInstance(clone, child.memoizedProps);
592+
trackHostMutation();
578593
}
579594
break;
580595
}
@@ -590,6 +605,7 @@ function recursivelyInsertClonesFromExistingTree(
590605
appendChild(hostParentClone, clone);
591606
if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {
592607
unhideTextInstance(clone, child.memoizedProps);
608+
trackHostMutation();
593609
}
594610
break;
595611
}
@@ -679,6 +695,10 @@ function recursivelyInsertClones(
679695
for (let i = 0; i < deletions.length; i++) {
680696
const childToDelete = deletions[i];
681697
trackEnterViewTransitions(childToDelete);
698+
// Normally we would only mark something as triggering a mutation if there was
699+
// actually a HostInstance below here. If this tree didn't contain a HostInstances
700+
// we shouldn't trigger a mutation even though a virtual component was deleted.
701+
trackHostMutation();
682702
}
683703
}
684704

@@ -801,6 +821,7 @@ function insertDestinationClonesOfFiber(
801821
clone = cloneMutableInstance(instance, true);
802822
if (finishedWork.flags & ContentReset) {
803823
resetTextContent(clone);
824+
trackHostMutation();
804825
}
805826
} else {
806827
// If we have children we'll clone them as we walk the tree so we just
@@ -825,6 +846,7 @@ function insertDestinationClonesOfFiber(
825846
);
826847
appendChild(hostParentClone, clone);
827848
unhideInstance(clone, finishedWork.memoizedProps);
849+
trackHostMutation();
828850
} else {
829851
recursivelyInsertClones(finishedWork, clone, null, visitPhase);
830852
appendChild(hostParentClone, clone);
@@ -851,10 +873,12 @@ function insertDestinationClonesOfFiber(
851873
const newText: string = finishedWork.memoizedProps;
852874
const oldText: string = current.memoizedProps;
853875
commitTextUpdate(clone, newText, oldText);
876+
trackHostMutation();
854877
}
855878
appendChild(hostParentClone, clone);
856879
if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) {
857880
unhideTextInstance(clone, finishedWork.memoizedProps);
881+
trackHostMutation();
858882
}
859883
break;
860884
}
@@ -885,6 +909,10 @@ function insertDestinationClonesOfFiber(
885909
} else if (current !== null && current.memoizedState === null) {
886910
// Was previously mounted as visible but is now hidden.
887911
trackEnterViewTransitions(current);
912+
// Normally we would only mark something as triggering a mutation if there was
913+
// actually a HostInstance below here. If this tree didn't contain a HostInstances
914+
// we shouldn't trigger a mutation even though a virtual component was hidden.
915+
trackHostMutation();
888916
}
889917
break;
890918
}
@@ -991,13 +1019,6 @@ function measureExitViewTransitions(placement: Fiber): void {
9911019
}
9921020
}
9931021

994-
function measureUpdateViewTransition(
995-
current: Fiber,
996-
finishedWork: Fiber,
997-
): void {
998-
// TODO
999-
}
1000-
10011022
function recursivelyApplyViewTransitions(parentFiber: Fiber) {
10021023
const deletions = parentFiber.deletions;
10031024
if (deletions !== null) {
@@ -1037,15 +1058,6 @@ function applyViewTransitionsOnFiber(finishedWork: Fiber) {
10371058
// because the fiber tag is more specific. An exception is any flag related
10381059
// to reconciliation, because those can be set on all fiber types.
10391060
switch (finishedWork.tag) {
1040-
case HostComponent: {
1041-
// const instance: Instance = finishedWork.stateNode;
1042-
// TODO: Apply name and measure.
1043-
recursivelyApplyViewTransitions(finishedWork);
1044-
break;
1045-
}
1046-
case HostText: {
1047-
break;
1048-
}
10491061
case HostPortal: {
10501062
// TODO: Consider what should happen to Portals. For now we exclude them.
10511063
break;
@@ -1063,12 +1075,59 @@ function applyViewTransitionsOnFiber(finishedWork: Fiber) {
10631075
}
10641076
break;
10651077
}
1066-
case ViewTransitionComponent:
1067-
measureUpdateViewTransition(current, finishedWork);
1078+
case ViewTransitionComponent: {
1079+
const prevContextChanged = viewTransitionContextChanged;
1080+
const prevCancelableChildren = pushViewTransitionCancelableScope();
1081+
viewTransitionContextChanged = false;
1082+
recursivelyApplyViewTransitions(finishedWork);
1083+
1084+
if (viewTransitionContextChanged) {
1085+
finishedWork.flags |= Update;
1086+
}
1087+
1088+
const inViewport = measureUpdateViewTransition(
1089+
current,
1090+
finishedWork,
1091+
true,
1092+
);
1093+
1094+
if ((finishedWork.flags & Update) === NoFlags || !inViewport) {
1095+
// If this boundary didn't update, then we may be able to cancel its children.
1096+
// We bubble them up to the parent set to be determined later if we can cancel.
1097+
// Similarly, if old and new state was outside the viewport, we can skip it
1098+
// even if it did update.
1099+
if (prevCancelableChildren === null) {
1100+
// Bubbling up this whole set to the parent.
1101+
} else {
1102+
// Merge with parent set.
1103+
// $FlowFixMe[method-unbinding]
1104+
prevCancelableChildren.push.apply(
1105+
prevCancelableChildren,
1106+
viewTransitionCancelableChildren,
1107+
);
1108+
popViewTransitionCancelableScope(prevCancelableChildren);
1109+
}
1110+
// TODO: If this doesn't end up canceled, because a parent animates,
1111+
// then we should probably issue an event since this instance is part of it.
1112+
} else {
1113+
// TODO: Schedule gesture events.
1114+
// If this boundary did update, we cannot cancel its children so those are dropped.
1115+
popViewTransitionCancelableScope(prevCancelableChildren);
1116+
}
1117+
1118+
if ((finishedWork.flags & AffectedParentLayout) !== NoFlags) {
1119+
// This boundary changed size in a way that may have caused its parent to
1120+
// relayout. We need to bubble this information up to the parent.
1121+
viewTransitionContextChanged = true;
1122+
} else {
1123+
// Otherwise, we restore it to whatever the parent had found so far.
1124+
viewTransitionContextChanged = prevContextChanged;
1125+
}
1126+
10681127
const viewTransitionState: ViewTransitionState = finishedWork.stateNode;
10691128
viewTransitionState.clones = null; // Reset
1070-
recursivelyApplyViewTransitions(finishedWork);
10711129
break;
1130+
}
10721131
default: {
10731132
recursivelyApplyViewTransitions(finishedWork);
10741133
break;
@@ -1082,13 +1141,38 @@ export function applyDepartureTransitions(
10821141
finishedWork: Fiber,
10831142
): void {
10841143
// First measure and apply view-transition-names to the "new" states.
1144+
viewTransitionContextChanged = false;
1145+
pushViewTransitionCancelableScope();
1146+
10851147
recursivelyApplyViewTransitions(finishedWork);
1148+
10861149
// Then remove the clones.
10871150
const rootClone = root.gestureClone;
10881151
if (rootClone !== null) {
10891152
root.gestureClone = null;
10901153
removeRootViewTransitionClone(root.containerInfo, rootClone);
10911154
}
1155+
1156+
if (!viewTransitionContextChanged) {
1157+
// If we didn't leak any resizing out to the root, we don't have to transition
1158+
// the root itself. This means that we can now safely cancel any cancellations
1159+
// that bubbled all the way up.
1160+
const cancelableChildren = viewTransitionCancelableChildren;
1161+
if (cancelableChildren !== null) {
1162+
for (let i = 0; i < cancelableChildren.length; i += 3) {
1163+
cancelViewTransitionName(
1164+
((cancelableChildren[i]: any): Instance),
1165+
((cancelableChildren[i + 1]: any): string),
1166+
((cancelableChildren[i + 2]: any): Props),
1167+
);
1168+
}
1169+
}
1170+
// We also cancel the root itself. First we restore the name to the documentElement
1171+
// and then we cancel it.
1172+
restoreRootViewTransitionName(root.containerInfo);
1173+
cancelRootViewTransitionName(root.containerInfo);
1174+
}
1175+
popViewTransitionCancelableScope(null);
10921176
}
10931177

10941178
function recursivelyRestoreViewTransitions(parentFiber: Fiber) {
@@ -1130,15 +1214,6 @@ function restoreViewTransitionsOnFiber(finishedWork: Fiber) {
11301214
// because the fiber tag is more specific. An exception is any flag related
11311215
// to reconciliation, because those can be set on all fiber types.
11321216
switch (finishedWork.tag) {
1133-
case HostComponent: {
1134-
// const instance: Instance = finishedWork.stateNode;
1135-
// TODO: Restore the name.
1136-
recursivelyRestoreViewTransitions(finishedWork);
1137-
break;
1138-
}
1139-
case HostText: {
1140-
break;
1141-
}
11421217
case HostPortal: {
11431218
// TODO: Consider what should happen to Portals. For now we exclude them.
11441219
break;
@@ -1157,8 +1232,7 @@ function restoreViewTransitionsOnFiber(finishedWork: Fiber) {
11571232
break;
11581233
}
11591234
case ViewTransitionComponent:
1160-
const viewTransitionState: ViewTransitionState = finishedWork.stateNode;
1161-
viewTransitionState.clones = null; // Reset
1235+
restoreUpdateViewTransitionForGesture(current, finishedWork);
11621236
recursivelyRestoreViewTransitions(finishedWork);
11631237
break;
11641238
default: {

packages/react-reconciler/src/ReactFiberCommitHostEffects.js

+1
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ export function commitShowHideHostTextInstance(node: Fiber, isHidden: boolean) {
199199
unhideTextInstance(instance, node.memoizedProps);
200200
}
201201
}
202+
trackHostMutation();
202203
} catch (error) {
203204
captureCommitPhaseError(node, node.return, error);
204205
}

0 commit comments

Comments
 (0)