Skip to content

Commit 90c23e4

Browse files
committed
Mark the root as animating if any Portal mutates or resizes
In a way that's not contained by a ViewTransition inside the Portal itself.
1 parent 8039f1b commit 90c23e4

File tree

3 files changed

+54
-1
lines changed

3 files changed

+54
-1
lines changed

Diff for: fixtures/view-transition/src/components/Page.css

+7
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,11 @@
2020
border: 0px;
2121
border-radius: 5px;
2222
padding: 10px;
23+
}
24+
25+
.portal {
26+
position: fixed;
27+
top: 10px;
28+
left: 360px;
29+
border: 1px solid #ccc;
2330
}

Diff for: fixtures/view-transition/src/components/Page.js

+20
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import React, {
66
useEffect,
77
useState,
88
useId,
9+
startTransition,
910
} from 'react';
11+
import {createPortal} from 'react-dom';
1012

1113
import SwipeRecognizer from './SwipeRecognizer';
1214

@@ -79,6 +81,23 @@ export default function Page({url, navigate}) {
7981
// });
8082
}, [show]);
8183

84+
const [showModal, setShowModal] = useState(false);
85+
const portal = showModal ? (
86+
createPortal(
87+
<div className="portal">
88+
Portal: {!show ? 'A' : 'B'}
89+
<ViewTransition>
90+
<div>{!show ? 'A' : 'B'}</div>
91+
</ViewTransition>
92+
</div>,
93+
document.body
94+
)
95+
) : (
96+
<button onClick={() => startTransition(() => setShowModal(true))}>
97+
Show Modal
98+
</button>
99+
);
100+
82101
const exclamation = (
83102
<ViewTransition name="exclamation" onShare={onTransition}>
84103
<span>!</span>
@@ -153,6 +172,7 @@ export default function Page({url, navigate}) {
153172
<p>content</p>
154173
<p>out</p>
155174
<p>of</p>
175+
{portal}
156176
<p>the</p>
157177
<p>viewport</p>
158178
{show ? <Component /> : null}

Diff for: packages/react-reconciler/src/ReactFiberCommitWork.js

+27-1
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ export let shouldFireAfterActiveInstanceBlur: boolean = false;
283283
// Used during the commit phase to track whether a parent ViewTransition component
284284
// might have been affected by any mutations / relayouts below.
285285
let viewTransitionContextChanged: boolean = false;
286+
let rootViewTransitionAffected: boolean = false;
286287

287288
export function commitBeforeMutationEffects(
288289
root: FiberRoot,
@@ -1750,6 +1751,8 @@ export function commitMutationEffects(
17501751
inProgressLanes = committedLanes;
17511752
inProgressRoot = root;
17521753

1754+
rootViewTransitionAffected = false;
1755+
17531756
resetComponentEffectTimers();
17541757

17551758
commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
@@ -2068,6 +2071,7 @@ function commitMutationEffectsOnFiber(
20682071
break;
20692072
}
20702073
case HostPortal: {
2074+
const prevMutationContext = pushMutationContext();
20712075
if (supportsResources) {
20722076
const previousHoistableRoot = currentHoistableRoot;
20732077
currentHoistableRoot = getHoistableRoot(
@@ -2080,6 +2084,14 @@ function commitMutationEffectsOnFiber(
20802084
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
20812085
commitReconciliationEffects(finishedWork, lanes);
20822086
}
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);
20832095

20842096
if (flags & Update) {
20852097
if (supportsPersistence) {
@@ -2432,7 +2444,7 @@ function commitAfterMutationEffectsOnFiber(
24322444
viewTransitionContextChanged = false;
24332445
pushViewTransitionCancelableScope();
24342446
recursivelyTraverseAfterMutationEffects(root, finishedWork, lanes);
2435-
if (!viewTransitionContextChanged) {
2447+
if (!viewTransitionContextChanged && !rootViewTransitionAffected) {
24362448
// If we didn't leak any resizing out to the root, we don't have to transition
24372449
// the root itself. This means that we can now safely cancel any cancellations
24382450
// that bubbled all the way up.
@@ -2456,6 +2468,20 @@ function commitAfterMutationEffectsOnFiber(
24562468
recursivelyTraverseAfterMutationEffects(root, finishedWork, lanes);
24572469
break;
24582470
}
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+
}
24592485
case OffscreenComponent: {
24602486
const isModernRoot =
24612487
disableLegacyMode || (finishedWork.mode & ConcurrentMode) !== NoMode;

0 commit comments

Comments
 (0)