@samitouri / QOS-React / commits / a8c2bbdabf

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.

Sebastian Markbåge committed Mar 10, 2025 at 15:27 UTC a8c2bbdabf561a53b7908c2b341815e64840e8ca
1 file changed +26 -14
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+26 -14
@@ -1550,6 +1550,25 @@ export function hasInstanceAffectedParent(
1550 return oldRect.height !== newRect.height || oldRect.width !== newRect.width;
1551 }
1552
1553 +function cancelAllViewTransitionAnimations(scope: Element) {
1554 + // In Safari, we need to manually cancel all manually start animations
1555 + // or it'll block or interfer with future transitions.
1556 + const animations = scope.getAnimations({subtree: true});
1557 + for (let i = 0; i < animations.length; i++) {
1558 + const anim = animations[i];
1559 + const effect: KeyframeEffect = (anim.effect: any);
1560 + // $FlowFixMe
1561 + const pseudo: ?string = effect.pseudoElement;
1562 + if (
1563 + pseudo != null &&
1564 + pseudo.startsWith('::view-transition') &&
1565 + effect.target === scope
1566 + ) {
1567 + anim.cancel();
1568 + }
1569 + }
1570 +}
1571 +
1572 // How long to wait for new fonts to load before just committing anyway.
1573 // This freezes the screen. It needs to be short enough that it doesn't cause too much of
1574 // 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(
1659 }
1660 transition.ready.then(spawnedWorkCallback, spawnedWorkCallback);
1661 transition.finished.then(() => {
1662 + cancelAllViewTransitionAnimations((ownerDocument.documentElement: any));
1663 // $FlowFixMe[prop-missing]
1664 if (ownerDocument.__reactViewTransition === transition) {
1665 // $FlowFixMe[prop-missing]
@@ -1817,12 +1837,16 @@ export function startGestureTransition(
1837 }
1838 for (let i = 0; i < animations.length; i++) {
1839 const anim = animations[i];
1840 + if (anim.playState !== 'running') {
1841 + continue;
1842 + }
1843 const effect: KeyframeEffect = (anim.effect: any);
1844 // $FlowFixMe
1845 const pseudoElement: ?string = effect.pseudoElement;
1846 if (
1847 pseudoElement != null &&
1825 - pseudoElement.startsWith('::view-transition')
1848 + pseudoElement.startsWith('::view-transition') &&
1849 + effect.target === documentElement
1850 ) {
1851 // Ideally we could mutate the existing animation but unfortunately
1852 // the mutable APIs seem less tested and therefore are lacking or buggy.
@@ -1913,19 +1937,7 @@ export function startGestureTransition(
1937 : readyCallback;
1938 transition.ready.then(readyForAnimations, readyCallback);
1939 transition.finished.then(() => {
1916 - // In Safari, we need to manually cancel all manually start animations
1917 - // or it'll block future transitions.
1918 - const documentElement: Element = (ownerDocument.documentElement: any);
1919 - const animations = documentElement.getAnimations({subtree: true});
1920 - for (let i = 0; i < animations.length; i++) {
1921 - const anim = animations[i];
1922 - const effect: KeyframeEffect = (anim.effect: any);
1923 - // $FlowFixMe
1924 - const pseudo: ?string = effect.pseudoElement;
1925 - if (pseudo != null && pseudo.startsWith('::view-transition')) {
1926 - anim.cancel();
1927 - }
1928 - }
1940 + cancelAllViewTransitionAnimations((ownerDocument.documentElement: any));
1941 // $FlowFixMe[prop-missing]
1942 if (ownerDocument.__reactViewTransition === transition) {
1943 // $FlowFixMe[prop-missing]