| 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 {ViewTransitionProps} from 'shared/ReactTypes'; |
| 11 | |
| 12 | import type {Fiber, FiberRoot} from './ReactInternalTypes'; |
| 13 | |
| 14 | import type {Instance, TextInstance, Props} from './ReactFiberConfig'; |
| 15 | |
| 16 | import type {OffscreenState} from './ReactFiberOffscreenComponent'; |
| 17 | |
| 18 | import type {ViewTransitionState} from './ReactFiberViewTransitionComponent'; |
| 19 | |
| 20 | import { |
| 21 | cloneMutableInstance, |
| 22 | cloneMutableTextInstance, |
| 23 | cloneRootViewTransitionContainer, |
| 24 | removeRootViewTransitionClone, |
| 25 | cancelRootViewTransitionName, |
| 26 | restoreRootViewTransitionName, |
| 27 | cancelViewTransitionName, |
| 28 | applyViewTransitionName, |
| 29 | appendChild, |
| 30 | commitUpdate, |
| 31 | commitTextUpdate, |
| 32 | resetTextContent, |
| 33 | supportsResources, |
| 34 | supportsSingletons, |
| 35 | unhideInstance, |
| 36 | unhideTextInstance, |
| 37 | } from './ReactFiberConfig'; |
| 38 | import { |
| 39 | popMutationContext, |
| 40 | pushMutationContext, |
| 41 | viewTransitionMutationContext, |
| 42 | trackHostMutation, |
| 43 | } from './ReactFiberMutationTracking'; |
| 44 | import { |
| 45 | MutationMask, |
| 46 | Placement, |
| 47 | Update, |
| 48 | ContentReset, |
| 49 | NoFlags, |
| 50 | Visibility, |
| 51 | ViewTransitionNamedStatic, |
| 52 | ViewTransitionStatic, |
| 53 | AffectedParentLayout, |
| 54 | } from './ReactFiberFlags'; |
| 55 | import { |
| 56 | HasEffect as HookHasEffect, |
| 57 | Insertion as HookInsertion, |
| 58 | } from './ReactHookEffectTags'; |
| 59 | import { |
| 60 | FunctionComponent, |
| 61 | ForwardRef, |
| 62 | MemoComponent, |
| 63 | SimpleMemoComponent, |
| 64 | HostComponent, |
| 65 | HostHoistable, |
| 66 | HostSingleton, |
| 67 | HostText, |
| 68 | HostPortal, |
| 69 | OffscreenComponent, |
| 70 | ViewTransitionComponent, |
| 71 | } from './ReactWorkTags'; |
| 72 | import { |
| 73 | restoreEnterOrExitViewTransitions, |
| 74 | restoreNestedViewTransitions, |
| 75 | restoreUpdateViewTransitionForGesture, |
| 76 | appearingViewTransitions, |
| 77 | commitEnterViewTransitions, |
| 78 | commitParentExitViewTransitions, |
| 79 | measureNestedViewTransitions, |
| 80 | measureUpdateViewTransition, |
| 81 | viewTransitionCancelableChildren, |
| 82 | pushViewTransitionCancelableScope, |
| 83 | popViewTransitionCancelableScope, |
| 84 | } from './ReactFiberCommitViewTransitions'; |
| 85 | import { |
| 86 | commitHookEffectListMount, |
| 87 | commitHookEffectListUnmount, |
| 88 | } from './ReactFiberCommitEffects'; |
| 89 | import { |
| 90 | getViewTransitionName, |
| 91 | getViewTransitionClassName, |
| 92 | } from './ReactFiberViewTransitionComponent'; |
| 93 | |
| 94 | import { |
| 95 | enableProfilerTimer, |
| 96 | enableComponentPerformanceTrack, |
| 97 | enableViewTransitionParentEnterExit, |
| 98 | } from 'shared/ReactFeatureFlags'; |
| 99 | import {trackAnimatingTask} from './ReactProfilerTimer'; |
| 100 | import {scheduleGestureTransitionEvent} from './ReactFiberWorkLoop'; |
| 101 | |
| 102 | // Used during the apply phase to track whether a parent ViewTransition component |
| 103 | // might have been affected by any mutations / relayouts below. |
| 104 | let viewTransitionContextChanged: boolean = false; |
| 105 | |
| 106 | function detectMutationOrInsertClones(finishedWork: Fiber): boolean { |
| 107 | return true; |
| 108 | } |
| 109 | |
| 110 | const CLONE_UPDATE = 0; // Mutations in this subtree or potentially affected by layout. |
| 111 | const CLONE_EXIT = 1; // Inside a reappearing offscreen before the next ViewTransition or HostComponent. |
| 112 | const CLONE_UNHIDE = 2; // Inside a reappearing offscreen before the next HostComponent. |
| 113 | const CLONE_APPEARING_PAIR = 3; // Like UNHIDE but we're already inside the first Host Component only finding pairs. |
| 114 | const CLONE_UNCHANGED = 4; // Nothing in this tree was changed but we're still walking to clone it. |
| 115 | const INSERT_EXIT = 5; // Inside a newly mounted tree before the next ViewTransition or HostComponent. |
| 116 | const INSERT_APPEND = 6; // Inside a newly mounted tree before the next HostComponent. |
| 117 | const INSERT_APPEARING_PAIR = 7; // Inside a newly mounted tree only finding pairs. |
| 118 | type VisitPhase = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; |
| 119 | |
| 120 | function applyViewTransitionToClones( |
| 121 | name: string, |
| 122 | className: ?string, |
| 123 | clones: Array<Instance>, |
| 124 | fiber: Fiber, |
| 125 | ): void { |
| 126 | // This gets called when we have found a pair, but after the clone in created. The clone is |
| 127 | // created by the insertion side. If the insertion side if found before the deletion side |
| 128 | // then this is called by the deletion. If the deletion is visited first then this is called |
| 129 | // later by the insertion when the clone has been created. |
| 130 | for (let i = 0; i < clones.length; i++) { |
| 131 | applyViewTransitionName( |
| 132 | clones[i], |
| 133 | i === 0 |
| 134 | ? name |
| 135 | : // If we have multiple Host Instances below, we add a suffix to the name to give |
| 136 | // each one a unique name. |
| 137 | name + '_' + i, |
| 138 | className, |
| 139 | ); |
| 140 | } |
| 141 | if (enableProfilerTimer && enableComponentPerformanceTrack) { |
| 142 | if (fiber._debugTask != null) { |
| 143 | trackAnimatingTask(fiber._debugTask); |
| 144 | } |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | function trackDeletedPairViewTransitions(deletion: Fiber): void { |
| 149 | if ( |
| 150 | appearingViewTransitions === null || |
| 151 | appearingViewTransitions.size === 0 |
| 152 | ) { |
| 153 | // We've found all. |
| 154 | return; |
| 155 | } |
| 156 | const pairs = appearingViewTransitions; |
| 157 | if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) { |
| 158 | // This has no named view transitions in its subtree. |
| 159 | return; |
| 160 | } |
| 161 | let child = deletion.child; |
| 162 | while (child !== null) { |
| 163 | if (child.tag === OffscreenComponent && child.memoizedState !== null) { |
| 164 | // This tree was already hidden so we skip it. |
| 165 | } else { |
| 166 | if ( |
| 167 | child.tag === ViewTransitionComponent && |
| 168 | (child.flags & ViewTransitionNamedStatic) !== NoFlags |
| 169 | ) { |
| 170 | const props: ViewTransitionProps = child.memoizedProps; |
| 171 | const name = props.name; |
| 172 | if (name != null && name !== 'auto') { |
| 173 | const pair = pairs.get(name); |
| 174 | if (pair !== undefined) { |
| 175 | // Delete the entry so that we know when we've found all of them |
| 176 | // and can stop searching (size reaches zero). |
| 177 | pairs.delete(name); |
| 178 | const className: ?string = getViewTransitionClassName( |
| 179 | props.default, |
| 180 | props.share, |
| 181 | ); |
| 182 | if (className !== 'none') { |
| 183 | // TODO: Since the deleted instance already has layout we could |
| 184 | // check if it's in the viewport and if not skip the pairing. |
| 185 | // It would currently cause layout thrash though so if we did that |
| 186 | // we need to avoid inserting the root of the cloned trees until |
| 187 | // the end. |
| 188 | |
| 189 | // The "old" instance is actually the one we're inserting. |
| 190 | const oldInstance: ViewTransitionState = pair; |
| 191 | // The "new" instance is the already mounted one we're deleting. |
| 192 | const newInstance: ViewTransitionState = child.stateNode; |
| 193 | oldInstance.paired = newInstance; |
| 194 | newInstance.paired = oldInstance; |
| 195 | const clones = oldInstance.clones; |
| 196 | if (clones !== null) { |
| 197 | // If we have clones that means that we've already visited this |
| 198 | // ViewTransition boundary before and we can now apply the name |
| 199 | // to those clones. Otherwise, we have to wait until we clone it. |
| 200 | applyViewTransitionToClones(name, className, clones, child); |
| 201 | } |
| 202 | } |
| 203 | if (pairs.size === 0) { |
| 204 | break; |
| 205 | } |
| 206 | } |
| 207 | } |
| 208 | } |
| 209 | trackDeletedPairViewTransitions(child); |
| 210 | } |
| 211 | child = child.sibling; |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | function trackEnterViewTransitions(deletion: Fiber): void { |
| 216 | if (deletion.tag === ViewTransitionComponent) { |
| 217 | const props: ViewTransitionProps = deletion.memoizedProps; |
| 218 | const name = getViewTransitionName(props, deletion.stateNode); |
| 219 | const pair = |
| 220 | appearingViewTransitions !== null |
| 221 | ? appearingViewTransitions.get(name) |
| 222 | : undefined; |
| 223 | const className: ?string = getViewTransitionClassName( |
| 224 | props.default, |
| 225 | pair !== undefined ? props.share : props.enter, |
| 226 | ); |
| 227 | if (className !== 'none') { |
| 228 | if (pair !== undefined) { |
| 229 | // TODO: Since the deleted instance already has layout we could |
| 230 | // check if it's in the viewport and if not skip the pairing. |
| 231 | // It would currently cause layout thrash though so if we did that |
| 232 | // we need to avoid inserting the root of the cloned trees until |
| 233 | // the end. |
| 234 | |
| 235 | // Delete the entry so that we know when we've found all of them |
| 236 | // and can stop searching (size reaches zero). |
| 237 | // $FlowFixMe[incompatible-use]: Refined by the pair. |
| 238 | appearingViewTransitions.delete(name); |
| 239 | // The "old" instance is actually the one we're inserting. |
| 240 | const oldInstance: ViewTransitionState = pair; |
| 241 | // The "new" instance is the already mounted one we're deleting. |
| 242 | const newInstance: ViewTransitionState = deletion.stateNode; |
| 243 | oldInstance.paired = newInstance; |
| 244 | newInstance.paired = oldInstance; |
| 245 | const clones = oldInstance.clones; |
| 246 | if (clones !== null) { |
| 247 | // If we have clones that means that we've already visited this |
| 248 | // ViewTransition boundary before and we can now apply the name |
| 249 | // to those clones. Otherwise, we have to wait until we clone it. |
| 250 | applyViewTransitionToClones(name, className, clones, deletion); |
| 251 | } |
| 252 | } |
| 253 | } |
| 254 | // Look for more pairs deeper in the tree. |
| 255 | trackDeletedPairViewTransitions(deletion); |
| 256 | } else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) { |
| 257 | let child = deletion.child; |
| 258 | while (child !== null) { |
| 259 | trackEnterViewTransitions(child); |
| 260 | child = child.sibling; |
| 261 | } |
| 262 | } else { |
| 263 | trackDeletedPairViewTransitions(deletion); |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | function applyAppearingPairViewTransition(child: Fiber): void { |
| 268 | // Normally these helpers do recursive calls but since insertion/offscreen is forked |
| 269 | // we call this helper from those loops instead. This must be called only on |
| 270 | // ViewTransitionComponent that has already had their clones filled. |
| 271 | if ((child.flags & ViewTransitionNamedStatic) !== NoFlags) { |
| 272 | const state: ViewTransitionState = child.stateNode; |
| 273 | // If this is not yet paired, it doesn't mean that we won't pair it later when |
| 274 | // we find the deletion side. If that's the case then we'll add the names to |
| 275 | // the clones then. |
| 276 | if (state.paired) { |
| 277 | const props: ViewTransitionProps = child.memoizedProps; |
| 278 | if (props.name == null || props.name === 'auto') { |
| 279 | throw new Error( |
| 280 | 'Found a pair with an auto name. This is a bug in React.', |
| 281 | ); |
| 282 | } |
| 283 | const name = props.name; |
| 284 | // Note that this class name that doesn't actually really matter because the |
| 285 | // "new" side will be the one that wins in practice. |
| 286 | const className: ?string = getViewTransitionClassName( |
| 287 | props.default, |
| 288 | props.share, |
| 289 | ); |
| 290 | if (className !== 'none') { |
| 291 | const clones = state.clones; |
| 292 | // If there are no clones at this point, that should mean that there are no |
| 293 | // HostComponent children in this ViewTransition. |
| 294 | if (clones !== null) { |
| 295 | applyViewTransitionToClones(name, className, clones, child); |
| 296 | } |
| 297 | scheduleGestureTransitionEvent(child, props.onGestureShare); |
| 298 | } |
| 299 | } |
| 300 | } |
| 301 | } |
| 302 | |
| 303 | function applyExitViewTransition(placement: Fiber): void { |
| 304 | // Normally these helpers do recursive calls but since insertion/offscreen is forked |
| 305 | // we call this helper from those loops instead. This must be called only on |
| 306 | // ViewTransitionComponent that has already had their clones filled. |
| 307 | const state: ViewTransitionState = placement.stateNode; |
| 308 | const props: ViewTransitionProps = placement.memoizedProps; |
| 309 | const name = getViewTransitionName(props, state); |
| 310 | const className: ?string = getViewTransitionClassName( |
| 311 | props.default, |
| 312 | // Note that just because we don't have a pair yet doesn't mean we won't find one |
| 313 | // later. However, that doesn't matter because if we do the class name that wins |
| 314 | // is the one applied by the "new" side anyway. |
| 315 | state.paired ? props.share : props.exit, |
| 316 | ); |
| 317 | if (className !== 'none') { |
| 318 | // TODO: Ideally we could determine if this exit is in the viewport and |
| 319 | // exclude it otherwise but that would require waiting until we insert |
| 320 | // and layout the clones first. Currently wait until the view transition |
| 321 | // starts before reading the layout. |
| 322 | const clones = state.clones; |
| 323 | // If there are no clones at this point, that should mean that there are no |
| 324 | // HostComponent children in this ViewTransition. |
| 325 | if (clones !== null) { |
| 326 | applyViewTransitionToClones(name, className, clones, placement); |
| 327 | } |
| 328 | if (state.paired) { |
| 329 | scheduleGestureTransitionEvent(placement, props.onGestureShare); |
| 330 | } else { |
| 331 | scheduleGestureTransitionEvent(placement, props.onGestureExit); |
| 332 | if (enableViewTransitionParentEnterExit) { |
| 333 | commitParentExitViewTransitions(placement, true); |
| 334 | } |
| 335 | } |
| 336 | } |
| 337 | } |
| 338 | |
| 339 | function applyNestedViewTransition(child: Fiber): void { |
| 340 | const state: ViewTransitionState = child.stateNode; |
| 341 | const props: ViewTransitionProps = child.memoizedProps; |
| 342 | const name = getViewTransitionName(props, state); |
| 343 | const className: ?string = getViewTransitionClassName( |
| 344 | props.default, |
| 345 | props.update, |
| 346 | ); |
| 347 | if (className !== 'none') { |
| 348 | const clones = state.clones; |
| 349 | // If there are no clones at this point, that should mean that there are no |
| 350 | // HostComponent children in this ViewTransition. |
| 351 | if (clones !== null) { |
| 352 | applyViewTransitionToClones(name, className, clones, child); |
| 353 | } |
| 354 | } |
| 355 | } |
| 356 | |
| 357 | function applyUpdateViewTransition(current: Fiber, finishedWork: Fiber): void { |
| 358 | const state: ViewTransitionState = finishedWork.stateNode; |
| 359 | // Updates can have conflicting names and classNames. |
| 360 | // Since we're doing a reverse animation the "new" state is actually the current |
| 361 | // and the "old" state is the finishedWork. |
| 362 | const newProps: ViewTransitionProps = current.memoizedProps; |
| 363 | const oldProps: ViewTransitionProps = finishedWork.memoizedProps; |
| 364 | const oldName = getViewTransitionName(oldProps, state); |
| 365 | // This className applies only if there are fewer child DOM nodes than |
| 366 | // before or if this update should've been cancelled but we ended up with |
| 367 | // a parent animating so we need to animate the child too. Otherwise |
| 368 | // the "new" state wins. Since "new" normally wins, that's usually what |
| 369 | // we would use. However, since this animation is going in reverse we actually |
| 370 | // want the props from "current" since that's the class that would've won if |
| 371 | // it was the normal direction. To preserve the same effect in either direction. |
| 372 | const className: ?string = getViewTransitionClassName( |
| 373 | newProps.default, |
| 374 | newProps.update, |
| 375 | ); |
| 376 | if (className === 'none') { |
| 377 | // If update is "none" then we don't have to apply a name. Since we won't animate this boundary. |
| 378 | return; |
| 379 | } |
| 380 | const clones = state.clones; |
| 381 | // If there are no clones at this point, that should mean that there are no |
| 382 | // HostComponent children in this ViewTransition. |
| 383 | if (clones !== null) { |
| 384 | applyViewTransitionToClones(oldName, className, clones, finishedWork); |
| 385 | } |
| 386 | } |
| 387 | |
| 388 | function recursivelyInsertNew( |
| 389 | parentFiber: Fiber, |
| 390 | hostParentClone: Instance, |
| 391 | parentViewTransition: null | ViewTransitionState, |
| 392 | visitPhase: VisitPhase, |
| 393 | ): void { |
| 394 | if ( |
| 395 | visitPhase === INSERT_APPEARING_PAIR && |
| 396 | parentViewTransition === null && |
| 397 | (parentFiber.subtreeFlags & (ViewTransitionNamedStatic | Placement)) === |
| 398 | NoFlags |
| 399 | ) { |
| 400 | // We're just searching for pairs or insertion effects but we have reached the end. |
| 401 | return; |
| 402 | } |
| 403 | let child = parentFiber.child; |
| 404 | while (child !== null) { |
| 405 | recursivelyInsertNewFiber( |
| 406 | child, |
| 407 | hostParentClone, |
| 408 | parentViewTransition, |
| 409 | visitPhase, |
| 410 | ); |
| 411 | child = child.sibling; |
| 412 | } |
| 413 | } |
| 414 | |
| 415 | function recursivelyInsertNewFiber( |
| 416 | finishedWork: Fiber, |
| 417 | hostParentClone: Instance, |
| 418 | parentViewTransition: null | ViewTransitionState, |
| 419 | visitPhase: VisitPhase, |
| 420 | ): void { |
| 421 | switch (finishedWork.tag) { |
| 422 | case FunctionComponent: |
| 423 | case ForwardRef: |
| 424 | case MemoComponent: |
| 425 | case SimpleMemoComponent: { |
| 426 | recursivelyInsertNew( |
| 427 | finishedWork, |
| 428 | hostParentClone, |
| 429 | parentViewTransition, |
| 430 | visitPhase, |
| 431 | ); |
| 432 | if (finishedWork.flags & Update) { |
| 433 | // Insertion Effects are mounted temporarily during the rendering of the snapshot. |
| 434 | // This does not affect cloned Offscreen content since those would've been mounted |
| 435 | // while inside the offscreen tree already. |
| 436 | // Note that because we are mounting a clone of the DOM tree and the previous DOM |
| 437 | // tree remains mounted during the snapshot, we can't unmount any previous insertion |
| 438 | // effects. This can lead to conflicts but that is similar to what can happen with |
| 439 | // conflicts for two mounted Activity boundaries. |
| 440 | commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork); |
| 441 | } |
| 442 | break; |
| 443 | } |
| 444 | case HostHoistable: { |
| 445 | // $FlowFixMe[constant-condition] |
| 446 | if (supportsResources) { |
| 447 | // TODO: Hoistables should get optimistically inserted and then removed. |
| 448 | recursivelyInsertNew( |
| 449 | finishedWork, |
| 450 | hostParentClone, |
| 451 | parentViewTransition, |
| 452 | visitPhase, |
| 453 | ); |
| 454 | break; |
| 455 | } |
| 456 | // Fall through |
| 457 | } |
| 458 | case HostSingleton: { |
| 459 | // $FlowFixMe[constant-condition] |
| 460 | if (supportsSingletons) { |
| 461 | recursivelyInsertNew( |
| 462 | finishedWork, |
| 463 | hostParentClone, |
| 464 | parentViewTransition, |
| 465 | visitPhase, |
| 466 | ); |
| 467 | |
| 468 | if (__DEV__) { |
| 469 | // We cannot apply mutations to Host Singletons since by definition |
| 470 | // they cannot be cloned. Therefore we warn in DEV if this commit |
| 471 | // had any effect. |
| 472 | if (finishedWork.flags & Update) { |
| 473 | console.error( |
| 474 | 'startGestureTransition() caused something to render a new <%s>. ' + |
| 475 | 'This is not possible in the current implementation. ' + |
| 476 | "Make sure that the swipe doesn't mount any new <%s> elements.", |
| 477 | finishedWork.type, |
| 478 | finishedWork.type, |
| 479 | ); |
| 480 | } |
| 481 | } |
| 482 | break; |
| 483 | } |
| 484 | // Fall through |
| 485 | } |
| 486 | case HostComponent: { |
| 487 | const instance: Instance = finishedWork.stateNode; |
| 488 | // For insertions we don't need to clone. It's already new state node. |
| 489 | if (visitPhase !== INSERT_APPEARING_PAIR) { |
| 490 | appendChild(hostParentClone, instance); |
| 491 | trackHostMutation(); |
| 492 | recursivelyInsertNew( |
| 493 | finishedWork, |
| 494 | instance, |
| 495 | null, |
| 496 | INSERT_APPEARING_PAIR, |
| 497 | ); |
| 498 | } else { |
| 499 | recursivelyInsertNew(finishedWork, instance, null, visitPhase); |
| 500 | } |
| 501 | if (parentViewTransition !== null) { |
| 502 | if (parentViewTransition.clones === null) { |
| 503 | parentViewTransition.clones = [instance]; |
| 504 | } else { |
| 505 | parentViewTransition.clones.push(instance); |
| 506 | } |
| 507 | } |
| 508 | break; |
| 509 | } |
| 510 | case HostText: { |
| 511 | const textInstance: TextInstance = finishedWork.stateNode; |
| 512 | // $FlowFixMe[invalid-compare] |
| 513 | if (textInstance === null) { |
| 514 | throw new Error( |
| 515 | 'This should have a text node initialized. This error is likely ' + |
| 516 | 'caused by a bug in React. Please file an issue.', |
| 517 | ); |
| 518 | } |
| 519 | // For insertions we don't need to clone. It's already new state node. |
| 520 | if (visitPhase !== INSERT_APPEARING_PAIR) { |
| 521 | appendChild(hostParentClone, textInstance); |
| 522 | trackHostMutation(); |
| 523 | } |
| 524 | break; |
| 525 | } |
| 526 | case HostPortal: { |
| 527 | // TODO: Consider what should happen to Portals. For now we exclude them. |
| 528 | break; |
| 529 | } |
| 530 | case OffscreenComponent: { |
| 531 | const newState: OffscreenState | null = finishedWork.memoizedState; |
| 532 | const isHidden = newState !== null; |
| 533 | if (!isHidden) { |
| 534 | // Only insert nodes if this tree is going to be visible. No need to |
| 535 | // insert invisible content. |
| 536 | // Since there was no mutation to this node, it couldn't have changed |
| 537 | // visibility so we don't need to update visitPhase here. |
| 538 | recursivelyInsertNew( |
| 539 | finishedWork, |
| 540 | hostParentClone, |
| 541 | parentViewTransition, |
| 542 | visitPhase, |
| 543 | ); |
| 544 | } |
| 545 | break; |
| 546 | } |
| 547 | case ViewTransitionComponent: |
| 548 | const prevMutationContext = pushMutationContext(); |
| 549 | const viewTransitionState: ViewTransitionState = finishedWork.stateNode; |
| 550 | // TODO: If this was already cloned by a previous pass we can reuse those clones. |
| 551 | viewTransitionState.clones = null; |
| 552 | let nextPhase: VisitPhase; |
| 553 | if (visitPhase === INSERT_EXIT) { |
| 554 | // This was an Enter of a ViewTransition. We now move onto inserting the inner |
| 555 | // HostComponents and finding inner pairs. |
| 556 | nextPhase = INSERT_APPEND; |
| 557 | } else { |
| 558 | nextPhase = visitPhase; |
| 559 | } |
| 560 | recursivelyInsertNew( |
| 561 | finishedWork, |
| 562 | hostParentClone, |
| 563 | viewTransitionState, |
| 564 | nextPhase, |
| 565 | ); |
| 566 | // After we've inserted the new nodes into the "clones" set we can apply share |
| 567 | // or exit transitions to them. |
| 568 | if (visitPhase === INSERT_EXIT) { |
| 569 | applyExitViewTransition(finishedWork); |
| 570 | } else if ( |
| 571 | visitPhase === INSERT_APPEARING_PAIR || |
| 572 | visitPhase === INSERT_APPEND |
| 573 | ) { |
| 574 | applyAppearingPairViewTransition(finishedWork); |
| 575 | } |
| 576 | popMutationContext(prevMutationContext); |
| 577 | break; |
| 578 | default: { |
| 579 | recursivelyInsertNew( |
| 580 | finishedWork, |
| 581 | hostParentClone, |
| 582 | parentViewTransition, |
| 583 | visitPhase, |
| 584 | ); |
| 585 | break; |
| 586 | } |
| 587 | } |
| 588 | } |
| 589 | |
| 590 | function recursivelyInsertClonesFromExistingTree( |
| 591 | parentFiber: Fiber, |
| 592 | hostParentClone: Instance, |
| 593 | parentViewTransition: null | ViewTransitionState, |
| 594 | visitPhase: VisitPhase, |
| 595 | ): void { |
| 596 | let child = parentFiber.child; |
| 597 | while (child !== null) { |
| 598 | switch (child.tag) { |
| 599 | case HostComponent: { |
| 600 | const instance: Instance = child.stateNode; |
| 601 | let nextPhase: VisitPhase; |
| 602 | switch (visitPhase) { |
| 603 | case CLONE_EXIT: |
| 604 | case CLONE_UNHIDE: |
| 605 | case CLONE_APPEARING_PAIR: |
| 606 | // If this was an unhide, we need to keep going if there are any named |
| 607 | // pairs in this subtree, since they might need to be marked. |
| 608 | nextPhase = |
| 609 | (child.subtreeFlags & ViewTransitionNamedStatic) !== NoFlags |
| 610 | ? CLONE_APPEARING_PAIR |
| 611 | : CLONE_UNCHANGED; |
| 612 | break; |
| 613 | default: |
| 614 | // We've found any "layout" View Transitions at this point so we can bail. |
| 615 | nextPhase = CLONE_UNCHANGED; |
| 616 | } |
| 617 | let clone: Instance; |
| 618 | if (nextPhase !== CLONE_UNCHANGED) { |
| 619 | // We might need a handle on these clones, so we need to do a shallow clone |
| 620 | // and keep going. |
| 621 | clone = cloneMutableInstance(instance, false); |
| 622 | recursivelyInsertClonesFromExistingTree( |
| 623 | child, |
| 624 | clone, |
| 625 | null, |
| 626 | nextPhase, |
| 627 | ); |
| 628 | } else { |
| 629 | // If we have no mutations in this subtree, and we don't need a handle on the |
| 630 | // clones, then we can do a deep clone instead and bailout. |
| 631 | clone = cloneMutableInstance(instance, true); |
| 632 | // TODO: We may need to transfer some DOM state such as scroll position |
| 633 | // for the deep clones. |
| 634 | // TODO: If there's a manual view-transition-name inside the clone we |
| 635 | // should ideally remove it from the original and then restore it in mutation |
| 636 | // phase. Otherwise it leads to duplicate names. |
| 637 | } |
| 638 | appendChild(hostParentClone, clone); |
| 639 | if (parentViewTransition !== null) { |
| 640 | if (parentViewTransition.clones === null) { |
| 641 | parentViewTransition.clones = [clone]; |
| 642 | } else { |
| 643 | parentViewTransition.clones.push(clone); |
| 644 | } |
| 645 | } |
| 646 | if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) { |
| 647 | unhideInstance(clone, child.memoizedProps); |
| 648 | trackHostMutation(); |
| 649 | } |
| 650 | break; |
| 651 | } |
| 652 | case HostText: { |
| 653 | const textInstance: TextInstance = child.stateNode; |
| 654 | // $FlowFixMe[invalid-compare] |
| 655 | if (textInstance === null) { |
| 656 | throw new Error( |
| 657 | 'This should have a text node initialized. This error is likely ' + |
| 658 | 'caused by a bug in React. Please file an issue.', |
| 659 | ); |
| 660 | } |
| 661 | const clone = cloneMutableTextInstance(textInstance); |
| 662 | appendChild(hostParentClone, clone); |
| 663 | if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) { |
| 664 | unhideTextInstance(clone, child.memoizedProps); |
| 665 | trackHostMutation(); |
| 666 | } |
| 667 | break; |
| 668 | } |
| 669 | case HostPortal: { |
| 670 | // TODO: Consider what should happen to Portals. For now we exclude them. |
| 671 | break; |
| 672 | } |
| 673 | case OffscreenComponent: { |
| 674 | const newState: OffscreenState | null = child.memoizedState; |
| 675 | const isHidden = newState !== null; |
| 676 | if (!isHidden) { |
| 677 | // Only insert clones if this tree is going to be visible. No need to |
| 678 | // clone invisible content. |
| 679 | // TODO: If this is visible but detached it should still be cloned. |
| 680 | // Since there was no mutation to this node, it couldn't have changed |
| 681 | // visibility so we don't need to update visitPhase here. |
| 682 | recursivelyInsertClonesFromExistingTree( |
| 683 | child, |
| 684 | hostParentClone, |
| 685 | parentViewTransition, |
| 686 | visitPhase, |
| 687 | ); |
| 688 | } |
| 689 | break; |
| 690 | } |
| 691 | case ViewTransitionComponent: |
| 692 | const prevMutationContext = pushMutationContext(); |
| 693 | const viewTransitionState: ViewTransitionState = child.stateNode; |
| 694 | // TODO: If this was already cloned by a previous pass we can reuse those clones. |
| 695 | viewTransitionState.clones = null; |
| 696 | // "Existing" view transitions are in subtrees that didn't update so |
| 697 | // this is a "current". We normally clear this upon rerendering |
| 698 | // but we use this flag to track changes from layout in the commit. |
| 699 | // So we need it to be cleared before we do that. |
| 700 | // TODO: Use some other temporary state to track this. |
| 701 | child.flags &= ~Update; |
| 702 | let nextPhase: VisitPhase; |
| 703 | if (visitPhase === CLONE_EXIT) { |
| 704 | // This was an Enter of a ViewTransition. We now move onto unhiding the inner |
| 705 | // HostComponents and finding inner pairs. |
| 706 | nextPhase = CLONE_UNHIDE; |
| 707 | // TODO: Mark the name and find a pair. |
| 708 | } else if (visitPhase === CLONE_UPDATE) { |
| 709 | // If the tree had no mutations and we've found the top most ViewTransition |
| 710 | // then this is the one we might apply the "layout" state too if it has changed |
| 711 | // position. After we've found its HostComponents we can bail out. |
| 712 | nextPhase = CLONE_UNCHANGED; |
| 713 | } else { |
| 714 | nextPhase = visitPhase; |
| 715 | } |
| 716 | recursivelyInsertClonesFromExistingTree( |
| 717 | child, |
| 718 | hostParentClone, |
| 719 | viewTransitionState, |
| 720 | nextPhase, |
| 721 | ); |
| 722 | // After we've collected the cloned instances, we can apply exit or share transitions |
| 723 | // to them. |
| 724 | if (visitPhase === CLONE_EXIT) { |
| 725 | applyExitViewTransition(child); |
| 726 | } else if ( |
| 727 | visitPhase === CLONE_APPEARING_PAIR || |
| 728 | visitPhase === CLONE_UNHIDE |
| 729 | ) { |
| 730 | applyAppearingPairViewTransition(child); |
| 731 | } else if (visitPhase === CLONE_UPDATE) { |
| 732 | applyNestedViewTransition(child); |
| 733 | } |
| 734 | popMutationContext(prevMutationContext); |
| 735 | break; |
| 736 | default: { |
| 737 | recursivelyInsertClonesFromExistingTree( |
| 738 | child, |
| 739 | hostParentClone, |
| 740 | parentViewTransition, |
| 741 | visitPhase, |
| 742 | ); |
| 743 | break; |
| 744 | } |
| 745 | } |
| 746 | child = child.sibling; |
| 747 | } |
| 748 | } |
| 749 | |
| 750 | function recursivelyInsertClones( |
| 751 | parentFiber: Fiber, |
| 752 | hostParentClone: Instance, |
| 753 | parentViewTransition: null | ViewTransitionState, |
| 754 | visitPhase: VisitPhase, |
| 755 | ) { |
| 756 | const deletions = parentFiber.deletions; |
| 757 | if (deletions !== null) { |
| 758 | for (let i = 0; i < deletions.length; i++) { |
| 759 | const childToDelete = deletions[i]; |
| 760 | trackEnterViewTransitions(childToDelete); |
| 761 | // Normally we would only mark something as triggering a mutation if there was |
| 762 | // actually a HostInstance below here. If this tree didn't contain a HostInstances |
| 763 | // we shouldn't trigger a mutation even though a virtual component was deleted. |
| 764 | trackHostMutation(); |
| 765 | } |
| 766 | } |
| 767 | |
| 768 | if ( |
| 769 | parentFiber.alternate === null || |
| 770 | (parentFiber.subtreeFlags & MutationMask) !== NoFlags |
| 771 | ) { |
| 772 | // If we have mutations or if this is a newly inserted tree, clone as we go. |
| 773 | let child = parentFiber.child; |
| 774 | while (child !== null) { |
| 775 | insertDestinationClonesOfFiber( |
| 776 | child, |
| 777 | hostParentClone, |
| 778 | parentViewTransition, |
| 779 | visitPhase, |
| 780 | ); |
| 781 | child = child.sibling; |
| 782 | } |
| 783 | } else { |
| 784 | // Once we reach a subtree with no more mutations we can bail out. |
| 785 | // However, we must still insert deep clones of the HostComponents. |
| 786 | recursivelyInsertClonesFromExistingTree( |
| 787 | parentFiber, |
| 788 | hostParentClone, |
| 789 | parentViewTransition, |
| 790 | visitPhase, |
| 791 | ); |
| 792 | } |
| 793 | } |
| 794 | |
| 795 | function insertDestinationClonesOfFiber( |
| 796 | finishedWork: Fiber, |
| 797 | hostParentClone: Instance, |
| 798 | parentViewTransition: null | ViewTransitionState, |
| 799 | visitPhase: VisitPhase, |
| 800 | ) { |
| 801 | const current = finishedWork.alternate; |
| 802 | if (current === null) { |
| 803 | // This is a newly mounted subtree. Insert any HostComponents and trigger |
| 804 | // Enter transitions. |
| 805 | recursivelyInsertNewFiber( |
| 806 | finishedWork, |
| 807 | hostParentClone, |
| 808 | parentViewTransition, |
| 809 | INSERT_EXIT, |
| 810 | ); |
| 811 | return; |
| 812 | } |
| 813 | |
| 814 | const flags = finishedWork.flags; |
| 815 | // The effect flag should be checked *after* we refine the type of fiber, |
| 816 | // because the fiber tag is more specific. An exception is any flag related |
| 817 | // to reconciliation, because those can be set on all fiber types. |
| 818 | switch (finishedWork.tag) { |
| 819 | case HostHoistable: { |
| 820 | // $FlowFixMe[constant-condition] |
| 821 | if (supportsResources) { |
| 822 | // TODO: Hoistables should get optimistically inserted and then removed. |
| 823 | recursivelyInsertClones( |
| 824 | finishedWork, |
| 825 | hostParentClone, |
| 826 | parentViewTransition, |
| 827 | visitPhase, |
| 828 | ); |
| 829 | break; |
| 830 | } |
| 831 | // Fall through |
| 832 | } |
| 833 | case HostSingleton: { |
| 834 | // $FlowFixMe[constant-condition] |
| 835 | if (supportsSingletons) { |
| 836 | recursivelyInsertClones( |
| 837 | finishedWork, |
| 838 | hostParentClone, |
| 839 | parentViewTransition, |
| 840 | visitPhase, |
| 841 | ); |
| 842 | if (__DEV__) { |
| 843 | // We cannot apply mutations to Host Singletons since by definition |
| 844 | // they cannot be cloned. Therefore we warn in DEV if this commit |
| 845 | // had any effect. |
| 846 | if (flags & Update) { |
| 847 | const newProps = finishedWork.memoizedProps; |
| 848 | const oldProps = current.memoizedProps; |
| 849 | const instance = finishedWork.stateNode; |
| 850 | const type = finishedWork.type; |
| 851 | const prev = pushMutationContext(); |
| 852 | |
| 853 | try { |
| 854 | // Since we currently don't have a separate diffing algorithm for |
| 855 | // individual properties, the Update flag can be a false positive. |
| 856 | // We have to apply the new props first o detect any mutations and |
| 857 | // then revert them. |
| 858 | commitUpdate(instance, type, oldProps, newProps, finishedWork); |
| 859 | if (viewTransitionMutationContext) { |
| 860 | console.error( |
| 861 | 'startGestureTransition() caused something to mutate <%s>. ' + |
| 862 | 'This is not possible in the current implementation. ' + |
| 863 | "Make sure that the swipe doesn't update any state which " + |
| 864 | 'causes <%s> to change.', |
| 865 | finishedWork.type, |
| 866 | finishedWork.type, |
| 867 | ); |
| 868 | } |
| 869 | // Revert |
| 870 | commitUpdate(instance, type, newProps, oldProps, finishedWork); |
| 871 | } finally { |
| 872 | popMutationContext(prev); |
| 873 | } |
| 874 | } |
| 875 | } |
| 876 | break; |
| 877 | } |
| 878 | // Fall through |
| 879 | } |
| 880 | case HostComponent: { |
| 881 | const instance: Instance = finishedWork.stateNode; |
| 882 | let clone: Instance; |
| 883 | if (finishedWork.child === null) { |
| 884 | // This node is terminal. We still do a deep clone in case this has user |
| 885 | // inserted content, text content or dangerouslySetInnerHTML. |
| 886 | clone = cloneMutableInstance(instance, true); |
| 887 | if (finishedWork.flags & ContentReset) { |
| 888 | resetTextContent(clone); |
| 889 | trackHostMutation(); |
| 890 | } |
| 891 | } else { |
| 892 | // If we have children we'll clone them as we walk the tree so we just |
| 893 | // do a shallow clone here. |
| 894 | clone = cloneMutableInstance(instance, false); |
| 895 | } |
| 896 | |
| 897 | if (flags & Update) { |
| 898 | const newProps = finishedWork.memoizedProps; |
| 899 | const oldProps = current.memoizedProps; |
| 900 | const type = finishedWork.type; |
| 901 | // Apply the delta to the clone. |
| 902 | commitUpdate(clone, type, oldProps, newProps, finishedWork); |
| 903 | } |
| 904 | |
| 905 | if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) { |
| 906 | appendChild(hostParentClone, clone); |
| 907 | unhideInstance(clone, finishedWork.memoizedProps); |
| 908 | recursivelyInsertClones( |
| 909 | finishedWork, |
| 910 | clone, |
| 911 | null, |
| 912 | CLONE_APPEARING_PAIR, |
| 913 | ); |
| 914 | trackHostMutation(); |
| 915 | } else { |
| 916 | appendChild(hostParentClone, clone); |
| 917 | recursivelyInsertClones(finishedWork, clone, null, visitPhase); |
| 918 | } |
| 919 | if (parentViewTransition !== null) { |
| 920 | if (parentViewTransition.clones === null) { |
| 921 | parentViewTransition.clones = [clone]; |
| 922 | } else { |
| 923 | parentViewTransition.clones.push(clone); |
| 924 | } |
| 925 | } |
| 926 | break; |
| 927 | } |
| 928 | case HostText: { |
| 929 | const textInstance: TextInstance = finishedWork.stateNode; |
| 930 | // $FlowFixMe[invalid-compare] |
| 931 | if (textInstance === null) { |
| 932 | throw new Error( |
| 933 | 'This should have a text node initialized. This error is likely ' + |
| 934 | 'caused by a bug in React. Please file an issue.', |
| 935 | ); |
| 936 | } |
| 937 | const clone = cloneMutableTextInstance(textInstance); |
| 938 | if (flags & Update) { |
| 939 | const newText: string = finishedWork.memoizedProps; |
| 940 | const oldText: string = current.memoizedProps; |
| 941 | commitTextUpdate(clone, newText, oldText); |
| 942 | trackHostMutation(); |
| 943 | } |
| 944 | appendChild(hostParentClone, clone); |
| 945 | if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) { |
| 946 | unhideTextInstance(clone, finishedWork.memoizedProps); |
| 947 | trackHostMutation(); |
| 948 | } |
| 949 | break; |
| 950 | } |
| 951 | case HostPortal: { |
| 952 | // TODO: Consider what should happen to Portals. For now we exclude them. |
| 953 | break; |
| 954 | } |
| 955 | case OffscreenComponent: { |
| 956 | const newState: OffscreenState | null = finishedWork.memoizedState; |
| 957 | const isHidden = newState !== null; |
| 958 | if (!isHidden) { |
| 959 | // Only insert clones if this tree is going to be visible. No need to |
| 960 | // clone invisible content. |
| 961 | // TODO: If this is visible but detached it should still be cloned. |
| 962 | let nextPhase: VisitPhase; |
| 963 | if (visitPhase === CLONE_UPDATE && (flags & Visibility) !== NoFlags) { |
| 964 | // This is the root of an appear. We need to trigger Enter transitions. |
| 965 | nextPhase = CLONE_EXIT; |
| 966 | } else { |
| 967 | nextPhase = visitPhase; |
| 968 | } |
| 969 | recursivelyInsertClones( |
| 970 | finishedWork, |
| 971 | hostParentClone, |
| 972 | parentViewTransition, |
| 973 | nextPhase, |
| 974 | ); |
| 975 | // $FlowFixMe[invalid-compare] |
| 976 | } else if (current !== null && current.memoizedState === null) { |
| 977 | // Was previously mounted as visible but is now hidden. |
| 978 | trackEnterViewTransitions(current); |
| 979 | // Normally we would only mark something as triggering a mutation if there was |
| 980 | // actually a HostInstance below here. If this tree didn't contain a HostInstances |
| 981 | // we shouldn't trigger a mutation even though a virtual component was hidden. |
| 982 | trackHostMutation(); |
| 983 | } |
| 984 | break; |
| 985 | } |
| 986 | case ViewTransitionComponent: |
| 987 | const prevMutationContext = pushMutationContext(); |
| 988 | const viewTransitionState: ViewTransitionState = finishedWork.stateNode; |
| 989 | // TODO: If this was already cloned by a previous pass we can reuse those clones. |
| 990 | viewTransitionState.clones = null; |
| 991 | let nextPhase: VisitPhase; |
| 992 | if (visitPhase === CLONE_EXIT) { |
| 993 | // This was an Enter of a ViewTransition. We now move onto unhiding the inner |
| 994 | // HostComponents and finding inner pairs. |
| 995 | nextPhase = CLONE_UNHIDE; |
| 996 | // TODO: Mark the name and find a pair. |
| 997 | } else { |
| 998 | nextPhase = visitPhase; |
| 999 | } |
| 1000 | recursivelyInsertClones( |
| 1001 | finishedWork, |
| 1002 | hostParentClone, |
| 1003 | viewTransitionState, |
| 1004 | nextPhase, |
| 1005 | ); |
| 1006 | if (viewTransitionMutationContext) { |
| 1007 | // Track that this boundary had a mutation and therefore needs to animate |
| 1008 | // whether it resized or not. |
| 1009 | finishedWork.flags |= Update; |
| 1010 | } |
| 1011 | // After we've collected the cloned instances, we can apply exit or share transitions |
| 1012 | // to them. |
| 1013 | if (visitPhase === CLONE_EXIT) { |
| 1014 | applyExitViewTransition(finishedWork); |
| 1015 | } else if ( |
| 1016 | visitPhase === CLONE_APPEARING_PAIR || |
| 1017 | visitPhase === CLONE_UNHIDE |
| 1018 | ) { |
| 1019 | applyAppearingPairViewTransition(finishedWork); |
| 1020 | } else if (visitPhase === CLONE_UPDATE) { |
| 1021 | applyUpdateViewTransition(current, finishedWork); |
| 1022 | } |
| 1023 | popMutationContext(prevMutationContext); |
| 1024 | break; |
| 1025 | default: { |
| 1026 | recursivelyInsertClones( |
| 1027 | finishedWork, |
| 1028 | hostParentClone, |
| 1029 | parentViewTransition, |
| 1030 | visitPhase, |
| 1031 | ); |
| 1032 | break; |
| 1033 | } |
| 1034 | } |
| 1035 | } |
| 1036 | |
| 1037 | // Clone View Transition boundaries that have any mutations or might have had their |
| 1038 | // layout affected by child insertions. |
| 1039 | export function insertDestinationClones( |
| 1040 | root: FiberRoot, |
| 1041 | finishedWork: Fiber, |
| 1042 | ): void { |
| 1043 | // We'll either not transition the root, or we'll transition the clone. Regardless |
| 1044 | // we cancel the root view transition name. |
| 1045 | const needsClone = detectMutationOrInsertClones(finishedWork); |
| 1046 | if (needsClone) { |
| 1047 | // Clone the whole root |
| 1048 | const rootClone = cloneRootViewTransitionContainer(root.containerInfo); |
| 1049 | root.gestureClone = rootClone; |
| 1050 | recursivelyInsertClones(finishedWork, rootClone, null, CLONE_UPDATE); |
| 1051 | } else { |
| 1052 | root.gestureClone = null; |
| 1053 | cancelRootViewTransitionName(root.containerInfo); |
| 1054 | } |
| 1055 | } |
| 1056 | |
| 1057 | function measureExitViewTransitions(placement: Fiber): void { |
| 1058 | if (placement.tag === ViewTransitionComponent) { |
| 1059 | // const state: ViewTransitionState = placement.stateNode; |
| 1060 | const props: ViewTransitionProps = placement.memoizedProps; |
| 1061 | const name = props.name; |
| 1062 | if (name != null && name !== 'auto') { |
| 1063 | // TODO: Find a pair |
| 1064 | } |
| 1065 | } else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) { |
| 1066 | // TODO: Check if this is a hidden Offscreen or a Portal. |
| 1067 | let child = placement.child; |
| 1068 | while (child !== null) { |
| 1069 | measureExitViewTransitions(child); |
| 1070 | child = child.sibling; |
| 1071 | } |
| 1072 | } else { |
| 1073 | // We don't need to find pairs here because we would've already found and |
| 1074 | // measured the pairs inside the deletion phase. |
| 1075 | } |
| 1076 | } |
| 1077 | |
| 1078 | function recursivelyRestoreNew( |
| 1079 | finishedWork: Fiber, |
| 1080 | nearestMountedAncestor: Fiber, |
| 1081 | ): void { |
| 1082 | // There has to be move a Placement AND an Update flag somewhere below for this |
| 1083 | // pass to be relevant since we only apply insertion effects for new components here. |
| 1084 | if (((Placement | Update) & finishedWork.subtreeFlags) !== NoFlags) { |
| 1085 | let child = finishedWork.child; |
| 1086 | while (child !== null) { |
| 1087 | recursivelyRestoreNew(child, nearestMountedAncestor); |
| 1088 | child = child.sibling; |
| 1089 | } |
| 1090 | } |
| 1091 | switch (finishedWork.tag) { |
| 1092 | case FunctionComponent: |
| 1093 | case ForwardRef: |
| 1094 | case MemoComponent: |
| 1095 | case SimpleMemoComponent: { |
| 1096 | const current = finishedWork.alternate; |
| 1097 | if (current === null && finishedWork.flags & Update) { |
| 1098 | // Insertion Effects are mounted temporarily during the rendering of the snapshot. |
| 1099 | // We have now already takes a snapshot of the inserted state so we can now unmount |
| 1100 | // them to get back into the original state before starting the animation. |
| 1101 | commitHookEffectListUnmount( |
| 1102 | HookInsertion | HookHasEffect, |
| 1103 | finishedWork, |
| 1104 | nearestMountedAncestor, |
| 1105 | ); |
| 1106 | } |
| 1107 | break; |
| 1108 | } |
| 1109 | } |
| 1110 | } |
| 1111 | |
| 1112 | function recursivelyApplyViewTransitions(parentFiber: Fiber) { |
| 1113 | const deletions = parentFiber.deletions; |
| 1114 | if (deletions !== null) { |
| 1115 | for (let i = 0; i < deletions.length; i++) { |
| 1116 | const childToDelete = deletions[i]; |
| 1117 | commitEnterViewTransitions(childToDelete, true); |
| 1118 | } |
| 1119 | } |
| 1120 | |
| 1121 | if ( |
| 1122 | parentFiber.alternate === null || |
| 1123 | (parentFiber.subtreeFlags & MutationMask) !== NoFlags |
| 1124 | ) { |
| 1125 | // If we have mutations or if this is a newly inserted tree, clone as we go. |
| 1126 | let child = parentFiber.child; |
| 1127 | while (child !== null) { |
| 1128 | const current = child.alternate; |
| 1129 | if (current === null) { |
| 1130 | measureExitViewTransitions(child); |
| 1131 | recursivelyRestoreNew(child, parentFiber); |
| 1132 | } else { |
| 1133 | applyViewTransitionsOnFiber(child, current); |
| 1134 | } |
| 1135 | child = child.sibling; |
| 1136 | } |
| 1137 | } else { |
| 1138 | // Nothing has changed in this subtree, but the parent may have still affected |
| 1139 | // its size and position. We need to measure the old and new state to see if |
| 1140 | // we should animate its size and position. |
| 1141 | measureNestedViewTransitions(parentFiber, true); |
| 1142 | } |
| 1143 | } |
| 1144 | |
| 1145 | function applyViewTransitionsOnFiber(finishedWork: Fiber, current: Fiber) { |
| 1146 | // The effect flag should be checked *after* we refine the type of fiber, |
| 1147 | // because the fiber tag is more specific. An exception is any flag related |
| 1148 | // to reconciliation, because those can be set on all fiber types. |
| 1149 | switch (finishedWork.tag) { |
| 1150 | case HostPortal: { |
| 1151 | // TODO: Consider what should happen to Portals. For now we exclude them. |
| 1152 | break; |
| 1153 | } |
| 1154 | case OffscreenComponent: { |
| 1155 | const newState: OffscreenState | null = finishedWork.memoizedState; |
| 1156 | const isHidden = newState !== null; |
| 1157 | const wasHidden = current.memoizedState !== null; |
| 1158 | if (!isHidden) { |
| 1159 | if (wasHidden) { |
| 1160 | measureExitViewTransitions(finishedWork); |
| 1161 | recursivelyRestoreNew(finishedWork, finishedWork); |
| 1162 | } else { |
| 1163 | recursivelyApplyViewTransitions(finishedWork); |
| 1164 | } |
| 1165 | } else { |
| 1166 | if (!wasHidden) { |
| 1167 | // Was previously mounted as visible but is now hidden. |
| 1168 | commitEnterViewTransitions(current, true); |
| 1169 | } |
| 1170 | } |
| 1171 | break; |
| 1172 | } |
| 1173 | case ViewTransitionComponent: { |
| 1174 | const prevContextChanged = viewTransitionContextChanged; |
| 1175 | const prevCancelableChildren = pushViewTransitionCancelableScope(); |
| 1176 | viewTransitionContextChanged = false; |
| 1177 | recursivelyApplyViewTransitions(finishedWork); |
| 1178 | |
| 1179 | if (viewTransitionContextChanged) { |
| 1180 | finishedWork.flags |= Update; |
| 1181 | } |
| 1182 | |
| 1183 | const inViewport = measureUpdateViewTransition( |
| 1184 | current, |
| 1185 | finishedWork, |
| 1186 | true, |
| 1187 | ); |
| 1188 | |
| 1189 | if ((finishedWork.flags & Update) === NoFlags || !inViewport) { |
| 1190 | // If this boundary didn't update, then we may be able to cancel its children. |
| 1191 | // We bubble them up to the parent set to be determined later if we can cancel. |
| 1192 | // Similarly, if old and new state was outside the viewport, we can skip it |
| 1193 | // even if it did update. |
| 1194 | if (prevCancelableChildren === null) { |
| 1195 | // Bubbling up this whole set to the parent. |
| 1196 | } else { |
| 1197 | // Merge with parent set. |
| 1198 | // $FlowFixMe[method-unbinding] |
| 1199 | prevCancelableChildren.push.apply( |
| 1200 | prevCancelableChildren, |
| 1201 | viewTransitionCancelableChildren, |
| 1202 | ); |
| 1203 | popViewTransitionCancelableScope(prevCancelableChildren); |
| 1204 | } |
| 1205 | // TODO: If this doesn't end up canceled, because a parent animates, |
| 1206 | // then we should probably issue an event since this instance is part of it. |
| 1207 | } else { |
| 1208 | const props: ViewTransitionProps = finishedWork.memoizedProps; |
| 1209 | scheduleGestureTransitionEvent(finishedWork, props.onGestureUpdate); |
| 1210 | // If this boundary did update, we cannot cancel its children so those are dropped. |
| 1211 | popViewTransitionCancelableScope(prevCancelableChildren); |
| 1212 | } |
| 1213 | |
| 1214 | if ((finishedWork.flags & AffectedParentLayout) !== NoFlags) { |
| 1215 | // This boundary changed size in a way that may have caused its parent to |
| 1216 | // relayout. We need to bubble this information up to the parent. |
| 1217 | viewTransitionContextChanged = true; |
| 1218 | } else { |
| 1219 | // Otherwise, we restore it to whatever the parent had found so far. |
| 1220 | viewTransitionContextChanged = prevContextChanged; |
| 1221 | } |
| 1222 | |
| 1223 | const viewTransitionState: ViewTransitionState = finishedWork.stateNode; |
| 1224 | viewTransitionState.clones = null; // Reset |
| 1225 | break; |
| 1226 | } |
| 1227 | default: { |
| 1228 | recursivelyApplyViewTransitions(finishedWork); |
| 1229 | break; |
| 1230 | } |
| 1231 | } |
| 1232 | } |
| 1233 | |
| 1234 | // Revert insertions and apply view transition names to the "new" (current) state. |
| 1235 | export function applyDepartureTransitions( |
| 1236 | root: FiberRoot, |
| 1237 | finishedWork: Fiber, |
| 1238 | ): void { |
| 1239 | // First measure and apply view-transition-names to the "new" states. |
| 1240 | viewTransitionContextChanged = false; |
| 1241 | pushViewTransitionCancelableScope(); |
| 1242 | |
| 1243 | recursivelyApplyViewTransitions(finishedWork); |
| 1244 | |
| 1245 | // Then remove the clones. |
| 1246 | const rootClone = root.gestureClone; |
| 1247 | if (rootClone !== null) { |
| 1248 | root.gestureClone = null; |
| 1249 | removeRootViewTransitionClone(root.containerInfo, rootClone); |
| 1250 | } |
| 1251 | |
| 1252 | if (!viewTransitionContextChanged) { |
| 1253 | // If we didn't leak any resizing out to the root, we don't have to transition |
| 1254 | // the root itself. This means that we can now safely cancel any cancellations |
| 1255 | // that bubbled all the way up. |
| 1256 | const cancelableChildren = viewTransitionCancelableChildren; |
| 1257 | if (cancelableChildren !== null) { |
| 1258 | for (let i = 0; i < cancelableChildren.length; i += 3) { |
| 1259 | cancelViewTransitionName( |
| 1260 | cancelableChildren[i] as any as Instance, |
| 1261 | cancelableChildren[i + 1] as any as string, |
| 1262 | cancelableChildren[i + 2] as any as Props, |
| 1263 | ); |
| 1264 | } |
| 1265 | } |
| 1266 | // We also cancel the root itself. First we restore the name to the documentElement |
| 1267 | // and then we cancel it. |
| 1268 | restoreRootViewTransitionName(root.containerInfo); |
| 1269 | cancelRootViewTransitionName(root.containerInfo); |
| 1270 | } |
| 1271 | popViewTransitionCancelableScope(null); |
| 1272 | } |
| 1273 | |
| 1274 | function recursivelyRestoreViewTransitions(parentFiber: Fiber) { |
| 1275 | const deletions = parentFiber.deletions; |
| 1276 | if (deletions !== null) { |
| 1277 | for (let i = 0; i < deletions.length; i++) { |
| 1278 | const childToDelete = deletions[i]; |
| 1279 | restoreEnterOrExitViewTransitions(childToDelete); |
| 1280 | } |
| 1281 | } |
| 1282 | |
| 1283 | if ( |
| 1284 | parentFiber.alternate === null || |
| 1285 | (parentFiber.subtreeFlags & MutationMask) !== NoFlags |
| 1286 | ) { |
| 1287 | // If we have mutations or if this is a newly inserted tree, clone as we go. |
| 1288 | let child = parentFiber.child; |
| 1289 | while (child !== null) { |
| 1290 | restoreViewTransitionsOnFiber(child); |
| 1291 | child = child.sibling; |
| 1292 | } |
| 1293 | } else { |
| 1294 | // Nothing has changed in this subtree, but the parent may have still affected |
| 1295 | // its size and position. We need to measure the old and new state to see if |
| 1296 | // we should animate its size and position. |
| 1297 | restoreNestedViewTransitions(parentFiber); |
| 1298 | } |
| 1299 | } |
| 1300 | |
| 1301 | function restoreViewTransitionsOnFiber(finishedWork: Fiber) { |
| 1302 | const current = finishedWork.alternate; |
| 1303 | if (current === null) { |
| 1304 | restoreEnterOrExitViewTransitions(finishedWork); |
| 1305 | return; |
| 1306 | } |
| 1307 | |
| 1308 | const flags = finishedWork.flags; |
| 1309 | // The effect flag should be checked *after* we refine the type of fiber, |
| 1310 | // because the fiber tag is more specific. An exception is any flag related |
| 1311 | // to reconciliation, because those can be set on all fiber types. |
| 1312 | switch (finishedWork.tag) { |
| 1313 | case HostPortal: { |
| 1314 | // TODO: Consider what should happen to Portals. For now we exclude them. |
| 1315 | break; |
| 1316 | } |
| 1317 | case OffscreenComponent: { |
| 1318 | if (flags & Visibility) { |
| 1319 | const newState: OffscreenState | null = finishedWork.memoizedState; |
| 1320 | const isHidden = newState !== null; |
| 1321 | if (!isHidden) { |
| 1322 | restoreEnterOrExitViewTransitions(finishedWork); |
| 1323 | // $FlowFixMe[invalid-compare] |
| 1324 | } else if (current !== null && current.memoizedState === null) { |
| 1325 | // Was previously mounted as visible but is now hidden. |
| 1326 | restoreEnterOrExitViewTransitions(current); |
| 1327 | } |
| 1328 | } |
| 1329 | break; |
| 1330 | } |
| 1331 | case ViewTransitionComponent: |
| 1332 | restoreUpdateViewTransitionForGesture(current, finishedWork); |
| 1333 | recursivelyRestoreViewTransitions(finishedWork); |
| 1334 | break; |
| 1335 | default: { |
| 1336 | recursivelyRestoreViewTransitions(finishedWork); |
| 1337 | break; |
| 1338 | } |
| 1339 | } |
| 1340 | } |
| 1341 | |
| 1342 | // Revert transition names and start/adjust animations on the started View Transition. |
| 1343 | export function startGestureAnimations( |
| 1344 | root: FiberRoot, |
| 1345 | finishedWork: Fiber, |
| 1346 | ): void { |
| 1347 | restoreViewTransitionsOnFiber(finishedWork); |
| 1348 | restoreRootViewTransitionName(root.containerInfo); |
| 1349 | } |