Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfixes for back navigation in the view transition client-side router #8491

Merged
merged 3 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/witty-readers-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Bugfixes for back navigation in the view transition client-side router
23 changes: 15 additions & 8 deletions packages/astro/components/ViewTransitions.astro
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@ const { fallback = 'animate' } = Astro.props as Props;
return;
}

// Now we are sure that we will push state, and it is time to create a state if it is still missing.
!state && history.replaceState({ index: currentHistoryIndex, scrollY }, '');

document.documentElement.dataset.astroTransition = dir;
if (supportsViewTransitions) {
finished = document.startViewTransition(() => updateDOM(doc, loc, state)).finished;
Expand Down Expand Up @@ -335,28 +338,28 @@ const { fallback = 'animate' } = Astro.props as Props;
// 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' });
// push state on the first navigation but not if we were here already
if (location.hash) {
// last target was different
history.replaceState({ index: currentHistoryIndex, scrollY: -(scrollY + 1) }, '');
const newState: State = { index: ++currentHistoryIndex, scrollY: 0 };
history.pushState(newState, '', link.href);
}
scrollTo({ left: 0, top: 0, behavior: 'instant' });
return;
}
}

// these are the cases we will handle: same origin, different page
ev.preventDefault();
persistState({ index: currentHistoryIndex, scrollY });
navigate('forward', new URL(link.href));
});

addEventListener('popstate', (ev) => {
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)
// The current page doesn't have View Transitions enabled
// but the page we navigate to does (because it set the state).
// Do a full page refresh to reload the client-side router from the new page.
// Scroll restauration will then happen during the reload when the router's code is re-executed
history.scrollRestoration && (history.scrollRestoration = 'manual');
location.reload();
return;
Expand All @@ -383,7 +386,11 @@ const { fallback = 'animate' } = Astro.props as Props;
const nextIndex = state.index;
const direction: Direction = nextIndex > currentHistoryIndex ? 'forward' : 'back';
currentHistoryIndex = nextIndex;
navigate(direction, new URL(location.href), state);
if (state.scrollY < 0) {
scrollTo(0, -(state.scrollY + 1));
} else {
navigate(direction, new URL(location.href), state);
}
});

['mouseenter', 'touchstart', 'focus'].forEach((evName) => {
Expand Down
22 changes: 22 additions & 0 deletions packages/astro/e2e/view-transitions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,28 @@ test.describe('View Transitions', () => {
await expect(locator).toBeInViewport();
});

test('Scroll position restored when transitioning back to fragment', async ({ page, astro }) => {
// Go to the long page
await page.goto(astro.resolveUrl('/long-page'));
let locator = page.locator('#longpage');
await expect(locator).toBeInViewport();

// Scroll down to middle fragment
await page.click('#click-scroll-down');
locator = page.locator('#click-one-again');
await expect(locator).toBeInViewport();

// Scroll up to top fragment
await page.click('#click-one-again');
locator = page.locator('#one');
await expect(locator).toHaveText('Page 1');

// Back to middle of the page
await page.goBack();
locator = page.locator('#click-one-again');
await expect(locator).toBeInViewport();
});

test('Scroll position restored on forward button', async ({ page, astro }) => {
// Go to page 1
await page.goto(astro.resolveUrl('/one'));
Expand Down
Loading