Skip to content

Commit 2e38573

Browse files
authored
Find Pairs and Apply View Transition Names to the Clones in the "old" Phase (facebook#32599)
Stacked on facebook#32578. We need to apply view-transition-names to the clones that we create in the "old" phase for the ViewTransition boundaries that should activate. Finding pairs is a little trickier than in ReactFiberCommitViewTransitions. Normally we collect all name "insertions" in the `accumulateSuspenseyCommit` phase before we even commit. Then in the snapshot do we visit all "deletions" and since we already collected all the insertions we know immediately if the deletion had a pair and should therefore get a "name" assigned to activate the boundary. For ReactFiberApplyGesture we need to assign names to "insertions" since it's in reverse but we don't already have a map of deletions. Therefore we need to first visit all deletions. Instead of doing that in a completely separate pass, we instead visit deletions in the same pass to find pairs. Since this is in the same pass we might visit insertions before deletions or vice versa depending on document order. However, we can deal with this by applying the name to the insertion when we find the deletion if we've already made the clones at that point. Applying names to pure exits, updates or nested (relayout) is a bit more straight-forward.
1 parent c4a3b92 commit 2e38573

File tree

2 files changed

+263
-12
lines changed

2 files changed

+263
-12
lines changed

packages/react-reconciler/src/ReactFiberApplyGesture.js

Lines changed: 255 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
removeRootViewTransitionClone,
2626
cancelRootViewTransitionName,
2727
restoreRootViewTransitionName,
28+
applyViewTransitionName,
2829
appendChild,
2930
commitUpdate,
3031
commitTextUpdate,
@@ -60,7 +61,12 @@ import {
6061
import {
6162
restoreEnterOrExitViewTransitions,
6263
restoreNestedViewTransitions,
64+
appearingViewTransitions,
6365
} from './ReactFiberCommitViewTransitions';
66+
import {
67+
getViewTransitionName,
68+
getViewTransitionClassName,
69+
} from './ReactFiberViewTransitionComponent';
6470

6571
let didWarnForRootClone = false;
6672

@@ -78,7 +84,37 @@ const INSERT_APPEND = 6; // Inside a newly mounted tree before the next HostComp
7884
const INSERT_APPEARING_PAIR = 7; // Inside a newly mounted tree only finding pairs.
7985
type VisitPhase = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
8086

87+
function applyViewTransitionToClones(
88+
name: string,
89+
className: ?string,
90+
clones: Array<Instance>,
91+
): void {
92+
// This gets called when we have found a pair, but after the clone in created. The clone is
93+
// created by the insertion side. If the insertion side if found before the deletion side
94+
// then this is called by the deletion. If the deletion is visited first then this is called
95+
// later by the insertion when the clone has been created.
96+
for (let i = 0; i < clones.length; i++) {
97+
applyViewTransitionName(
98+
clones[i],
99+
i === 0
100+
? name
101+
: // If we have multiple Host Instances below, we add a suffix to the name to give
102+
// each one a unique name.
103+
name + '_' + i,
104+
className,
105+
);
106+
}
107+
}
108+
81109
function trackDeletedPairViewTransitions(deletion: Fiber): void {
110+
if (
111+
appearingViewTransitions === null ||
112+
appearingViewTransitions.size === 0
113+
) {
114+
// We've found all.
115+
return;
116+
}
117+
const pairs = appearingViewTransitions;
82118
if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
83119
// This has no named view transitions in its subtree.
84120
return;
@@ -95,7 +131,40 @@ function trackDeletedPairViewTransitions(deletion: Fiber): void {
95131
const props: ViewTransitionProps = child.memoizedProps;
96132
const name = props.name;
97133
if (name != null && name !== 'auto') {
98-
// TODO: Find a pair
134+
const pair = pairs.get(name);
135+
if (pair !== undefined) {
136+
// Delete the entry so that we know when we've found all of them
137+
// and can stop searching (size reaches zero).
138+
pairs.delete(name);
139+
const className: ?string = getViewTransitionClassName(
140+
props.className,
141+
props.share,
142+
);
143+
if (className !== 'none') {
144+
// TODO: Since the deleted instance already has layout we could
145+
// check if it's in the viewport and if not skip the pairing.
146+
// It would currently cause layout thrash though so if we did that
147+
// we need to avoid inserting the root of the cloned trees until
148+
// the end.
149+
150+
// The "old" instance is actually the one we're inserting.
151+
const oldInstance: ViewTransitionState = pair;
152+
// The "new" instance is the already mounted one we're deleting.
153+
const newInstance: ViewTransitionState = child.stateNode;
154+
oldInstance.paired = newInstance;
155+
newInstance.paired = oldInstance;
156+
const clones = oldInstance.clones;
157+
if (clones !== null) {
158+
// If we have clones that means that we've already visited this
159+
// ViewTransition boundary before and we can now apply the name
160+
// to those clones. Otherwise, we have to wait until we clone it.
161+
applyViewTransitionToClones(name, className, clones);
162+
}
163+
}
164+
if (pairs.size === 0) {
165+
break;
166+
}
167+
}
99168
}
100169
}
101170
trackDeletedPairViewTransitions(child);
@@ -107,9 +176,41 @@ function trackDeletedPairViewTransitions(deletion: Fiber): void {
107176
function trackEnterViewTransitions(deletion: Fiber): void {
108177
if (deletion.tag === ViewTransitionComponent) {
109178
const props: ViewTransitionProps = deletion.memoizedProps;
110-
const name = props.name;
111-
if (name != null && name !== 'auto') {
112-
// TODO: Find a pair
179+
const name = getViewTransitionName(props, deletion.stateNode);
180+
const pair =
181+
appearingViewTransitions !== null
182+
? appearingViewTransitions.get(name)
183+
: undefined;
184+
const className: ?string = getViewTransitionClassName(
185+
props.className,
186+
pair !== undefined ? props.share : props.enter,
187+
);
188+
if (className !== 'none') {
189+
if (pair !== undefined) {
190+
// TODO: Since the deleted instance already has layout we could
191+
// check if it's in the viewport and if not skip the pairing.
192+
// It would currently cause layout thrash though so if we did that
193+
// we need to avoid inserting the root of the cloned trees until
194+
// the end.
195+
196+
// Delete the entry so that we know when we've found all of them
197+
// and can stop searching (size reaches zero).
198+
// $FlowFixMe[incompatible-use]: Refined by the pair.
199+
appearingViewTransitions.delete(name);
200+
// The "old" instance is actually the one we're inserting.
201+
const oldInstance: ViewTransitionState = pair;
202+
// The "new" instance is the already mounted one we're deleting.
203+
const newInstance: ViewTransitionState = deletion.stateNode;
204+
oldInstance.paired = newInstance;
205+
newInstance.paired = oldInstance;
206+
const clones = oldInstance.clones;
207+
if (clones !== null) {
208+
// If we have clones that means that we've already visited this
209+
// ViewTransition boundary before and we can now apply the name
210+
// to those clones. Otherwise, we have to wait until we clone it.
211+
applyViewTransitionToClones(name, className, clones);
212+
}
213+
}
113214
}
114215
// Look for more pairs deeper in the tree.
115216
trackDeletedPairViewTransitions(deletion);
@@ -124,6 +225,122 @@ function trackEnterViewTransitions(deletion: Fiber): void {
124225
}
125226
}
126227

228+
function applyAppearingPairViewTransition(child: Fiber): void {
229+
// Normally these helpers do recursive calls but since insertion/offscreen is forked
230+
// we call this helper from those loops instead. This must be called only on
231+
// ViewTransitionComponent that has already had their clones filled.
232+
if ((child.flags & ViewTransitionNamedStatic) !== NoFlags) {
233+
const state: ViewTransitionState = child.stateNode;
234+
// If this is not yet paired, it doesn't mean that we won't pair it later when
235+
// we find the deletion side. If that's the case then we'll add the names to
236+
// the clones then.
237+
if (state.paired) {
238+
const props: ViewTransitionProps = child.memoizedProps;
239+
if (props.name == null || props.name === 'auto') {
240+
throw new Error(
241+
'Found a pair with an auto name. This is a bug in React.',
242+
);
243+
}
244+
const name = props.name;
245+
// Note that this class name that doesn't actually really matter because the
246+
// "new" side will be the one that wins in practice.
247+
const className: ?string = getViewTransitionClassName(
248+
props.className,
249+
props.share,
250+
);
251+
if (className !== 'none') {
252+
const clones = state.clones;
253+
// If there are no clones at this point, that should mean that there are no
254+
// HostComponent children in this ViewTransition.
255+
if (clones !== null) {
256+
applyViewTransitionToClones(name, className, clones);
257+
}
258+
}
259+
}
260+
}
261+
}
262+
263+
function applyExitViewTransition(placement: Fiber): void {
264+
// Normally these helpers do recursive calls but since insertion/offscreen is forked
265+
// we call this helper from those loops instead. This must be called only on
266+
// ViewTransitionComponent that has already had their clones filled.
267+
const state: ViewTransitionState = placement.stateNode;
268+
const props: ViewTransitionProps = placement.memoizedProps;
269+
const name = getViewTransitionName(props, state);
270+
const className: ?string = getViewTransitionClassName(
271+
props.className,
272+
// Note that just because we don't have a pair yet doesn't mean we won't find one
273+
// later. However, that doesn't matter because if we do the class name that wins
274+
// is the one applied by the "new" side anyway.
275+
state.paired ? props.share : props.exit,
276+
);
277+
if (className !== 'none') {
278+
// TODO: Ideally we could determine if this exit is in the viewport and
279+
// exclude it otherwise but that would require waiting until we insert
280+
// and layout the clones first. Currently wait until the view transition
281+
// starts before reading the layout.
282+
const clones = state.clones;
283+
// If there are no clones at this point, that should mean that there are no
284+
// HostComponent children in this ViewTransition.
285+
if (clones !== null) {
286+
applyViewTransitionToClones(name, className, clones);
287+
}
288+
}
289+
}
290+
291+
function applyNestedViewTransition(child: Fiber): void {
292+
const state: ViewTransitionState = child.stateNode;
293+
const props: ViewTransitionProps = child.memoizedProps;
294+
const name = getViewTransitionName(props, state);
295+
const className: ?string = getViewTransitionClassName(
296+
props.className,
297+
props.layout,
298+
);
299+
if (className !== 'none') {
300+
const clones = state.clones;
301+
// If there are no clones at this point, that should mean that there are no
302+
// HostComponent children in this ViewTransition.
303+
if (clones !== null) {
304+
applyViewTransitionToClones(name, className, clones);
305+
}
306+
}
307+
}
308+
309+
function applyUpdateViewTransition(current: Fiber, finishedWork: Fiber): void {
310+
const state: ViewTransitionState = finishedWork.stateNode;
311+
// Updates can have conflicting names and classNames.
312+
// Since we're doing a reverse animation the "new" state is actually the current
313+
// and the "old" state is the finishedWork.
314+
const newProps: ViewTransitionProps = current.memoizedProps;
315+
const oldProps: ViewTransitionProps = finishedWork.memoizedProps;
316+
const oldName = getViewTransitionName(oldProps, state);
317+
// This className applies only if there are fewer child DOM nodes than
318+
// before or if this update should've been cancelled but we ended up with
319+
// a parent animating so we need to animate the child too. Otherwise
320+
// the "new" state wins. Since "new" normally wins, that's usually what
321+
// we would use. However, since this animation is going in reverse we actually
322+
// want the props from "current" since that's the class that would've won if
323+
// it was the normal direction. To preserve the same effect in either direction.
324+
let className: ?string = getViewTransitionClassName(
325+
newProps.className,
326+
newProps.update,
327+
);
328+
if (className === 'none') {
329+
className = getViewTransitionClassName(newProps.className, newProps.layout);
330+
if (className === 'none') {
331+
// If both update and layout are both "none" then we don't have to
332+
// apply a name. Since we won't animate this boundary.
333+
return;
334+
}
335+
}
336+
const clones = state.clones;
337+
// If there are no clones at this point, that should mean that there are no
338+
// HostComponent children in this ViewTransition.
339+
if (clones !== null) {
340+
applyViewTransitionToClones(oldName, className, clones);
341+
}
342+
}
343+
127344
function recursivelyInsertNew(
128345
parentFiber: Fiber,
129346
hostParentClone: Instance,
@@ -265,7 +482,6 @@ function recursivelyInsertNewFiber(
265482
// This was an Enter of a ViewTransition. We now move onto inserting the inner
266483
// HostComponents and finding inner pairs.
267484
nextPhase = INSERT_APPEND;
268-
// TODO: Mark the name and find a pair.
269485
} else {
270486
nextPhase = visitPhase;
271487
}
@@ -275,6 +491,16 @@ function recursivelyInsertNewFiber(
275491
viewTransitionState,
276492
nextPhase,
277493
);
494+
// After we've inserted the new nodes into the "clones" set we can apply share
495+
// or exit transitions to them.
496+
if (visitPhase === INSERT_EXIT) {
497+
applyExitViewTransition(finishedWork);
498+
} else if (
499+
visitPhase === INSERT_APPEARING_PAIR ||
500+
visitPhase === INSERT_APPEND
501+
) {
502+
applyAppearingPairViewTransition(finishedWork);
503+
}
278504
popMutationContext(prevMutationContext);
279505
break;
280506
default: {
@@ -412,8 +638,18 @@ function recursivelyInsertClonesFromExistingTree(
412638
viewTransitionState,
413639
nextPhase,
414640
);
415-
// TODO: Only the first level should track if this was s
416-
// child.flags |= Update;
641+
// After we've collected the cloned instances, we can apply exit or share transitions
642+
// to them.
643+
if (visitPhase === CLONE_EXIT) {
644+
applyExitViewTransition(child);
645+
} else if (
646+
visitPhase === CLONE_APPEARING_PAIR ||
647+
visitPhase === CLONE_UNHIDE
648+
) {
649+
applyAppearingPairViewTransition(child);
650+
} else if (visitPhase === CLONE_UPDATE) {
651+
applyNestedViewTransition(child);
652+
}
417653
popMutationContext(prevMutationContext);
418654
break;
419655
default: {
@@ -675,6 +911,18 @@ function insertDestinationClonesOfFiber(
675911
// whether it resized or not.
676912
finishedWork.flags |= Update;
677913
}
914+
// After we've collected the cloned instances, we can apply exit or share transitions
915+
// to them.
916+
if (visitPhase === CLONE_EXIT) {
917+
applyExitViewTransition(finishedWork);
918+
} else if (
919+
visitPhase === CLONE_APPEARING_PAIR ||
920+
visitPhase === CLONE_UNHIDE
921+
) {
922+
applyAppearingPairViewTransition(finishedWork);
923+
} else if (visitPhase === CLONE_UPDATE) {
924+
applyUpdateViewTransition(current, finishedWork);
925+
}
678926
popMutationContext(prevMutationContext);
679927
break;
680928
default: {

packages/react-reconciler/src/ReactFiberCommitViewTransitions.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ export function resetShouldStartViewTransition(): void {
4949

5050
// This tracks named ViewTransition components found in the accumulateSuspenseyCommit
5151
// phase that might need to find deleted pairs in the beforeMutation phase.
52-
let appearingViewTransitions: Map<string, ViewTransitionState> | null = null;
52+
export let appearingViewTransitions: Map<string, ViewTransitionState> | null =
53+
null;
5354

5455
export function resetAppearingViewTransitions(): void {
5556
appearingViewTransitions = null;
@@ -347,9 +348,10 @@ function commitDeletedPairViewTransitions(deletion: Fiber): void {
347348
restoreViewTransitionOnHostInstances(child.child, false);
348349
} else {
349350
// We'll transition between them.
350-
const oldinstance: ViewTransitionState = child.stateNode;
351+
const oldInstance: ViewTransitionState = child.stateNode;
351352
const newInstance: ViewTransitionState = pair;
352-
newInstance.paired = oldinstance;
353+
newInstance.paired = oldInstance;
354+
oldInstance.paired = newInstance;
353355
// Note: If the other side ends up outside the viewport, we'll still run this.
354356
// Therefore it's possible for onShare to be called with only an old snapshot.
355357
scheduleViewTransitionEvent(child, props.onShare);
@@ -398,9 +400,10 @@ export function commitExitViewTransitions(deletion: Fiber): void {
398400
} else if (pair !== undefined) {
399401
// We found a new appearing view transition with the same name as this deletion.
400402
// We'll transition between them instead of running the normal exit.
401-
const oldinstance: ViewTransitionState = deletion.stateNode;
403+
const oldInstance: ViewTransitionState = deletion.stateNode;
402404
const newInstance: ViewTransitionState = pair;
403-
newInstance.paired = oldinstance;
405+
newInstance.paired = oldInstance;
406+
oldInstance.paired = newInstance;
404407
// Delete the entry so that we know when we've found all of them
405408
// and can stop searching (size reaches zero).
406409
// $FlowFixMe[incompatible-use]: Refined by the pair.

0 commit comments

Comments
 (0)