Skip to content
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
1 change: 1 addition & 0 deletions app/pages/compare.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import FacetBarChart from '~/components/Compare/FacetBarChart.vue'

definePageMeta({
name: 'compare',
preserveScrollOnQuery: true,
})

const { locale } = useI18n()
Expand Down
9 changes: 8 additions & 1 deletion app/router.options.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
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) {
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
Expand Down
4 changes: 4 additions & 0 deletions app/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
73 changes: 73 additions & 0 deletions test/unit/app/router.options.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest'
import routerOptions from '../../../app/router.options'

type ScrollBehavior = NonNullable<typeof routerOptions.scrollBehavior>
type RouteArg = Parameters<ScrollBehavior>[0]

function createRoute(overrides: Partial<RouteArg> = {}) {
return {
path: '/',
hash: '',
query: {},
meta: {},
...overrides,
} as RouteArg
}

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',
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',
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 })
})
})
Loading