From 78d8cce01dfa92128508d10f229ce23c27ca5c8d Mon Sep 17 00:00:00 2001 From: Mihai <73397939+mihaizaurus@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:07:51 +0100 Subject: [PATCH 1/4] added scroll preservation opt in to the compare page to avoid input updates resetting it --- app/pages/compare.vue | 1 + app/router.options.ts | 11 ++++++ test/unit/app/router.options.spec.ts | 54 ++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 test/unit/app/router.options.spec.ts diff --git a/app/pages/compare.vue b/app/pages/compare.vue index 5582274a60..26a92862f9 100644 --- a/app/pages/compare.vue +++ b/app/pages/compare.vue @@ -5,6 +5,7 @@ import FacetBarChart from '~/components/Compare/FacetBarChart.vue' definePageMeta({ name: 'compare', + preserveScrollOnQuery: true, }) const { locale } = useI18n() diff --git a/app/router.options.ts b/app/router.options.ts index a6d9db7ccd..abaf104f70 100644 --- a/app/router.options.ts +++ b/app/router.options.ts @@ -7,6 +7,17 @@ export default { if (savedPosition) { return savedPosition } + + // Preserve the current viewport for query-only updates on pages that opt in, + // such as compare where controls sync state to the URL in-place. + if ( + to.path === _from.path && + to.hash === _from.hash && + to.meta.preserveScrollOnQuery === true + ) { + return false + } + // If navigating to a hash anchor, scroll to it if (to.hash) { const { scrollMargin } = to.meta diff --git a/test/unit/app/router.options.spec.ts b/test/unit/app/router.options.spec.ts new file mode 100644 index 0000000000..a3922d271d --- /dev/null +++ b/test/unit/app/router.options.spec.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from 'vitest' +import routerOptions from '../../../app/router.options' + +function createRoute(overrides: Record = {}) { + return { + path: '/', + hash: '', + meta: {}, + ...overrides, + } as any +} + +describe('router scrollBehavior', () => { + it('restores saved position when available', () => { + const savedPosition = { left: 12, top: 345 } + + expect(routerOptions.scrollBehavior(createRoute(), createRoute(), savedPosition)).toEqual( + savedPosition, + ) + }) + + it('preserves scroll on query-only updates for pages that opt in', () => { + const to = createRoute({ + path: '/compare', + meta: { preserveScrollOnQuery: true }, + }) + const from = createRoute({ + path: '/compare', + meta: { preserveScrollOnQuery: true }, + }) + + expect(routerOptions.scrollBehavior(to, from, null)).toBe(false) + }) + + it('scrolls to hash anchors', () => { + const to = createRoute({ + hash: '#section-function', + meta: { scrollMargin: 96 }, + }) + + expect(routerOptions.scrollBehavior(to, createRoute(), null)).toEqual({ + el: '#section-function', + behavior: 'smooth', + top: 96, + }) + }) + + it('scrolls to top for regular navigations', () => { + const to = createRoute({ path: '/compare', meta: { preserveScrollOnQuery: true } }) + const from = createRoute({ path: '/search' }) + + expect(routerOptions.scrollBehavior(to, from, null)).toEqual({ left: 0, top: 0 }) + }) +}) From 507be8ce5334f7a843bc17cfb37867a634e98d96 Mon Sep 17 00:00:00 2001 From: Mihai <73397939+mihaizaurus@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:46:16 +0100 Subject: [PATCH 2/4] updated with changes from coderabbit's comments --- app/types/index.ts | 4 ++++ test/unit/app/router.options.spec.ts | 23 +++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/types/index.ts b/app/types/index.ts index 5058830705..f736f8b329 100644 --- a/app/types/index.ts +++ b/app/types/index.ts @@ -10,5 +10,9 @@ declare module '#app' { * @default 70 */ scrollMargin?: number + /** + * preserve scroll position when only query params change on same path/hash + */ + preserveScrollOnQuery?: boolean } } diff --git a/test/unit/app/router.options.spec.ts b/test/unit/app/router.options.spec.ts index a3922d271d..1c6c6b4f74 100644 --- a/test/unit/app/router.options.spec.ts +++ b/test/unit/app/router.options.spec.ts @@ -1,13 +1,17 @@ import { describe, expect, it } from 'vitest' import routerOptions from '../../../app/router.options' -function createRoute(overrides: Record = {}) { +type ScrollBehavior = NonNullable +type RouteArg = Parameters[0] + +function createRoute(overrides: Partial = {}) { return { path: '/', hash: '', + query: {}, meta: {}, ...overrides, - } as any + } as RouteArg } describe('router scrollBehavior', () => { @@ -22,16 +26,31 @@ describe('router scrollBehavior', () => { it('preserves scroll on query-only updates for pages that opt in', () => { const to = createRoute({ path: '/compare', + query: { packages: 'vue,nuxt', facets: 'downloads,license' }, meta: { preserveScrollOnQuery: true }, }) const from = createRoute({ path: '/compare', + query: { packages: 'vue', facets: 'downloads' }, meta: { preserveScrollOnQuery: true }, }) expect(routerOptions.scrollBehavior(to, from, null)).toBe(false) }) + it('does not preserve scroll on query-only updates without opt-in', () => { + const to = createRoute({ + path: '/compare', + query: { packages: 'vue,nuxt', facets: 'downloads,license' }, + }) + const from = createRoute({ + path: '/compare', + query: { packages: 'vue', facets: 'downloads' }, + }) + + expect(routerOptions.scrollBehavior(to, from, null)).toEqual({ left: 0, top: 0 }) + }) + it('scrolls to hash anchors', () => { const to = createRoute({ hash: '#section-function', From 8bc85436f7ea662d85c56e0265fa71db07481456 Mon Sep 17 00:00:00 2001 From: Alec Lloyd Probert <55991794+graphieros@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:53:35 +0100 Subject: [PATCH 3/4] fix: remove unused var underscore --- app/router.options.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/router.options.ts b/app/router.options.ts index abaf104f70..b91b41ce86 100644 --- a/app/router.options.ts +++ b/app/router.options.ts @@ -1,7 +1,7 @@ import type { RouterConfig } from 'nuxt/schema' export default { - scrollBehavior(to, _from, savedPosition) { + scrollBehavior(to, from, savedPosition) { // If the browser has a saved position (e.g. back/forward navigation), restore it if (savedPosition) { @@ -11,8 +11,8 @@ export default { // Preserve the current viewport for query-only updates on pages that opt in, // such as compare where controls sync state to the URL in-place. if ( - to.path === _from.path && - to.hash === _from.hash && + to.path === from.path && + to.hash === from.hash && to.meta.preserveScrollOnQuery === true ) { return false From 990768520041c479cfee7a539f200cdf3a8369c5 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:54:40 +0000 Subject: [PATCH 4/4] [autofix.ci] apply automated fixes --- app/router.options.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/router.options.ts b/app/router.options.ts index b91b41ce86..3d1ec07693 100644 --- a/app/router.options.ts +++ b/app/router.options.ts @@ -10,11 +10,7 @@ export default { // Preserve the current viewport for query-only updates on pages that opt in, // such as compare where controls sync state to the URL in-place. - if ( - to.path === from.path && - to.hash === from.hash && - to.meta.preserveScrollOnQuery === true - ) { + if (to.path === from.path && to.hash === from.hash && to.meta.preserveScrollOnQuery === true) { return false }