Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
11 changes: 11 additions & 0 deletions app/router.options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

// 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