Cancel finished view transitions Animations manually in fire-and-forget too (#32545)

Otherwise these can survive into the next View Transition and cause
havoc to that transition.

This was appearing as a flash in Safari in the fixture when going from
A->B. This triggers a View Transition and at the same time the scroll
position updates in an effect. That fires a scroll event which starts a
gesture. This shouldn't really happen and the SwipeRecognizer should
ideally ignore those but it's good to surface edge cases. That gesture
is blocked on the View Transition finishing and then immediately after
it starts a gesture View Transition. That gesture then picked up the
former Animation from the previous transition which caused issues. This
PR fixes that flash.
This commit is contained in:
Sebastian Markbåge 2025-03-10 15:27:37 -04:00 committed by GitHub
parent 50ab2dde94
commit a8c2bbdabf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1550,6 +1550,25 @@ export function hasInstanceAffectedParent(
return oldRect.height !== newRect.height || oldRect.width !== newRect.width;
}
function cancelAllViewTransitionAnimations(scope: Element) {
// In Safari, we need to manually cancel all manually start animations
// or it'll block or interfer with future transitions.
const animations = scope.getAnimations({subtree: true});
for (let i = 0; i < animations.length; i++) {
const anim = animations[i];
const effect: KeyframeEffect = (anim.effect: any);
// $FlowFixMe
const pseudo: ?string = effect.pseudoElement;
if (
pseudo != null &&
pseudo.startsWith('::view-transition') &&
effect.target === scope
) {
anim.cancel();
}
}
}
// How long to wait for new fonts to load before just committing anyway.
// This freezes the screen. It needs to be short enough that it doesn't cause too much of
// an issue when it's a new load and slow, yet long enough that you have a chance to load
@ -1640,6 +1659,7 @@ export function startViewTransition(
}
transition.ready.then(spawnedWorkCallback, spawnedWorkCallback);
transition.finished.then(() => {
cancelAllViewTransitionAnimations((ownerDocument.documentElement: any));
// $FlowFixMe[prop-missing]
if (ownerDocument.__reactViewTransition === transition) {
// $FlowFixMe[prop-missing]
@ -1817,12 +1837,16 @@ export function startGestureTransition(
}
for (let i = 0; i < animations.length; i++) {
const anim = animations[i];
if (anim.playState !== 'running') {
continue;
}
const effect: KeyframeEffect = (anim.effect: any);
// $FlowFixMe
const pseudoElement: ?string = effect.pseudoElement;
if (
pseudoElement != null &&
pseudoElement.startsWith('::view-transition')
pseudoElement.startsWith('::view-transition') &&
effect.target === documentElement
) {
// Ideally we could mutate the existing animation but unfortunately
// the mutable APIs seem less tested and therefore are lacking or buggy.
@ -1913,19 +1937,7 @@ export function startGestureTransition(
: readyCallback;
transition.ready.then(readyForAnimations, readyCallback);
transition.finished.then(() => {
// In Safari, we need to manually cancel all manually start animations
// or it'll block future transitions.
const documentElement: Element = (ownerDocument.documentElement: any);
const animations = documentElement.getAnimations({subtree: true});
for (let i = 0; i < animations.length; i++) {
const anim = animations[i];
const effect: KeyframeEffect = (anim.effect: any);
// $FlowFixMe
const pseudo: ?string = effect.pseudoElement;
if (pseudo != null && pseudo.startsWith('::view-transition')) {
anim.cancel();
}
}
cancelAllViewTransitionAnimations((ownerDocument.documentElement: any));
// $FlowFixMe[prop-missing]
if (ownerDocument.__reactViewTransition === transition) {
// $FlowFixMe[prop-missing]