Skip to content

Commit f66053a

Browse files
authored
Transition only between pages where both have ViewTransitions enabled (#8441)
* added e2e test regarding loss of router * only navigate to pages from which we can navigate back * location does not change before deferred pushState * initialize history state * test cases adapted to new semantics (only traverse to pages w/ ViewTransigs) * type URL instead of Location * + changeset
1 parent 7ea32c7 commit f66053a

File tree

5 files changed

+106
-60
lines changed

5 files changed

+106
-60
lines changed

.changeset/curvy-dolls-thank.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Only transition between pages where both have ViewTransitions enabled

packages/astro/components/ViewTransitions.astro

+39-33
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ const { fallback = 'animate' } = Astro.props as Props;
1919
};
2020
type Events = 'astro:page-load' | 'astro:after-swap';
2121

22-
const persistState = (state: State) => history.replaceState(state, '');
22+
// only update history entries that are managed by us
23+
// leave other entries alone and do not accidently add state.
24+
const persistState = (state: State) => history.state && history.replaceState(state, '');
2325
const supportsViewTransitions = !!document.startViewTransition;
2426
const transitionEnabledOnThisPage = () =>
2527
!!document.querySelector('[name="astro-view-transitions-enabled"]');
@@ -32,11 +34,13 @@ const { fallback = 'animate' } = Astro.props as Props;
3234
// can use that to determine popstate if going forward or back.
3335
let currentHistoryIndex = 0;
3436
if (history.state) {
35-
// we reloaded a page with history state (e.g. back button or browser reload)
37+
// we reloaded a page with history state
38+
// (e.g. history navigation from non-transition page or browser reload)
3639
currentHistoryIndex = history.state.index;
3740
scrollTo({ left: 0, top: history.state.scrollY });
41+
} else if (transitionEnabledOnThisPage()) {
42+
history.replaceState({index: currentHistoryIndex, scrollY}, '');
3843
}
39-
4044
const throttle = (cb: (...args: any[]) => any, delay: number) => {
4145
let wait = false;
4246
// During the waiting time additional events are lost.
@@ -109,9 +113,7 @@ const { fallback = 'animate' } = Astro.props as Props;
109113

110114
const parser = new DOMParser();
111115

112-
async function updateDOM(html: string, state?: State, fallback?: Fallback) {
113-
const doc = parser.parseFromString(html, 'text/html');
114-
116+
async function updateDOM(doc: Document, loc: URL, state?: State, fallback?: Fallback) {
115117
// Check for a head element that should persist, either because it has the data
116118
// attribute or is a link el.
117119
const persistedHeadElement = (el: Element): Element | null => {
@@ -189,19 +191,21 @@ const { fallback = 'animate' } = Astro.props as Props;
189191
// Chromium based browsers (Chrome, Edge, Opera, ...)
190192
scrollTo({ left: 0, top: 0, behavior: 'instant' });
191193

192-
if (state?.scrollY === 0 && location.hash) {
193-
const id = decodeURIComponent(location.hash.slice(1));
194+
let initialScrollY = 0;
195+
if (!state && loc.hash) {
196+
const id = decodeURIComponent(loc.hash.slice(1));
194197
const elem = document.getElementById(id);
195198
// prefer scrollIntoView() over scrollTo() because it takes scroll-padding into account
196-
if (elem) {
197-
state.scrollY = elem.offsetTop;
198-
persistState(state); // first guess, later updated by scroll handler
199-
elem.scrollIntoView(); // for Firefox, this should better be {behavior: 'instant'}
200-
}
199+
elem && (initialScrollY = elem.offsetTop) && elem.scrollIntoView();
201200
} else if (state && state.scrollY !== 0) {
202201
scrollTo(0, state.scrollY); // usings default scrollBehavior
203202
}
204-
203+
!state &&
204+
history.pushState(
205+
{ index: ++currentHistoryIndex, scrollY: initialScrollY },
206+
'',
207+
loc.href
208+
);
205209
triggerEvent('astro:after-swap');
206210
};
207211

@@ -247,19 +251,26 @@ const { fallback = 'animate' } = Astro.props as Props;
247251
}
248252
}
249253

250-
async function navigate(dir: Direction, href: string, state?: State) {
254+
async function navigate(dir: Direction, loc: URL, state?: State) {
251255
let finished: Promise<void>;
256+
const href=loc.href;
252257
const { html, ok } = await getHTML(href);
253258
// If there is a problem fetching the new page, just do an MPA navigation to it.
254259
if (!ok) {
255260
location.href = href;
256261
return;
257262
}
263+
const doc = parser.parseFromString(html, 'text/html');
264+
if (!doc.querySelector('[name="astro-view-transitions-enabled"]')) {
265+
location.href = href;
266+
return;
267+
}
268+
258269
document.documentElement.dataset.astroTransition = dir;
259270
if (supportsViewTransitions) {
260-
finished = document.startViewTransition(() => updateDOM(html, state)).finished;
271+
finished = document.startViewTransition(() => updateDOM(doc, loc, state)).finished;
261272
} else {
262-
finished = updateDOM(html, state, getFallback());
273+
finished = updateDOM(doc, loc, state, getFallback());
263274
}
264275
try {
265276
await finished;
@@ -311,11 +322,11 @@ const { fallback = 'animate' } = Astro.props as Props;
311322
ev.shiftKey || // new window
312323
ev.defaultPrevented ||
313324
!transitionEnabledOnThisPage()
314-
)
325+
) {
315326
// No page transitions in these cases,
316327
// Let the browser standard action handle this
317328
return;
318-
329+
}
319330
// We do not need to handle same page links because there are no page transitions
320331
// Same page means same path and same query params (but different hash)
321332
if (location.pathname === link.pathname && location.search === link.search) {
@@ -341,10 +352,8 @@ const { fallback = 'animate' } = Astro.props as Props;
341352

342353
// these are the cases we will handle: same origin, different page
343354
ev.preventDefault();
344-
navigate('forward', link.href, { index: ++currentHistoryIndex, scrollY: 0 });
345-
const newState: State = { index: currentHistoryIndex, scrollY };
346-
persistState({ index: currentHistoryIndex - 1, scrollY });
347-
history.pushState(newState, '', link.href);
355+
persistState({ index: currentHistoryIndex, scrollY });
356+
navigate('forward', new URL(link.href));
348357
});
349358

350359
addEventListener('popstate', (ev) => {
@@ -374,11 +383,11 @@ const { fallback = 'animate' } = Astro.props as Props;
374383
history.scrollRestoration = 'manual';
375384
}
376385

377-
const state: State | undefined = history.state;
378-
const nextIndex = state?.index ?? currentHistoryIndex + 1;
386+
const state: State = history.state;
387+
const nextIndex = state.index;
379388
const direction: Direction = nextIndex > currentHistoryIndex ? 'forward' : 'back';
380-
navigate(direction, location.href, state);
381389
currentHistoryIndex = nextIndex;
390+
navigate(direction, new URL(location.href), state);
382391
});
383392

384393
['mouseenter', 'touchstart', 'focus'].forEach((evName) => {
@@ -402,13 +411,10 @@ const { fallback = 'animate' } = Astro.props as Props;
402411
addEventListener('load', onPageLoad);
403412
// There's not a good way to record scroll position before a back button.
404413
// So the way we do it is by listening to scrollend if supported, and if not continuously record the scroll position.
405-
const updateState = () => {
406-
// only update history entries that are managed by us
407-
// leave other entries alone and do not accidently add state.
408-
if (history.state) {
409-
persistState({ ...history.state, scrollY });
410-
}
411-
};
414+
const updateState = () => {
415+
persistState({ ...history.state, scrollY });
416+
}
417+
412418
if ('onscrollend' in window) addEventListener('scrollend', updateState);
413419
else addEventListener('scroll', throttle(updateState, 300));
414420
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<html>
2+
<head>
3+
<title>Page 5</title>
4+
</head>
5+
<body>
6+
<main>
7+
<p id="five">Page 5</p>
8+
<a id="click-three" href="/two">go to 3</a>
9+
</main>
10+
</body>
11+
</html>

packages/astro/e2e/fixtures/view-transitions/src/pages/three.astro

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
<p id="three">Page 3</p>
88
<a id="click-two" href="/two">go to 2</a>
99
<br/>
10+
<a id="click-five" href="/five">go to 5</a>
11+
<br/>
1012
<a id="click-hash" href="#click-hash">hash target</a>
1113
<p style="height: 150vh">Long paragraph</p>
1214
</main>

packages/astro/e2e/view-transitions.test.js

+49-27
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ test.describe('View Transitions', () => {
8383
expect(loads.length, 'There should only be 1 page load').toEqual(1);
8484
});
8585

86-
test('Moving from a page without ViewTransitions triggers a full page navigation', async ({
86+
test('Moving to a page without ViewTransitions triggers a full page navigation', async ({
8787
page,
8888
astro,
8989
}) => {
@@ -102,10 +102,6 @@ test.describe('View Transitions', () => {
102102
p = page.locator('#three');
103103
await expect(p, 'should have content').toHaveText('Page 3');
104104

105-
await page.click('#click-two');
106-
p = page.locator('#two');
107-
await expect(p, 'should have content').toHaveText('Page 2');
108-
109105
expect(
110106
loads.length,
111107
'There should be 2 page loads. The original, then going from 3 to 2'
@@ -142,8 +138,8 @@ test.describe('View Transitions', () => {
142138

143139
expect(
144140
loads.length,
145-
'There should be only 1 page load. The original, but no additional loads for the hash change'
146-
).toEqual(1);
141+
'There should be only 2 page loads (for page one & three), but no additional loads for the hash change'
142+
).toEqual(2);
147143
});
148144

149145
test('Moving from a page without ViewTransitions w/ back button', async ({ page, astro }) => {
@@ -501,25 +497,51 @@ test.describe('View Transitions', () => {
501497
await page.click('#click-logo');
502498
await downloadPromise;
503499
});
504-
});
505500

506-
test('Scroll position is restored on back navigation from page w/o ViewTransitions', async ({
507-
page,
508-
astro,
509-
}) => {
510-
// Go to middle of long page
511-
await page.goto(astro.resolveUrl('/long-page#click-external'));
512-
513-
let locator = page.locator('#click-external');
514-
await expect(locator).toBeInViewport();
515-
516-
// Go to a page that has not enabled ViewTransistions
517-
await page.click('#click-external');
518-
locator = page.locator('#three');
519-
await expect(locator).toHaveText('Page 3');
520-
521-
// Scroll back to long page
522-
await page.goBack();
523-
locator = page.locator('#click-external');
524-
await expect(locator).toBeInViewport();
501+
test('Scroll position is restored on back navigation from page w/o ViewTransitions', async ({
502+
page,
503+
astro,
504+
}) => {
505+
// Go to middle of long page
506+
await page.goto(astro.resolveUrl('/long-page#click-external'));
507+
508+
let locator = page.locator('#click-external');
509+
await expect(locator).toBeInViewport();
510+
511+
// Go to a page that has not enabled ViewTransistions
512+
await page.click('#click-external');
513+
locator = page.locator('#three');
514+
await expect(locator).toHaveText('Page 3');
515+
516+
// Scroll back to long page
517+
await page.goBack();
518+
locator = page.locator('#click-external');
519+
await expect(locator).toBeInViewport();
520+
});
521+
522+
test("Non transition navigation doesn't loose handlers", async ({ page, astro }) => {
523+
// Go to page 1
524+
await page.goto(astro.resolveUrl('/one'));
525+
let p = page.locator('#one');
526+
await expect(p, 'should have content').toHaveText('Page 1');
527+
528+
// go to page 3
529+
await page.click('#click-three');
530+
p = page.locator('#three');
531+
await expect(p, 'should have content').toHaveText('Page 3');
532+
533+
// go to page 5
534+
await page.click('#click-five');
535+
p = page.locator('#five');
536+
await expect(p, 'should have content').toHaveText('Page 5');
537+
538+
await page.goBack();
539+
p = page.locator('#three');
540+
await expect(p, 'should have content').toHaveText('Page 3');
541+
542+
await page.goBack();
543+
p = page.locator('#one');
544+
await expect(p, 'should have content').toHaveText('Page 1');
545+
});
525546
});
547+

0 commit comments

Comments
 (0)