diff --git a/.changeset/common-symbols-teach.md b/.changeset/common-symbols-teach.md new file mode 100644 index 000000000000..55c89ee1b3a9 --- /dev/null +++ b/.changeset/common-symbols-teach.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Instructs the client router to skip view transition animations when the browser is already providing its own visual transition, such as a swipe gesture. diff --git a/packages/astro/src/transitions/router.ts b/packages/astro/src/transitions/router.ts index 06a127e73c5f..e57f30cd7e0e 100644 --- a/packages/astro/src/transitions/router.ts +++ b/packages/astro/src/transitions/router.ts @@ -321,12 +321,13 @@ async function updateDOM( moveToLocation(swapEvent.to, swapEvent.from, options, pageTitleForBrowserHistory, historyState); triggerEvent(TRANSITION_AFTER_SWAP); - if (fallback === 'animate') { - if (!currentTransition.transitionSkipped && !swapEvent.signal.aborted) { - animate('new').finally(() => currentTransition.viewTransitionFinished!()); - } else { - currentTransition.viewTransitionFinished!(); - } + // Resolve the finished promise of the simulation's ViewTransition. + // For 'animate', wait for the new-page animation to complete first. + // For other fallback modes (e.g. 'swap'), resolve immediately — no animation needed. + if (fallback === 'animate' && !currentTransition.transitionSkipped && !swapEvent.signal.aborted) { + animate('new').finally(() => currentTransition.viewTransitionFinished!()); + } else { + currentTransition.viewTransitionFinished?.(); } } @@ -343,6 +344,7 @@ async function transition( to: URL, options: Options, historyState?: State, + hasUAVisualTransition = false, ) { // The most recent navigation always has precedence // Yes, there can be several navigation instances as the user can click links @@ -503,18 +505,21 @@ async function transition( } document.documentElement.setAttribute(DIRECTION_ATTR, prepEvent.direction); - if (supportsViewTransitions) { + if (supportsViewTransitions && !hasUAVisualTransition) { // This automatically cancels any previous transition // We also already took care that the earlier update callback got through currentTransition.viewTransition = document.startViewTransition( async () => await updateDOM(prepEvent, options, currentTransition, historyState), ); } else { - // Simulation mode requires a bit more manual work + // Simulation mode requires a bit more manual work. + // Also used when PopStateEvent.hasUAVisualTransition indicates the browser already + // provided a visual transition (e.g. Safari swipe gesture) — in that case, fallback + // is "swap" to skip animations. const updateDone = (async () => { // Immediately paused to set up the ViewTransition object for Fallback mode await Promise.resolve(); // hop through the micro task queue - await updateDOM(prepEvent, options, currentTransition, historyState, getFallback()); + await updateDOM(prepEvent, options, currentTransition, historyState, hasUAVisualTransition ? 'swap' : getFallback()); return undefined; })(); @@ -612,7 +617,14 @@ function onPopState(ev: PopStateEvent) { const nextIndex = state.index; const direction: Direction = nextIndex > currentHistoryIndex ? 'forward' : 'back'; currentHistoryIndex = nextIndex; - transition(direction, originalLocation, new URL(location.href), {}, state); + transition( + direction, + originalLocation, + new URL(location.href), + {}, + state, + ev.hasUAVisualTransition, + ); } const onScrollEnd = () => {