@@ -283,6 +283,7 @@ export let shouldFireAfterActiveInstanceBlur: boolean = false;
283
283
// Used during the commit phase to track whether a parent ViewTransition component
284
284
// might have been affected by any mutations / relayouts below.
285
285
let viewTransitionContextChanged : boolean = false ;
286
+ let rootViewTransitionAffected : boolean = false ;
286
287
287
288
export function commitBeforeMutationEffects (
288
289
root : FiberRoot ,
@@ -1750,6 +1751,8 @@ export function commitMutationEffects(
1750
1751
inProgressLanes = committedLanes ;
1751
1752
inProgressRoot = root ;
1752
1753
1754
+ rootViewTransitionAffected = false ;
1755
+
1753
1756
resetComponentEffectTimers ( ) ;
1754
1757
1755
1758
commitMutationEffectsOnFiber ( finishedWork , root , committedLanes ) ;
@@ -2068,6 +2071,7 @@ function commitMutationEffectsOnFiber(
2068
2071
break ;
2069
2072
}
2070
2073
case HostPortal : {
2074
+ const prevMutationContext = pushMutationContext ( ) ;
2071
2075
if ( supportsResources ) {
2072
2076
const previousHoistableRoot = currentHoistableRoot ;
2073
2077
currentHoistableRoot = getHoistableRoot (
@@ -2080,6 +2084,14 @@ function commitMutationEffectsOnFiber(
2080
2084
recursivelyTraverseMutationEffects ( root , finishedWork , lanes ) ;
2081
2085
commitReconciliationEffects ( finishedWork , lanes ) ;
2082
2086
}
2087
+ if ( viewTransitionMutationContext ) {
2088
+ // A Portal doesn't necessarily exist within the context of this subtree.
2089
+ // Ideally we would track which React ViewTransition component nests the container
2090
+ // but that's costly. Instead, we treat each Portal as if it's a new React root.
2091
+ // Therefore any leaked mutation means that the root should animate.
2092
+ rootViewTransitionAffected = true ;
2093
+ }
2094
+ popMutationContext ( prevMutationContext ) ;
2083
2095
2084
2096
if ( flags & Update ) {
2085
2097
if ( supportsPersistence ) {
@@ -2432,7 +2444,7 @@ function commitAfterMutationEffectsOnFiber(
2432
2444
viewTransitionContextChanged = false ;
2433
2445
pushViewTransitionCancelableScope ( ) ;
2434
2446
recursivelyTraverseAfterMutationEffects ( root , finishedWork , lanes ) ;
2435
- if ( ! viewTransitionContextChanged ) {
2447
+ if ( ! viewTransitionContextChanged && ! rootViewTransitionAffected ) {
2436
2448
// If we didn't leak any resizing out to the root, we don't have to transition
2437
2449
// the root itself. This means that we can now safely cancel any cancellations
2438
2450
// that bubbled all the way up.
@@ -2456,6 +2468,20 @@ function commitAfterMutationEffectsOnFiber(
2456
2468
recursivelyTraverseAfterMutationEffects ( root , finishedWork , lanes ) ;
2457
2469
break ;
2458
2470
}
2471
+ case HostPortal : {
2472
+ const prevContextChanged = viewTransitionContextChanged ;
2473
+ viewTransitionContextChanged = false ;
2474
+ recursivelyTraverseAfterMutationEffects ( root , finishedWork , lanes ) ;
2475
+ if ( viewTransitionContextChanged ) {
2476
+ // A Portal doesn't necessarily exist within the context of this subtree.
2477
+ // Ideally we would track which React ViewTransition component nests the container
2478
+ // but that's costly. Instead, we treat each Portal as if it's a new React root.
2479
+ // Therefore any leaked resize of a child could affect the root so the root should animate.
2480
+ rootViewTransitionAffected = true ;
2481
+ }
2482
+ viewTransitionContextChanged = prevContextChanged ;
2483
+ break ;
2484
+ }
2459
2485
case OffscreenComponent : {
2460
2486
const isModernRoot =
2461
2487
disableLegacyMode || ( finishedWork . mode & ConcurrentMode ) !== NoMode ;
0 commit comments