From ad6ca969cc82409efaa8fbb71bb2461006ecb7c9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 15 Feb 2022 17:06:33 -0500 Subject: [PATCH 01/11] track scroll position without scroll listener --- .changeset/moody-dingos-happen.md | 5 ++++ packages/kit/src/runtime/client/router.js | 31 ++++++++++------------- 2 files changed, 19 insertions(+), 17 deletions(-) create mode 100644 .changeset/moody-dingos-happen.md diff --git a/.changeset/moody-dingos-happen.md b/.changeset/moody-dingos-happen.md new file mode 100644 index 000000000000..d1b6bc504f54 --- /dev/null +++ b/.changeset/moody-dingos-happen.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +Track scroll position without scroll listener, and recover on reload diff --git a/packages/kit/src/runtime/client/router.js b/packages/kit/src/runtime/client/router.js index 0f47aa5418ab..58448c8aaf06 100644 --- a/packages/kit/src/runtime/client/router.js +++ b/packages/kit/src/runtime/client/router.js @@ -2,6 +2,10 @@ import { onMount } from 'svelte'; import { normalize_path } from '../../utils/url'; import { get_base_uri } from './utils'; +const SCROLL_KEY = 'sveltekit:scroll'; + +const scroll_positions = JSON.parse(sessionStorage.getItem(SCROLL_KEY) || '{}'); + function scroll_state() { return { x: pageXOffset, @@ -63,6 +67,11 @@ export class Router { history.replaceState({ ...history.state, 'sveltekit:index': 0 }, '', location.href); } + // if we reload the page, or Cmd-Shift-T back to it, + // recover scroll position + const scroll = scroll_positions[this.current_history_index]; + if (scroll) scrollTo(scroll.x, scroll.y); + this.hash_navigating = false; this.callbacks = { @@ -98,6 +107,8 @@ export class Router { e.preventDefault(); e.returnValue = ''; } else { + scroll_positions[this.current_history_index] = scroll_state(); + sessionStorage.setItem(SCROLL_KEY, JSON.stringify(scroll_positions)); history.scrollRestoration = 'auto'; } }); @@ -110,22 +121,6 @@ export class Router { // There's no API to capture the scroll location right before the user // hits the back/forward button, so we listen for scroll events - /** @type {NodeJS.Timeout} */ - let scroll_timer; - addEventListener('scroll', () => { - clearTimeout(scroll_timer); - scroll_timer = setTimeout(() => { - // Store the scroll location in the history - // This will persist even if we navigate away from the site and come back - const new_state = { - ...(history.state || {}), - 'sveltekit:scroll': scroll_state() - }; - history.replaceState(new_state, document.title, window.location.href); - // iOS scroll event intervals happen between 30-150ms, sometimes around 200ms - }, 200); - }); - /** @param {Event} event */ const trigger_prefetch = (event) => { const a = find_anchor(event); @@ -223,7 +218,7 @@ export class Router { this._navigate({ url: new URL(location.href), - scroll: event.state['sveltekit:scroll'], + scroll: scroll_positions[event.state['sveltekit:index']], keepfocus: false, chain: [], details: null, @@ -397,6 +392,8 @@ export class Router { }); } + scroll_positions[this.current_history_index] = scroll_state(); + accepted(); if (!this.navigating) { From 16f66dca4c2a83b0581688d8e5b2e48bec53f7f8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 15 Feb 2022 17:26:34 -0500 Subject: [PATCH 02/11] add a test --- packages/kit/test/apps/basics/test/test.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index acfa23fd32ac..917892a2a952 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -253,6 +253,19 @@ test.describe('Scrolling', () => { expect(await in_view('#input')).toBe(true); expect(await page.locator('#input')).toBeFocused(); }); + + test('scroll positions are recovered on reloading the page', async ({ page, back, app }) => { + await page.goto('/anchor'); + await page.evaluate(() => window.scrollTo(0, 1000)); + await app.goto('/anchor/anchor'); + await page.evaluate(() => window.scrollTo(0, 1000)); + + await page.reload(); + expect(await page.evaluate(() => window.scrollY)).toBe(1000); + + await back(); + expect(await page.evaluate(() => window.scrollY)).toBe(1000); + }); }); test.describe.parallel('Imports', () => { From d538ac6d3491197f27568005048642be999c761d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 15 Feb 2022 22:18:29 -0500 Subject: [PATCH 03/11] add explanatory comment --- packages/kit/src/runtime/client/router.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/kit/src/runtime/client/router.js b/packages/kit/src/runtime/client/router.js index 58448c8aaf06..6901336b83a9 100644 --- a/packages/kit/src/runtime/client/router.js +++ b/packages/kit/src/runtime/client/router.js @@ -4,6 +4,10 @@ import { get_base_uri } from './utils'; const SCROLL_KEY = 'sveltekit:scroll'; +// We track the scroll position associated with each history entry in sessionStorage, +// rather than on history.state itself, because when navigation is driven by +// popstate it's too late to update the scroll position associated with the +// state we're navigating from const scroll_positions = JSON.parse(sessionStorage.getItem(SCROLL_KEY) || '{}'); function scroll_state() { From 5f7b2f2b66d778abe7f3b73d94aeaef58c0969c4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Feb 2022 15:52:27 -0500 Subject: [PATCH 04/11] save scroll state on visibilitychange rather than beforeunload --- packages/kit/src/runtime/client/router.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/client/router.js b/packages/kit/src/runtime/client/router.js index 6901336b83a9..beb39cd43169 100644 --- a/packages/kit/src/runtime/client/router.js +++ b/packages/kit/src/runtime/client/router.js @@ -111,9 +111,14 @@ export class Router { e.preventDefault(); e.returnValue = ''; } else { + history.scrollRestoration = 'auto'; + } + }); + + addEventListener('visibilitychange', () => { + if (document.visibilityState === 'hidden') { scroll_positions[this.current_history_index] = scroll_state(); sessionStorage.setItem(SCROLL_KEY, JSON.stringify(scroll_positions)); - history.scrollRestoration = 'auto'; } }); From f90fca53bd64e34cae5a80f531e503b51c1dd699 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Feb 2022 16:06:34 -0500 Subject: [PATCH 05/11] tidy up --- packages/kit/src/runtime/client/router.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/kit/src/runtime/client/router.js b/packages/kit/src/runtime/client/router.js index beb39cd43169..506f6123d699 100644 --- a/packages/kit/src/runtime/client/router.js +++ b/packages/kit/src/runtime/client/router.js @@ -88,9 +88,7 @@ export class Router { } init_listeners() { - if ('scrollRestoration' in history) { - history.scrollRestoration = 'manual'; - } + history.scrollRestoration = 'manual'; // Adopted from Nuxt.js // Reset scrollRestoration to auto when leaving page, allowing page reload @@ -122,14 +120,6 @@ export class Router { } }); - // Setting scrollRestoration to manual again when returning to this page. - addEventListener('load', () => { - history.scrollRestoration = 'manual'; - }); - - // There's no API to capture the scroll location right before the user - // hits the back/forward button, so we listen for scroll events - /** @param {Event} event */ const trigger_prefetch = (event) => { const a = find_anchor(event); From 1623a6950c0cd20397a6b45fd0d6f9e8c6d1cb5a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Feb 2022 16:14:33 -0500 Subject: [PATCH 06/11] centralise scroll_positions update logic --- packages/kit/src/runtime/client/router.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/client/router.js b/packages/kit/src/runtime/client/router.js index 9916ccb62eff..7244ea15eea6 100644 --- a/packages/kit/src/runtime/client/router.js +++ b/packages/kit/src/runtime/client/router.js @@ -115,7 +115,7 @@ export class Router { addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') { - scroll_positions[this.current_history_index] = scroll_state(); + this.#update_scroll_positions(); sessionStorage.setItem(SCROLL_KEY, JSON.stringify(scroll_positions)); } }); @@ -190,6 +190,8 @@ export class Router { // clicking a hash link and those triggered by popstate this.hash_navigating = true; + this.#update_scroll_positions(); + const info = this.parse(url); if (info) { return this.renderer.update(info, [], false); @@ -248,6 +250,10 @@ export class Router { }); } + #update_scroll_positions() { + scroll_positions[this.current_history_index] = scroll_state(); + } + /** * Returns true if `url` has the same origin and basepath as the app * @param {URL} url @@ -395,7 +401,7 @@ export class Router { }); } - scroll_positions[this.current_history_index] = scroll_state(); + this.#update_scroll_positions(); accepted(); From 1481c3fe26ea006b908b4367e4d5eb8a8ae54145 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Feb 2022 16:18:08 -0500 Subject: [PATCH 07/11] belt and braces --- packages/kit/src/runtime/client/router.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/client/router.js b/packages/kit/src/runtime/client/router.js index 7244ea15eea6..83a022cb7c72 100644 --- a/packages/kit/src/runtime/client/router.js +++ b/packages/kit/src/runtime/client/router.js @@ -4,11 +4,18 @@ import { get_base_uri } from './utils'; const SCROLL_KEY = 'sveltekit:scroll'; +/** @typedef {{ x: number, y: number }} ScrollPosition */ + // We track the scroll position associated with each history entry in sessionStorage, // rather than on history.state itself, because when navigation is driven by // popstate it's too late to update the scroll position associated with the // state we're navigating from -const scroll_positions = JSON.parse(sessionStorage.getItem(SCROLL_KEY) || '{}'); + +/** @type {Record} */ +let scroll_positions = {}; +try { + scroll_positions = JSON.parse(/** @type {string} */ (sessionStorage.getItem(SCROLL_KEY))); +} catch {} function scroll_state() { return { @@ -116,7 +123,10 @@ export class Router { addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') { this.#update_scroll_positions(); - sessionStorage.setItem(SCROLL_KEY, JSON.stringify(scroll_positions)); + + try { + sessionStorage.setItem(SCROLL_KEY, JSON.stringify(scroll_positions)); + } catch {} } }); From 0d3ebab5808d39830be71f3182d7eb37c8da92d1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Feb 2022 16:22:02 -0500 Subject: [PATCH 08/11] group code --- packages/kit/src/runtime/client/router.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/runtime/client/router.js b/packages/kit/src/runtime/client/router.js index 83a022cb7c72..0be18edc046d 100644 --- a/packages/kit/src/runtime/client/router.js +++ b/packages/kit/src/runtime/client/router.js @@ -2,15 +2,13 @@ import { onMount } from 'svelte'; import { normalize_path } from '../../utils/url'; import { get_base_uri } from './utils'; -const SCROLL_KEY = 'sveltekit:scroll'; - -/** @typedef {{ x: number, y: number }} ScrollPosition */ - // We track the scroll position associated with each history entry in sessionStorage, // rather than on history.state itself, because when navigation is driven by // popstate it's too late to update the scroll position associated with the // state we're navigating from +const SCROLL_KEY = 'sveltekit:scroll'; +/** @typedef {{ x: number, y: number }} ScrollPosition */ /** @type {Record} */ let scroll_positions = {}; try { From e25757d4d2309c6e95cac755f98fa456f37ea0c1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Feb 2022 16:26:33 -0500 Subject: [PATCH 09/11] appease that whiny little funsucker eslint --- packages/kit/src/runtime/client/router.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/client/router.js b/packages/kit/src/runtime/client/router.js index 0be18edc046d..e315b577e3c1 100644 --- a/packages/kit/src/runtime/client/router.js +++ b/packages/kit/src/runtime/client/router.js @@ -13,7 +13,9 @@ const SCROLL_KEY = 'sveltekit:scroll'; let scroll_positions = {}; try { scroll_positions = JSON.parse(/** @type {string} */ (sessionStorage.getItem(SCROLL_KEY))); -} catch {} +} catch { + // do nothing +} function scroll_state() { return { @@ -124,7 +126,9 @@ export class Router { try { sessionStorage.setItem(SCROLL_KEY, JSON.stringify(scroll_positions)); - } catch {} + } catch { + // do nothing + } } }); From c95076b1199147a48bd6bd30816bd810eddd4e94 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Feb 2022 17:51:53 -0500 Subject: [PATCH 10/11] huh --- packages/kit/src/runtime/client/router.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/client/router.js b/packages/kit/src/runtime/client/router.js index e315b577e3c1..a1cda78ff814 100644 --- a/packages/kit/src/runtime/client/router.js +++ b/packages/kit/src/runtime/client/router.js @@ -12,7 +12,7 @@ const SCROLL_KEY = 'sveltekit:scroll'; /** @type {Record} */ let scroll_positions = {}; try { - scroll_positions = JSON.parse(/** @type {string} */ (sessionStorage.getItem(SCROLL_KEY))); + scroll_positions = JSON.parse(sessionStorage.getItem(SCROLL_KEY) || '{}'); } catch { // do nothing } From f304f0d68da5654aadd1e00f3b640f0a7e326162 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Feb 2022 17:54:15 -0500 Subject: [PATCH 11/11] microscopically less code --- packages/kit/src/runtime/client/router.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/client/router.js b/packages/kit/src/runtime/client/router.js index a1cda78ff814..32f09f6aae99 100644 --- a/packages/kit/src/runtime/client/router.js +++ b/packages/kit/src/runtime/client/router.js @@ -12,7 +12,7 @@ const SCROLL_KEY = 'sveltekit:scroll'; /** @type {Record} */ let scroll_positions = {}; try { - scroll_positions = JSON.parse(sessionStorage.getItem(SCROLL_KEY) || '{}'); + scroll_positions = JSON.parse(sessionStorage[SCROLL_KEY]); } catch { // do nothing } @@ -125,7 +125,7 @@ export class Router { this.#update_scroll_positions(); try { - sessionStorage.setItem(SCROLL_KEY, JSON.stringify(scroll_positions)); + sessionStorage[SCROLL_KEY] = JSON.stringify(scroll_positions); } catch { // do nothing }