diff --git a/.changeset/gold-carpets-film.md b/.changeset/gold-carpets-film.md new file mode 100644 index 000000000000..dc17f7ab8bff --- /dev/null +++ b/.changeset/gold-carpets-film.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +View Transitions: self link (`href=""`) does not trigger page reload diff --git a/packages/astro/components/ViewTransitions.astro b/packages/astro/components/ViewTransitions.astro index 4b7a465517ff..be312b1bf25d 100644 --- a/packages/astro/components/ViewTransitions.astro +++ b/packages/astro/components/ViewTransitions.astro @@ -274,30 +274,54 @@ const { fallback = 'animate' } = Astro.props as Props; // that is going to another page within the same origin. Basically it determines // same-origin navigation, but omits special key combos for new tabs, etc. if ( - link && - link instanceof HTMLAnchorElement && - link.href && - (!link.target || link.target === '_self') && - link.origin === location.origin && - !( - // Same page means same path and same query params - (location.pathname === link.pathname && location.search === link.search) - ) && - ev.button === 0 && // left clicks only - !ev.metaKey && // new tab (mac) - !ev.ctrlKey && // new tab (windows) - !ev.altKey && // download - !ev.shiftKey && - !ev.defaultPrevented && - transitionEnabledOnThisPage() - ) { - ev.preventDefault(); - navigate('forward', link.href, { index: ++currentHistoryIndex, scrollY: 0 }); - const newState: State = { index: currentHistoryIndex, scrollY }; - persistState({ index: currentHistoryIndex - 1, scrollY }); - history.pushState(newState, '', link.href); + !link || + !(link instanceof HTMLAnchorElement) || + !link.href || + (link.target && link.target !== '_self') || + link.origin !== location.origin || + ev.button !== 0 || // left clicks only + ev.metaKey || // new tab (mac) + ev.ctrlKey || // new tab (windows) + ev.altKey || // download + ev.shiftKey || // new window + ev.defaultPrevented || + !transitionEnabledOnThisPage() + ) + // No page transitions in these cases, + // Let the browser standard action handle this + return; + + // We do not need to handle same page links because there are no page transitions + // Same page means same path and same query params (but different hash) + if (location.pathname === link.pathname && location.search === link.search) { + if (link.hash) { + // The browser default action will handle navigations with hash fragments + return; + } else { + // Special case: self link without hash + // If handed to the browser it will reload the page + // But we want to handle it like any other same page navigation + // So we scroll to the top of the page but do not start page transitions + ev.preventDefault(); + persistState({ ...history.state, scrollY }); + scrollTo({ left: 0, top: 0, behavior: 'instant' }); + if (location.hash) { + // last target was different + const newState: State = { index: ++currentHistoryIndex, scrollY: 0 }; + history.pushState(newState, '', link.href); + } + return; + } } + + // these are the cases we will handle: same origin, different page + ev.preventDefault(); + navigate('forward', link.href, { index: ++currentHistoryIndex, scrollY: 0 }); + const newState: State = { index: currentHistoryIndex, scrollY }; + persistState({ index: currentHistoryIndex - 1, scrollY }); + history.pushState(newState, '', link.href); }); + addEventListener('popstate', (ev) => { if (!transitionEnabledOnThisPage()) { // The current page doesn't haven't View Transitions, diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/one.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/one.astro index b24338d9d4d7..3f9666c1d6f4 100644 --- a/packages/astro/e2e/fixtures/view-transitions/src/pages/one.astro +++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/one.astro @@ -7,6 +7,7 @@ import Layout from '../components/Layout.astro'; go to 2 go to 3 go to long page + go to top
test content
diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js index 7aeb6502a67a..69fa7c55f578 100644 --- a/packages/astro/e2e/view-transitions.test.js +++ b/packages/astro/e2e/view-transitions.test.js @@ -190,6 +190,22 @@ test.describe('View Transitions', () => { await expect(p, 'should have content').toHaveText('Page 1'); }); + test('click self link (w/o hash) does not do navigation', async ({ page, astro }) => { + const loads = []; + page.addListener('load', (p) => { + loads.push(p.title()); + }); + // Go to page 1 + await page.goto(astro.resolveUrl('/one')); + const p = page.locator('#one'); + await expect(p, 'should have content').toHaveText('Page 1'); + + // Clicking href="" stays on page + await page.click('#click-self'); + await expect(p, 'should have content').toHaveText('Page 1'); + expect(loads.length, 'There should only be 1 page load').toEqual(1); + }); + test('Scroll position restored on back button', async ({ page, astro }) => { // Go to page 1 await page.goto(astro.resolveUrl('/long-page'));