diff --git a/frontend/src/router.js b/frontend/src/router.js index 00bd714..559af07 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -94,9 +94,25 @@ const routes = [ const history = typeof window !== 'undefined' ? createWebHistory() : createMemoryHistory() +// Where the window lands after a navigation. Without this the router keeps the +// window's scroll offset, so a view entered from halfway down another one opened +// halfway down itself, its heading and tab strip under the nav (operator-flagged +// 2026-09-24, Settings entered from a scrolled feed). +// - back/forward returns to where that entry was left; +// - the SAME view with a new query (a gallery filter, a Browse or +// Subscriptions tab) keeps its place — that is not a new page; +// - a new view opens at its top. +export function scrollBehavior(to, from, savedPosition) { + if (savedPosition) return savedPosition + if (from.matched.length && to.path === from.path) return false + if (to.hash) return { el: to.hash } + return { top: 0 } +} + const router = createRouter({ history, - routes + routes, + scrollBehavior }) const DEFAULT_TITLE = 'FabledCurator' diff --git a/frontend/test/router.spec.js b/frontend/test/router.spec.js index 44f3b28..fc0356a 100644 --- a/frontend/test/router.spec.js +++ b/frontend/test/router.spec.js @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import router, { FRONT_DOOR } from '../src/router.js' +import router, { FRONT_DOOR, scrollBehavior } from '../src/router.js' describe('router', () => { it('FRONT_DOOR is the post feed', () => { @@ -74,4 +74,26 @@ describe('router', () => { expect(r.name).toBe('series-read') expect(r.meta.immersive).toBe(true) }) + + describe('scrollBehavior', () => { + const at = (path) => ({ path, hash: '', matched: [{}] }) + + it('opens a new view at its top, not at the last view\'s offset', () => { + expect(scrollBehavior(at('/settings'), at('/latest'), null)).toEqual({ top: 0 }) + }) + + it('back and forward return to where the entry was left', () => { + const saved = { left: 0, top: 1234 } + expect(scrollBehavior(at('/latest'), at('/settings'), saved)).toBe(saved) + }) + + it('a new query on the same view keeps its place', () => { + expect(scrollBehavior(at('/gallery'), at('/gallery'), null)).toBe(false) + }) + + it('the first navigation of a page load opens at the top', () => { + const initial = { path: '/', hash: '', matched: [] } + expect(scrollBehavior(at('/latest'), initial, null)).toEqual({ top: 0 }) + }) + }) })