Scroll position restoration (#7800)

* Scroll position restoration

* Adding a changeset

* Throttle state persistence

* very important change
This commit is contained in:
Matthew Phillips 2023-07-28 09:07:00 -04:00 committed by GitHub
parent 46f9848052
commit 49a4b28202
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 162 additions and 10 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Scroll position restoration with ViewTransitions router

View file

@ -13,14 +13,20 @@ const { fallback = 'animate' } = Astro.props as Props;
<script>
type Fallback = 'none' | 'animate' | 'swap';
type Direction = 'forward' | 'back';
type State = {
index: number;
scrollY: number;
};
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) {
history.replaceState({ index: currentHistoryIndex }, document.title);
persistState({ index: currentHistoryIndex, scrollY: 0 });
}
const supportsViewTransitions = !!document.startViewTransition;
@ -29,6 +35,19 @@ const { fallback = 'animate' } = Astro.props as Props;
const triggerEvent = (name: Events) => document.dispatchEvent(new Event(name));
const onload = () => triggerEvent('astro:load');
const throttle = (cb: (...args: any[]) => any, delay: number) => {
let wait = false;
return (...args: any[]) => {
if (wait) return;
cb(...args);
wait = true
setTimeout(() => {
wait = false
}, delay);
}
};
async function getHTML(href: string) {
const res = await fetch(href);
const html = await res.text();
@ -64,11 +83,16 @@ const { fallback = 'animate' } = Astro.props as Props;
const parser = new DOMParser();
async function updateDOM(dir: Direction, html: string, fallback?: Fallback) {
async function updateDOM(dir: Direction, html: string, state?: State, fallback?: Fallback) {
const doc = parser.parseFromString(html, 'text/html');
doc.documentElement.dataset.astroTransition = dir;
const swap = () => {
document.documentElement.replaceWith(doc.documentElement);
if(state?.scrollY != null) {
scrollTo(0, state.scrollY);
}
triggerEvent('astro:beforeload');
};
@ -103,7 +127,7 @@ const { fallback = 'animate' } = Astro.props as Props;
}
}
async function navigate(dir: Direction, href: string) {
async function navigate(dir: Direction, href: string, state?: State) {
let finished: Promise<void>;
const { html, ok } = await getHTML(href);
// If there is a problem fetching the new page, just do an MPA navigation to it.
@ -112,9 +136,9 @@ const { fallback = 'animate' } = Astro.props as Props;
return;
}
if (supportsViewTransitions) {
finished = document.startViewTransition(() => updateDOM(dir, html)).finished;
finished = document.startViewTransition(() => updateDOM(dir, html, state)).finished;
} else {
finished = updateDOM(dir, html, getFallback());
finished = updateDOM(dir, html, state, getFallback());
}
try {
await finished;
@ -165,25 +189,30 @@ const { fallback = 'animate' } = Astro.props as Props;
ev.preventDefault();
navigate('forward', link.href);
currentHistoryIndex++;
history.pushState({ index: currentHistoryIndex }, '', link.href);
const newState: State = { index: currentHistoryIndex, scrollY };
persistState({ index: currentHistoryIndex - 1, scrollY });
history.pushState(newState, '', link.href);
}
});
window.addEventListener('popstate', (ev) => {
addEventListener('popstate', (ev) => {
if (!transitionEnabledOnThisPage()) {
// The current page doesn't haven't View Transitions,
// respect that with a full page reload
location.reload();
return;
}
// hash change creates no state.
if (ev.state === null) {
history.replaceState({ index: currentHistoryIndex }, '');
persistState({ index: currentHistoryIndex, scrollY });
ev.preventDefault();
return;
}
const nextIndex = history.state?.index ?? currentHistoryIndex + 1;
const state: State | undefined = history.state;
const nextIndex = state?.index ?? currentHistoryIndex + 1;
const direction: Direction = nextIndex > currentHistoryIndex ? 'forward' : 'back';
navigate(direction, location.href);
navigate(direction, location.href, state);
currentHistoryIndex = nextIndex;
});
@ -206,5 +235,12 @@ const { fallback = 'animate' } = Astro.props as Props;
);
});
addEventListener('load', onload);
// There's not a good way to record scroll position before a back button.
// So the way we do it is by listening to scroll and just continuously recording it.
addEventListener('scroll', throttle(() => {
if(history.state) {
persistState({ ...history.state, scrollY })
}
}, 300), { passive: true });
}
</script>

View file

@ -12,6 +12,12 @@ const { link } = Astro.props as Props;
<head>
<title>Testing</title>
{link ? <link rel="stylesheet" href={link} > : ''}
<style>
main {
max-width: 900px;
margin: auto;
}
</style>
<ViewTransitions />
<DarkMode />
</head>

View file

@ -0,0 +1,50 @@
---
import Layout from '../components/Layout.astro';
---
<Layout>
<article id="longpage">
<div><a id="click-one" href="/one">go to 1</a></div>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Dictum varius duis at consectetur lorem donec massa sapien faucibus. Amet mauris commodo quis imperdiet massa. Sed pulvinar proin gravida hendrerit lectus a. Magna ac placerat vestibulum lectus. Blandit cursus risus at ultrices mi tempus. Luctus venenatis lectus magna fringilla urna porttitor. Auctor eu augue ut lectus arcu bibendum at varius vel. Tristique senectus et netus et. In fermentum et sollicitudin ac orci phasellus egestas tellus rutrum. Eget lorem dolor sed viverra ipsum nunc aliquet. Amet consectetur adipiscing elit ut aliquam purus. Accumsan lacus vel facilisis volutpat est velit egestas. Felis imperdiet proin fermentum leo vel. Ut tellus elementum sagittis vitae et leo duis ut diam. Nisl pretium fusce id velit. Lorem donec massa sapien faucibus et. Nibh sed pulvinar proin gravida hendrerit lectus a. In est ante in nibh mauris cursus mattis molestie.
Suscipit adipiscing bibendum est ultricies integer quis auctor elit. Sit amet aliquam id diam maecenas ultricies. Pretium lectus quam id leo. Dui id ornare arcu odio ut sem. In aliquam sem fringilla ut morbi. Mus mauris vitae ultricies leo integer malesuada nunc. Nisi scelerisque eu ultrices vitae auctor eu. Pellentesque sit amet porttitor eget dolor. Et netus et malesuada fames. Nisi porta lorem mollis aliquam. Sit amet purus gravida quis blandit turpis cursus. Magna sit amet purus gravida quis blandit turpis cursus in. Nulla aliquet porttitor lacus luctus accumsan tortor. Non nisi est sit amet facilisis magna etiam. Maecenas volutpat blandit aliquam etiam erat velit scelerisque in. Amet porttitor eget dolor morbi non arcu risus.
Porta lorem mollis aliquam ut. Dui sapien eget mi proin sed libero enim. Rhoncus urna neque viverra justo nec ultrices. Dictumst quisque sagittis purus sit amet volutpat. Egestas egestas fringilla phasellus faucibus scelerisque eleifend donec pretium. Metus dictum at tempor commodo. Diam quis enim lobortis scelerisque fermentum dui faucibus in ornare. Ultrices eros in cursus turpis massa tincidunt dui ut. Urna molestie at elementum eu facilisis sed odio. At varius vel pharetra vel turpis nunc eget lorem. Est ultricies integer quis auctor elit sed. Dignissim diam quis enim lobortis scelerisque fermentum dui faucibus. Viverra accumsan in nisl nisi. Pretium aenean pharetra magna ac placerat vestibulum lectus.
Accumsan in nisl nisi scelerisque. Euismod quis viverra nibh cras pulvinar mattis nunc sed blandit. Magna eget est lorem ipsum dolor sit. Sit amet cursus sit amet dictum sit amet justo donec. Ac turpis egestas sed tempus. Nulla facilisi nullam vehicula ipsum a arcu cursus vitae congue. Quam quisque id diam vel quam elementum. In vitae turpis massa sed elementum tempus egestas. Non odio euismod lacinia at. Nisi est sit amet facilisis magna etiam tempor. Sed sed risus pretium quam vulputate dignissim. Vel eros donec ac odio tempor. Elementum nisi quis eleifend quam adipiscing vitae proin sagittis nisl. Pellentesque nec nam aliquam sem et. Velit dignissim sodales ut eu.
Mauris nunc congue nisi vitae suscipit tellus mauris a diam. Nunc sed augue lacus viverra vitae congue eu consequat. Nullam eget felis eget nunc lobortis mattis aliquam faucibus. Amet nisl suscipit adipiscing bibendum est ultricies integer. Bibendum est ultricies integer quis auctor elit sed. Quis auctor elit sed vulputate mi. A pellentesque sit amet porttitor. Nulla porttitor massa id neque aliquam vestibulum. Pharetra convallis posuere morbi leo urna. Enim blandit volutpat maecenas volutpat blandit aliquam. Morbi tristique senectus et netus et malesuada fames. Fermentum iaculis eu non diam. Luctus accumsan tortor posuere ac ut consequat semper. Ut tellus elementum sagittis vitae et leo. Nisl tincidunt eget nullam non nisi est sit amet. Ornare aenean euismod elementum nisi quis eleifend quam. Adipiscing at in tellus integer feugiat scelerisque varius morbi enim. Lacus luctus accumsan tortor posuere ac ut. Congue eu consequat ac felis donec et odio pellentesque diam. Aliquam ut porttitor leo a diam sollicitudin tempor id eu.
Proin fermentum leo vel orci porta non. In fermentum posuere urna nec tincidunt praesent semper feugiat nibh. Vulputate ut pharetra sit amet. Nisi vitae suscipit tellus mauris a. Quis eleifend quam adipiscing vitae proin sagittis. Sit amet nisl suscipit adipiscing bibendum est ultricies integer. Duis at consectetur lorem donec massa sapien. Nibh sed pulvinar proin gravida. Tincidunt augue interdum velit euismod in pellentesque. Orci ac auctor augue mauris augue. Eget velit aliquet sagittis id consectetur purus.
Dignissim enim sit amet venenatis urna cursus eget. At volutpat diam ut venenatis tellus in metus vulputate eu. Mauris cursus mattis molestie a iaculis at erat. Pulvinar mattis nunc sed blandit libero volutpat sed. Blandit aliquam etiam erat velit. In nibh mauris cursus mattis molestie a iaculis at erat. Tempor nec feugiat nisl pretium fusce id. Enim nunc faucibus a pellentesque. Enim diam vulputate ut pharetra sit amet aliquam id. Fringilla ut morbi tincidunt augue interdum velit. Turpis egestas maecenas pharetra convallis posuere morbi leo urna. Pellentesque adipiscing commodo elit at. Mattis pellentesque id nibh tortor id aliquet. Et leo duis ut diam. Eu ultrices vitae auctor eu augue ut lectus. Leo integer malesuada nunc vel risus commodo viverra maecenas. Consequat nisl vel pretium lectus quam. Suspendisse in est ante in nibh mauris cursus mattis molestie. Cras pulvinar mattis nunc sed blandit. Lorem ipsum dolor sit amet consectetur adipiscing.
In nibh mauris cursus mattis molestie. Iaculis at erat pellentesque adipiscing. Eget velit aliquet sagittis id consectetur. Scelerisque in dictum non consectetur a erat. Velit ut tortor pretium viverra suspendisse potenti nullam. Eget magna fermentum iaculis eu non. Gravida arcu ac tortor dignissim convallis aenean. Sapien nec sagittis aliquam malesuada bibendum arcu. Libero enim sed faucibus turpis in eu mi bibendum. Rhoncus dolor purus non enim praesent elementum facilisis leo vel. Augue neque gravida in fermentum et sollicitudin ac orci. Dolor purus non enim praesent elementum facilisis leo vel fringilla. Semper viverra nam libero justo laoreet sit amet cursus. Curabitur vitae nunc sed velit dignissim. Nisi lacus sed viverra tellus in. Integer eget aliquet nibh praesent tristique magna. Velit aliquet sagittis id consectetur purus ut faucibus pulvinar elementum. Mauris nunc congue nisi vitae. Lacus sed viverra tellus in hac. Morbi non arcu risus quis varius quam quisque id.
Odio tempor orci dapibus ultrices. Amet facilisis magna etiam tempor. Lorem donec massa sapien faucibus et. Odio tempor orci dapibus ultrices in iaculis. Semper viverra nam libero justo laoreet sit amet cursus sit. Amet nisl suscipit adipiscing bibendum est ultricies integer quis. Ac ut consequat semper viverra nam libero. Nunc mattis enim ut tellus elementum sagittis. Dapibus ultrices in iaculis nunc sed augue lacus. Semper auctor neque vitae tempus quam pellentesque. Leo vel fringilla est ullamcorper eget nulla facilisi etiam. Neque ornare aenean euismod elementum. Ut ornare lectus sit amet est placerat in egestas erat. Pharetra pharetra massa massa ultricies mi quis hendrerit dolor magna. Nam at lectus urna duis convallis convallis tellus id interdum. Viverra orci sagittis eu volutpat.
Consequat id porta nibh venenatis. Scelerisque fermentum dui faucibus in ornare quam. In iaculis nunc sed augue lacus viverra vitae congue. Tellus cras adipiscing enim eu turpis egestas pretium. Turpis egestas maecenas pharetra convallis. Volutpat est velit egestas dui id. Sed nisi lacus sed viverra tellus in hac habitasse. Faucibus vitae aliquet nec ullamcorper. Nisl suscipit adipiscing bibendum est ultricies integer quis auctor. Lectus magna fringilla urna porttitor rhoncus dolor. Nec sagittis aliquam malesuada bibendum arcu. Lorem mollis aliquam ut porttitor leo. In hendrerit gravida rutrum quisque. Non curabitur gravida arcu ac. In ante metus dictum at tempor commodo ullamcorper. Commodo ullamcorper a lacus vestibulum. Nisl nisi scelerisque eu ultrices vitae auctor eu augue ut.
Nulla facilisi etiam dignissim diam quis enim lobortis scelerisque. Felis donec et odio pellentesque diam volutpat. Dolor morbi non arcu risus. Mauris sit amet massa vitae tortor condimentum lacinia. Amet volutpat consequat mauris nunc congue. Amet commodo nulla facilisi nullam vehicula ipsum a. Vitae nunc sed velit dignissim sodales ut eu sem. Vulputate dignissim suspendisse in est. Nunc sed id semper risus in hendrerit gravida rutrum quisque. In est ante in nibh mauris cursus mattis. Quam lacus suspendisse faucibus interdum posuere. Quam vulputate dignissim suspendisse in est ante in nibh.
Viverra aliquet eget sit amet. Dui vivamus arcu felis bibendum. Pharetra convallis posuere morbi leo urna molestie at elementum eu. Parturient montes nascetur ridiculus mus mauris vitae. Mus mauris vitae ultricies leo integer malesuada nunc vel. Consequat semper viverra nam libero. Amet venenatis urna cursus eget nunc scelerisque. Amet risus nullam eget felis eget nunc lobortis. Venenatis urna cursus eget nunc. Sagittis id consectetur purus ut faucibus pulvinar. Posuere sollicitudin aliquam ultrices sagittis orci a scelerisque purus semper.
<div><a id="click-one-again" href="/one">go to 1</a></div>
Morbi tristique senectus et netus et. Neque aliquam vestibulum morbi blandit cursus risus. Pharetra pharetra massa massa ultricies mi quis. Sit amet aliquam id diam maecenas ultricies mi eget mauris. Ultrices mi tempus imperdiet nulla malesuada. At consectetur lorem donec massa sapien faucibus et molestie. Non sodales neque sodales ut etiam. Eget nunc lobortis mattis aliquam faucibus purus in massa tempor. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus. Pellentesque eu tincidunt tortor aliquam nulla facilisi cras fermentum. Diam vulputate ut pharetra sit. Felis donec et odio pellentesque diam. Mollis aliquam ut porttitor leo. Vitae nunc sed velit dignissim sodales. Facilisis mauris sit amet massa vitae tortor condimentum lacinia quis.
Aliquet enim tortor at auctor urna nunc id cursus. Bibendum at varius vel pharetra vel turpis nunc eget. Mattis molestie a iaculis at erat. Vel turpis nunc eget lorem dolor sed viverra ipsum nunc. Aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc sed. Nunc congue nisi vitae suscipit. Donec massa sapien faucibus et molestie ac. Nec feugiat nisl pretium fusce. At imperdiet dui accumsan sit amet nulla facilisi. Sed viverra tellus in hac.
Gravida in fermentum et sollicitudin ac orci phasellus egestas. Tortor condimentum lacinia quis vel eros donec ac. Lacinia quis vel eros donec. Velit euismod in pellentesque massa placerat duis ultricies lacus. Eget mi proin sed libero. Condimentum mattis pellentesque id nibh tortor id aliquet. Aliquet nibh praesent tristique magna sit. Placerat vestibulum lectus mauris ultrices eros in cursus. Nunc sed augue lacus viverra vitae congue eu. Pellentesque elit ullamcorper dignissim cras tincidunt. Integer quis auctor elit sed vulputate mi sit amet. Nam at lectus urna duis convallis convallis tellus id. Amet dictum sit amet justo donec enim diam. Imperdiet nulla malesuada pellentesque elit eget gravida cum sociis natoque. Ullamcorper eget nulla facilisi etiam dignissim diam.
Magnis dis parturient montes nascetur ridiculus mus mauris vitae ultricies. Magna ac placerat vestibulum lectus mauris. Pellentesque habitant morbi tristique senectus. Risus at ultrices mi tempus imperdiet nulla malesuada. Blandit cursus risus at ultrices mi tempus. Faucibus in ornare quam viverra orci sagittis eu volutpat odio. Enim facilisis gravida neque convallis a. Elit eget gravida cum sociis natoque penatibus et. Nulla at volutpat diam ut. Non curabitur gravida arcu ac tortor dignissim convallis aenean et. Ullamcorper sit amet risus nullam eget. Id ornare arcu odio ut sem. Id neque aliquam vestibulum morbi blandit.
Accumsan tortor posuere ac ut consequat semper viverra nam. Non arcu risus quis varius quam. Pretium aenean pharetra magna ac placerat vestibulum lectus mauris. Egestas tellus rutrum tellus pellentesque eu tincidunt tortor aliquam nulla. Aenean et tortor at risus viverra adipiscing. Feugiat vivamus at augue eget arcu dictum varius. Ornare arcu odio ut sem nulla pharetra. Mauris pharetra et ultrices neque ornare aenean euismod. Lacinia at quis risus sed vulputate. Pulvinar pellentesque habitant morbi tristique. Ipsum faucibus vitae aliquet nec ullamcorper. Cum sociis natoque penatibus et magnis. Nibh nisl condimentum id venenatis a condimentum vitae sapien. Nisl nunc mi ipsum faucibus vitae aliquet. Aliquam sem fringilla ut morbi tincidunt augue interdum velit euismod.
Tortor consequat id porta nibh venenatis cras sed felis. Adipiscing elit pellentesque habitant morbi tristique. Lectus urna duis convallis convallis tellus. Sed felis eget velit aliquet sagittis. Laoreet sit amet cursus sit. Lacus laoreet non curabitur gravida arcu ac tortor dignissim. Ipsum a arcu cursus vitae congue. Nunc sed augue lacus viverra. Neque vitae tempus quam pellentesque nec. Vulputate sapien nec sagittis aliquam. Tincidunt dui ut ornare lectus sit amet est placerat in. Rhoncus urna neque viverra justo nec ultrices. Scelerisque mauris pellentesque pulvinar pellentesque habitant. Non pulvinar neque laoreet suspendisse. Vitae semper quis lectus nulla at volutpat. Suspendisse in est ante in nibh mauris cursus mattis.
Leo in vitae turpis massa. Placerat duis ultricies lacus sed turpis. Montes nascetur ridiculus mus mauris vitae ultricies leo. Nisi scelerisque eu ultrices vitae auctor eu augue ut lectus. Porttitor leo a diam sollicitudin tempor id eu nisl nunc. Nibh cras pulvinar mattis nunc sed. Arcu ac tortor dignissim convallis. Vitae semper quis lectus nulla at volutpat diam ut venenatis. Phasellus vestibulum lorem sed risus ultricies. Accumsan tortor posuere ac ut consequat. Nisi vitae suscipit tellus mauris a.
Libero id faucibus nisl tincidunt eget nullam non. Faucibus a pellentesque sit amet porttitor eget dolor. Posuere urna nec tincidunt praesent semper feugiat nibh sed. Suspendisse ultrices gravida dictum fusce. Porttitor eget dolor morbi non arcu. Neque egestas congue quisque egestas diam in. Suscipit tellus mauris a diam maecenas sed enim ut sem. Luctus accumsan tortor posuere ac. Tortor posuere ac ut consequat semper viverra. Egestas tellus rutrum tellus pellentesque eu tincidunt tortor aliquam nulla. Senectus et netus et malesuada fames ac turpis egestas. Sed libero enim sed faucibus turpis in eu mi bibendum. Sollicitudin tempor id eu nisl nunc mi.
</article>
</Layout>

View file

@ -6,6 +6,7 @@ import Layout from '../components/Layout.astro';
<a id="click-one" href="#test">test</a>
<a id="click-two" href="/two">go to 2</a>
<a id="click-three" href="/three">go to 3</a>
<a id="click-longpage" href="/long-page">go to long page</a>
<div id="test">test content</div>
</Layout>

View file

@ -13,6 +13,13 @@ test.afterAll(async () => {
await devServer.stop();
});
function scrollToBottom(page) {
return page.evaluate(() => {
window.scrollY = document.documentElement.scrollHeight;
window.dispatchEvent(new Event('scroll'));
});
}
test.describe('View Transitions', () => {
test('Moving from page 1 to page 2', async ({ page, astro }) => {
const loads = [];
@ -183,6 +190,53 @@ test.describe('View Transitions', () => {
await expect(p, 'should have content').toHaveText('Page 1');
});
test('Scroll position restored on back button', async ({ page, astro }) => {
// Go to page 1
await page.goto(astro.resolveUrl('/long-page'));
let article = page.locator('#longpage');
await expect(article, 'should have script content').toBeVisible('exists');
await scrollToBottom(page);
const oldScrollY = await page.evaluate(() => window.scrollY);
// go to page long-page
await page.click('#click-one');
let p = page.locator('#one');
await expect(p, 'should have content').toHaveText('Page 1');
// Back to page 1
await page.goBack();
const newScrollY = await page.evaluate(() => window.scrollY);
expect(oldScrollY).toEqual(newScrollY);
});
test('Scroll position restored on forward button', async ({ page, astro }) => {
// Go to page 1
await page.goto(astro.resolveUrl('/one'));
let p = page.locator('#one');
await expect(p, 'should have content').toHaveText('Page 1');
// go to page long-page
await page.click('#click-longpage');
let article = page.locator('#longpage');
await expect(article, 'should have script content').toBeVisible('exists');
await scrollToBottom(page);
const oldScrollY = await page.evaluate(() => window.scrollY);
// Back to page 1
await page.goBack();
// Go forward
await page.goForward();
article = page.locator('#longpage');
await expect(article, 'should have script content').toBeVisible('exists');
const newScrollY = await page.evaluate(() => window.scrollY);
expect(oldScrollY).toEqual(newScrollY);
})
test('<Image /> component forwards transitions to the <img>', async ({ page, astro }) => {
// Go to page 1
await page.goto(astro.resolveUrl('/image-one'));