diff --git a/fixtures/view-transition/src/components/Page.css b/fixtures/view-transition/src/components/Page.css index e7d876681d..06100a53e8 100644 --- a/fixtures/view-transition/src/components/Page.css +++ b/fixtures/view-transition/src/components/Page.css @@ -20,4 +20,11 @@ border: 0px; border-radius: 5px; padding: 10px; +} + +.portal { + position: fixed; + top: 10px; + left: 360px; + border: 1px solid #ccc; } \ No newline at end of file diff --git a/fixtures/view-transition/src/components/Page.js b/fixtures/view-transition/src/components/Page.js index d7b57c5110..9c158d5c77 100644 --- a/fixtures/view-transition/src/components/Page.js +++ b/fixtures/view-transition/src/components/Page.js @@ -6,7 +6,9 @@ import React, { useEffect, useState, useId, + startTransition, } from 'react'; +import {createPortal} from 'react-dom'; import SwipeRecognizer from './SwipeRecognizer'; @@ -79,6 +81,23 @@ export default function Page({url, navigate}) { // }); }, [show]); + const [showModal, setShowModal] = useState(false); + const portal = showModal ? ( + createPortal( +
+ Portal: {!show ? 'A' : 'B'} + +
{!show ? 'A' : 'B'}
+
+
, + document.body + ) + ) : ( + + ); + const exclamation = ( ! @@ -153,6 +172,7 @@ export default function Page({url, navigate}) {

content

out

of

+ {portal}

the

viewport

{show ? : null} diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 5b27c8e494..700e12b329 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -283,6 +283,7 @@ export let shouldFireAfterActiveInstanceBlur: boolean = false; // Used during the commit phase to track whether a parent ViewTransition component // might have been affected by any mutations / relayouts below. let viewTransitionContextChanged: boolean = false; +let rootViewTransitionAffected: boolean = false; export function commitBeforeMutationEffects( root: FiberRoot, @@ -1750,6 +1751,8 @@ export function commitMutationEffects( inProgressLanes = committedLanes; inProgressRoot = root; + rootViewTransitionAffected = false; + resetComponentEffectTimers(); commitMutationEffectsOnFiber(finishedWork, root, committedLanes); @@ -2068,6 +2071,7 @@ function commitMutationEffectsOnFiber( break; } case HostPortal: { + const prevMutationContext = pushMutationContext(); if (supportsResources) { const previousHoistableRoot = currentHoistableRoot; currentHoistableRoot = getHoistableRoot( @@ -2080,6 +2084,14 @@ function commitMutationEffectsOnFiber( recursivelyTraverseMutationEffects(root, finishedWork, lanes); commitReconciliationEffects(finishedWork, lanes); } + if (viewTransitionMutationContext) { + // A Portal doesn't necessarily exist within the context of this subtree. + // Ideally we would track which React ViewTransition component nests the container + // but that's costly. Instead, we treat each Portal as if it's a new React root. + // Therefore any leaked mutation means that the root should animate. + rootViewTransitionAffected = true; + } + popMutationContext(prevMutationContext); if (flags & Update) { if (supportsPersistence) { @@ -2432,7 +2444,7 @@ function commitAfterMutationEffectsOnFiber( viewTransitionContextChanged = false; pushViewTransitionCancelableScope(); recursivelyTraverseAfterMutationEffects(root, finishedWork, lanes); - if (!viewTransitionContextChanged) { + if (!viewTransitionContextChanged && !rootViewTransitionAffected) { // If we didn't leak any resizing out to the root, we don't have to transition // the root itself. This means that we can now safely cancel any cancellations // that bubbled all the way up. @@ -2456,6 +2468,20 @@ function commitAfterMutationEffectsOnFiber( recursivelyTraverseAfterMutationEffects(root, finishedWork, lanes); break; } + case HostPortal: { + const prevContextChanged = viewTransitionContextChanged; + viewTransitionContextChanged = false; + recursivelyTraverseAfterMutationEffects(root, finishedWork, lanes); + if (viewTransitionContextChanged) { + // A Portal doesn't necessarily exist within the context of this subtree. + // Ideally we would track which React ViewTransition component nests the container + // but that's costly. Instead, we treat each Portal as if it's a new React root. + // Therefore any leaked resize of a child could affect the root so the root should animate. + rootViewTransitionAffected = true; + } + viewTransitionContextChanged = prevContextChanged; + break; + } case OffscreenComponent: { const isModernRoot = disableLegacyMode || (finishedWork.mode & ConcurrentMode) !== NoMode;