mirror of
https://github.com/nuxt/nuxt.git
synced 2025-02-22 16:39:58 +00:00
fix: improved default router.options scrollBehaviour
handle first page load and page refresh scenarios (restore scroll position without jumps), fixes hash-navigation when changing pages and browser back/forward positions, especiall when page transitions are involved. fixes #24941, #22487, #25030 and #19664 Signed-off-by: Bernhard Berger <bernhard.berger@gmail.com>
This commit is contained in:
parent
6736decba0
commit
a3658ee7cd
@ -1,8 +1,8 @@
|
|||||||
import type { RouteLocationNormalized, RouterScrollBehavior } from '#vue-router'
|
|
||||||
import { nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
import type { RouterConfig } from 'nuxt/schema'
|
import type { RouterConfig } from 'nuxt/schema'
|
||||||
|
import { START_LOCATION } from 'vue-router'
|
||||||
|
import type { RouteLocationNormalized, RouterScrollBehavior } from '#vue-router'
|
||||||
import { useNuxtApp } from '#app/nuxt'
|
import { useNuxtApp } from '#app/nuxt'
|
||||||
import { isChangingPage } from '#app/components/utils'
|
|
||||||
import { useRouter } from '#app/composables/router'
|
import { useRouter } from '#app/composables/router'
|
||||||
// @ts-expect-error virtual file
|
// @ts-expect-error virtual file
|
||||||
import { appPageTransition as defaultPageTransition } from '#build/nuxt.config.mjs'
|
import { appPageTransition as defaultPageTransition } from '#build/nuxt.config.mjs'
|
||||||
@ -11,43 +11,54 @@ type ScrollPosition = Awaited<ReturnType<RouterScrollBehavior>>
|
|||||||
|
|
||||||
// Default router options
|
// Default router options
|
||||||
// https://router.vuejs.org/api/#routeroptions
|
// https://router.vuejs.org/api/#routeroptions
|
||||||
export default <RouterConfig> {
|
export default <RouterConfig>{
|
||||||
scrollBehavior (to, from, savedPosition) {
|
scrollBehavior: (
|
||||||
|
to: RouteLocationNormalized,
|
||||||
|
from: RouteLocationNormalized,
|
||||||
|
savedPosition: ScrollPosition | null
|
||||||
|
) => {
|
||||||
|
// Check if the navigation is the first page load
|
||||||
|
const isFirstLoad = from === START_LOCATION
|
||||||
const nuxtApp = useNuxtApp()
|
const nuxtApp = useNuxtApp()
|
||||||
// @ts-expect-error untyped, nuxt-injected option
|
// @ts-expect-error untyped, nuxt-injected option
|
||||||
const behavior = useRouter().options?.scrollBehaviorType ?? 'auto'
|
const scrollBehaviorType = useRouter().options?.scrollBehaviorType ?? 'auto'
|
||||||
|
|
||||||
// By default when the returned position is falsy or an empty object, vue-router will retain the current scroll position
|
// Handle page reload scenario where the saved position should be used
|
||||||
// savedPosition is only available for popstate navigations (back button)
|
if (isFirstLoad && savedPosition) {
|
||||||
let position: ScrollPosition = savedPosition || undefined
|
// Restore browser's default scroll behavior to auto (vue-router sets this when using custom ScrollBehavior)
|
||||||
|
// to avoid page jumps on load
|
||||||
|
window.history.scrollRestoration = 'auto'
|
||||||
|
return savedPosition
|
||||||
|
}
|
||||||
|
// For other navigations, ensure manual scrollRestoration
|
||||||
|
window.history.scrollRestoration = 'manual'
|
||||||
|
|
||||||
|
// Check if the route explicitly disables automatic scroll to top
|
||||||
const routeAllowsScrollToTop = typeof to.meta.scrollToTop === 'function' ? to.meta.scrollToTop(to, from) : to.meta.scrollToTop
|
const routeAllowsScrollToTop = typeof to.meta.scrollToTop === 'function' ? to.meta.scrollToTop(to, from) : to.meta.scrollToTop
|
||||||
|
if (routeAllowsScrollToTop === false) {
|
||||||
// Scroll to top if route is changed by default
|
return false // Do not scroll to top if the route disallows it
|
||||||
if (!position && from && to && routeAllowsScrollToTop !== false && isChangingPage(to, from)) {
|
|
||||||
position = { left: 0, top: 0 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash routes on the same page, no page hook is fired so resolve here
|
// Handle same page navigation or first load
|
||||||
if (to.path === from.path) {
|
if (isFirstLoad || to.path === from.path) {
|
||||||
if (from.hash && !to.hash) {
|
return _calculatePosition(to, savedPosition, to.path === from.path || isFirstLoad ? scrollBehaviorType : 'instant')
|
||||||
return { left: 0, top: 0 }
|
|
||||||
}
|
|
||||||
if (to.hash) {
|
|
||||||
return { el: to.hash, top: _getHashElementScrollMarginTop(to.hash), behavior }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for `page:transition:finish` or `page:finish` depending on if transitions are enabled or not
|
// Check if the route change involves a page transition
|
||||||
const hasTransition = (route: RouteLocationNormalized) => !!(route.meta.pageTransition ?? defaultPageTransition)
|
const routeHasTransition = (route: RouteLocationNormalized): boolean => {
|
||||||
const hookToWait = (hasTransition(from) && hasTransition(to)) ? 'page:transition:finish' : 'page:finish'
|
return !!(route.meta.pageTransition ?? defaultPageTransition) && !isFirstLoad
|
||||||
|
}
|
||||||
|
const hasTransition = (routeHasTransition(from) && routeHasTransition(to))
|
||||||
|
|
||||||
|
// Wait for page transition to finish before applying the scroll behavior
|
||||||
|
const hookToWait = hasTransition ? 'page:transition:finish' : 'page:finish'
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
nuxtApp.hooks.hookOnce(hookToWait, async () => {
|
nuxtApp.hooks.hookOnce(hookToWait, () => {
|
||||||
await nextTick()
|
return nextTick(() => {
|
||||||
if (to.hash) {
|
// Without this setTimeout, the scroll behaviour is not applied, this is however working reliably
|
||||||
position = { el: to.hash, top: _getHashElementScrollMarginTop(to.hash), behavior }
|
// todo: figure out how we can solve this without setTimeout of 0ms
|
||||||
}
|
setTimeout(() => resolve(_calculatePosition(to, savedPosition, 'instant')), 0)
|
||||||
resolve(position)
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -64,3 +75,22 @@ function _getHashElementScrollMarginTop (selector: string): number {
|
|||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _calculatePosition (
|
||||||
|
to: RouteLocationNormalized,
|
||||||
|
savedPosition: ScrollPosition | null,
|
||||||
|
scrollBehaviorType: ScrollBehavior
|
||||||
|
): ScrollPosition {
|
||||||
|
// Handle saved position for backward/forward navigation
|
||||||
|
if (savedPosition) {
|
||||||
|
return savedPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to the element specified in the URL hash, if present
|
||||||
|
if (to.hash) {
|
||||||
|
return { el: to.hash, top: _getHashElementScrollMarginTop(to.hash), behavior: scrollBehaviorType }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default scroll to the top left of the page
|
||||||
|
return { left: 0, top: 0, behavior: scrollBehaviorType }
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user