From cfc465ddebcc58d20f29ecffaa857a77525435a9 Mon Sep 17 00:00:00 2001
From: Martin Trapp <94928215+martrapp@users.noreply.github.com>
Date: Tue, 22 Aug 2023 17:17:30 +0200
Subject: [PATCH 01/11] fix: self link does not trigger page reload (#8182)
---
.changeset/gold-carpets-film.md | 5 ++
.../astro/components/ViewTransitions.astro | 68 +++++++++++++------
.../view-transitions/src/pages/one.astro | 1 +
packages/astro/e2e/view-transitions.test.js | 16 +++++
4 files changed, 68 insertions(+), 22 deletions(-)
create mode 100644 .changeset/gold-carpets-film.md
diff --git a/.changeset/gold-carpets-film.md b/.changeset/gold-carpets-film.md
new file mode 100644
index 000000000..dc17f7ab8
--- /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 4b7a46551..be312b1bf 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 b24338d9d..3f9666c1d 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 7aeb6502a..69fa7c55f 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'));
From fddd4dc71af321bd6b4d01bb4b1b955284846e60 Mon Sep 17 00:00:00 2001
From: Martin Trapp <94928215+martrapp@users.noreply.github.com>
Date: Tue, 22 Aug 2023 18:59:17 +0200
Subject: [PATCH 02/11] Fixes in the client-side router (#8166)
* Fixes in the client-side router
* reverted function declaration after review (#8166)
---------
Co-authored-by: Nate Moore
---
.changeset/chilled-shoes-fail.md | 5 ++
.../astro/components/ViewTransitions.astro | 20 +++---
.../src/pages/half-baked.astro | 24 +++++++
.../view-transitions/src/pages/three.astro | 3 +
packages/astro/e2e/view-transitions.test.js | 64 +++++++++++++++++++
5 files changed, 106 insertions(+), 10 deletions(-)
create mode 100644 .changeset/chilled-shoes-fail.md
create mode 100644 packages/astro/e2e/fixtures/view-transitions/src/pages/half-baked.astro
diff --git a/.changeset/chilled-shoes-fail.md b/.changeset/chilled-shoes-fail.md
new file mode 100644
index 000000000..1567ecca3
--- /dev/null
+++ b/.changeset/chilled-shoes-fail.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+ViewTransitions: Fixes in the client-side router
diff --git a/packages/astro/components/ViewTransitions.astro b/packages/astro/components/ViewTransitions.astro
index be312b1bf..12dfe0f4f 100644
--- a/packages/astro/components/ViewTransitions.astro
+++ b/packages/astro/components/ViewTransitions.astro
@@ -20,15 +20,6 @@ const { fallback = 'animate' } = Astro.props as Props;
type Events = 'astro:load' | 'astro:beforeload';
const persistState = (state: State) => history.replaceState(state, '');
-
- // The History API does not tell you if navigation is forward or back, so
- // you can figure it using an index. On pushState the index is incremented so you
- // can use that to determine popstate if going forward or back.
- let currentHistoryIndex = history.state?.index || 0;
- if (!history.state) {
- persistState({ index: currentHistoryIndex, scrollY: 0 });
- }
-
const supportsViewTransitions = !!document.startViewTransition;
const transitionEnabledOnThisPage = () =>
!!document.querySelector('[name="astro-view-transitions-enabled"]');
@@ -36,6 +27,14 @@ const { fallback = 'animate' } = Astro.props as Props;
const onload = () => triggerEvent('astro:load');
const PERSIST_ATTR = 'data-astro-transition-persist';
+ // The History API does not tell you if navigation is forward or back, so
+ // you can figure it using an index. On pushState the index is incremented so you
+ // can use that to determine popstate if going forward or back.
+ let currentHistoryIndex = history.state?.index || 0;
+ if (!history.state && transitionEnabledOnThisPage()) {
+ persistState({ index: currentHistoryIndex, scrollY: 0 });
+ }
+
const throttle = (cb: (...args: any[]) => any, delay: number) => {
let wait = false;
// During the waiting time additional events are lost.
@@ -323,9 +322,10 @@ const { fallback = 'animate' } = Astro.props as Props;
});
addEventListener('popstate', (ev) => {
- if (!transitionEnabledOnThisPage()) {
+ if (!transitionEnabledOnThisPage() && ev.state) {
// The current page doesn't haven't View Transitions,
// respect that with a full page reload
+ // -- but only for transition managed by us (ev.state is set)
location.reload();
return;
}
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/half-baked.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/half-baked.astro
new file mode 100644
index 000000000..40298d125
--- /dev/null
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/half-baked.astro
@@ -0,0 +1,24 @@
+---
+import { ViewTransitions } from 'astro:transitions';
+
+// For the test fixture, we import the script but we don't use the component
+// While this seems to be some strange mistake,
+// it might be realistic, e.g. in a configurable CommenHead component
+
+interface Props {
+ transitions?: string;
+}
+const { transitions } = Astro.props;
+---
+
+
+ Half-Baked
+ {transitions && }
+
+
+
+ Half Baked
+ hash target
+
+
+
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/three.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/three.astro
index 676e8b61b..eddc049a8 100644
--- a/packages/astro/e2e/fixtures/view-transitions/src/pages/three.astro
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/three.astro
@@ -6,6 +6,9 @@
Page 3
go to 2
+
+ hash target
+ Long paragraph