From baea741432ce4e5332a857873be444ec4b7edf15 Mon Sep 17 00:00:00 2001 From: Dream <1012377328@qq.com> Date: Fri, 24 Jul 2026 16:55:36 +0800 Subject: [PATCH 1/5] feat(@vben-core/shared): add layout scroll helpers --- .../base/shared/src/constants/globals.ts | 2 + .../shared/src/utils/__tests__/dom.test.ts | 85 ++++++++++++++++++- packages/@core/base/shared/src/utils/dom.ts | 21 ++++- 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/packages/@core/base/shared/src/constants/globals.ts b/packages/@core/base/shared/src/constants/globals.ts index 3c699570..b86c45cd 100644 --- a/packages/@core/base/shared/src/constants/globals.ts +++ b/packages/@core/base/shared/src/constants/globals.ts @@ -9,6 +9,8 @@ export const CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT = `--vben-footer-height`; /** 内容区域的组件ID */ export const ELEMENT_ID_MAIN_CONTENT = `__vben_main_content`; +/** layout 滚动容器ID */ +export const ELEMENT_ID_LAYOUT_SCROLL = `__vben_layout_scroll`; /** * @zh_CN 默认命名空间 diff --git a/packages/@core/base/shared/src/utils/__tests__/dom.test.ts b/packages/@core/base/shared/src/utils/__tests__/dom.test.ts index ffc5b49f..33df4d50 100644 --- a/packages/@core/base/shared/src/utils/__tests__/dom.test.ts +++ b/packages/@core/base/shared/src/utils/__tests__/dom.test.ts @@ -1,6 +1,11 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { getElementVisibleRect } from '../dom'; +import { ELEMENT_ID_LAYOUT_SCROLL } from '../../constants'; +import { + getElementVisibleRect, + getLayoutScrollElement, + needsScrollbar, +} from '../dom'; describe('getElementVisibleRect', () => { // 设置浏览器视口尺寸的 mock @@ -15,6 +20,10 @@ describe('getElementVisibleRect', () => { vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1000); }); + afterEach(() => { + vi.restoreAllMocks(); + }); + it('should return default rect if element is undefined', () => { expect(getElementVisibleRect()).toEqual({ bottom: 0, @@ -125,3 +134,75 @@ describe('getElementVisibleRect', () => { }); }); }); + +describe('getLayoutScrollElement', () => { + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('should return the layout scroll element', () => { + const element = document.createElement('div'); + element.id = ELEMENT_ID_LAYOUT_SCROLL; + document.body.append(element); + + expect(getLayoutScrollElement()).toBe(element); + }); + + it('should return null when the layout scroll element is missing', () => { + expect(getLayoutScrollElement()).toBeNull(); + }); +}); + +describe('needsScrollbar', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should check scrollbar state from target element', () => { + const element = document.createElement('div'); + vi.spyOn(element, 'clientHeight', 'get').mockReturnValue(100); + vi.spyOn(element, 'scrollHeight', 'get').mockReturnValue(120); + vi.spyOn(window, 'getComputedStyle').mockReturnValue({ + overflowY: 'auto', + } as CSSStyleDeclaration); + + expect(needsScrollbar(element)).toBe(true); + }); + + it('should return false when target content does not overflow', () => { + const element = document.createElement('div'); + vi.spyOn(element, 'clientHeight', 'get').mockReturnValue(100); + vi.spyOn(element, 'scrollHeight', 'get').mockReturnValue(100); + vi.spyOn(window, 'getComputedStyle').mockReturnValue({ + overflowY: 'auto', + } as CSSStyleDeclaration); + + expect(needsScrollbar(element)).toBe(false); + }); + + it.each(['clip', 'hidden'])( + 'should ignore %s overflow targets', + (overflowY) => { + const element = document.createElement('div'); + vi.spyOn(element, 'clientHeight', 'get').mockReturnValue(100); + vi.spyOn(element, 'scrollHeight', 'get').mockReturnValue(120); + vi.spyOn(window, 'getComputedStyle').mockReturnValue({ + overflowY, + } as CSSStyleDeclaration); + + expect(needsScrollbar(element)).toBe(false); + }, + ); + + it('should fall back to document scrollbar state', () => { + vi.spyOn(document.documentElement, 'scrollHeight', 'get').mockReturnValue( + 120, + ); + vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(100); + vi.spyOn(window, 'getComputedStyle').mockReturnValue({ + overflowY: 'auto', + } as CSSStyleDeclaration); + + expect(needsScrollbar()).toBe(true); + }); +}); diff --git a/packages/@core/base/shared/src/utils/dom.ts b/packages/@core/base/shared/src/utils/dom.ts index 35a7e5ff..7c9af078 100644 --- a/packages/@core/base/shared/src/utils/dom.ts +++ b/packages/@core/base/shared/src/utils/dom.ts @@ -1,3 +1,5 @@ +import { ELEMENT_ID_LAYOUT_SCROLL } from '../constants/globals'; + export interface VisibleDomRect { bottom: number; height: number; @@ -82,7 +84,24 @@ export function getScrollbarWidth() { return scrollbarWidth; } -export function needsScrollbar() { +export function getLayoutScrollElement() { + return document.querySelector(`#${ELEMENT_ID_LAYOUT_SCROLL}`); +} + +function elementNeedsScrollbar(element: HTMLElement) { + const overflowY = window.getComputedStyle(element).overflowY; + if (overflowY === 'hidden' || overflowY === 'clip') { + return false; + } + + return element.scrollHeight > element.clientHeight; +} + +export function needsScrollbar(target?: HTMLElement | null) { + if (target) { + return elementNeedsScrollbar(target); + } + const doc = document.documentElement; const body = document.body; From 9a5d052fa517b74c790f6422e8cd5141601c4f44 Mon Sep 17 00:00:00 2001 From: Dream <1012377328@qq.com> Date: Fri, 24 Jul 2026 16:56:35 +0800 Subject: [PATCH 2/5] fix(@vben-core/composables): lock layout scroll container --- .../src/__tests__/use-scroll-lock.test.ts | 129 ++++++++++++++++++ .../@core/composables/src/use-scroll-lock.ts | 128 ++++++++++++++--- 2 files changed, 236 insertions(+), 21 deletions(-) create mode 100644 packages/@core/composables/src/__tests__/use-scroll-lock.test.ts diff --git a/packages/@core/composables/src/__tests__/use-scroll-lock.test.ts b/packages/@core/composables/src/__tests__/use-scroll-lock.test.ts new file mode 100644 index 00000000..4e30ef9a --- /dev/null +++ b/packages/@core/composables/src/__tests__/use-scroll-lock.test.ts @@ -0,0 +1,129 @@ +import type { App, WritableComputedRef } from 'vue'; + +import { createApp, nextTick } from 'vue'; + +import { ELEMENT_ID_LAYOUT_SCROLL } from '@vben-core/shared/constants'; + +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { SCROLL_FIXED_CLASS, useScrollLock } from '../use-scroll-lock'; + +let activeApp: App | undefined; + +function createScrollableElement(id?: string) { + const element = document.createElement('div'); + if (id) { + element.id = id; + } + element.style.overflow = 'auto'; + Object.defineProperties(element, { + clientHeight: { configurable: true, value: 100 }, + scrollHeight: { configurable: true, value: 120 }, + }); + document.body.append(element); + return element; +} + +function mountScrollLock(options?: { immediate?: boolean }) { + let scrollLock: undefined | WritableComputedRef; + const host = document.createElement('div'); + document.body.append(host); + + activeApp = createApp({ + setup() { + scrollLock = useScrollLock(options); + return () => null; + }, + }); + activeApp.mount(host); + + if (!scrollLock) { + throw new Error('useScrollLock was not initialized'); + } + return scrollLock; +} + +async function flushMountedLock() { + await nextTick(); + await nextTick(); +} + +afterEach(() => { + activeApp?.unmount(); + activeApp = undefined; + document.body.innerHTML = ''; + document.body.style.cssText = ''; + vi.restoreAllMocks(); +}); + +describe('useScrollLock', () => { + it('should lock the layout scroll element first', async () => { + const element = createScrollableElement(ELEMENT_ID_LAYOUT_SCROLL); + element.style.setProperty('scrollbar-gutter', 'auto'); + const scrollLock = mountScrollLock(); + + await flushMountedLock(); + + expect(scrollLock.value).toBe(true); + expect(element.style.overflow).toBe('hidden'); + expect(element.style.getPropertyValue('scrollbar-gutter')).toBe('stable'); + expect(document.body.style.overflow).not.toBe('hidden'); + + activeApp?.unmount(); + activeApp = undefined; + + expect(element.style.overflow).toBe('auto'); + expect(element.style.getPropertyValue('scrollbar-gutter')).toBe('auto'); + }); + + it('should support manual locking', async () => { + const element = createScrollableElement(ELEMENT_ID_LAYOUT_SCROLL); + const scrollLock = mountScrollLock({ immediate: false }); + + await flushMountedLock(); + expect(scrollLock.value).toBe(false); + + scrollLock.value = true; + expect(element.style.overflow).toBe('hidden'); + expect(element.style.getPropertyValue('scrollbar-gutter')).toBe('stable'); + + scrollLock.value = false; + expect(element.style.overflow).toBe('auto'); + expect(element.style.getPropertyValue('scrollbar-gutter')).toBe(''); + }); + + it('should fall back to body and compensate fixed nodes', async () => { + document.body.style.overflow = 'auto'; + Object.defineProperties(document.body, { + clientHeight: { configurable: true, value: 100 }, + scrollHeight: { configurable: true, value: 120 }, + }); + vi.spyOn(document.documentElement, 'scrollHeight', 'get').mockReturnValue( + 120, + ); + vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(100); + vi.spyOn(window, 'getComputedStyle').mockReturnValue({ + overflowY: 'auto', + } as CSSStyleDeclaration); + + const fixedNode = document.createElement('div'); + fixedNode.className = SCROLL_FIXED_CLASS; + fixedNode.style.transition = 'opacity 200ms'; + document.body.append(fixedNode); + const scrollLock = mountScrollLock(); + + await flushMountedLock(); + + expect(scrollLock.value).toBe(true); + expect(document.body.style.overflow).toBe('hidden'); + expect(document.body.style.paddingRight).toBe('0px'); + expect(fixedNode.style.paddingRight).toBe('0px'); + + activeApp?.unmount(); + activeApp = undefined; + + expect(document.body.style.overflow).toBe('auto'); + expect(document.body.style.paddingRight).toBe(''); + expect(fixedNode.style.paddingRight).toBe(''); + }); +}); diff --git a/packages/@core/composables/src/use-scroll-lock.ts b/packages/@core/composables/src/use-scroll-lock.ts index d1c14975..1e176f53 100644 --- a/packages/@core/composables/src/use-scroll-lock.ts +++ b/packages/@core/composables/src/use-scroll-lock.ts @@ -1,4 +1,10 @@ -import { getScrollbarWidth, needsScrollbar } from '@vben-core/shared/utils'; +import { computed, nextTick, shallowRef } from 'vue'; + +import { + getLayoutScrollElement, + getScrollbarWidth, + needsScrollbar, +} from '@vben-core/shared/utils'; import { useScrollLock as _useScrollLock, @@ -8,20 +14,60 @@ import { export const SCROLL_FIXED_CLASS = `_scroll__fixed_`; -export function useScrollLock() { - const isLocked = _useScrollLock(document.body); - const scrollbarWidth = getScrollbarWidth(); +interface ScrollLockOptions { + immediate?: boolean; +} - tryOnMounted(() => { - if (!needsScrollbar()) { +function getScrollLockTarget() { + return getLayoutScrollElement() ?? document.body; +} + +function getLayoutFixedNodes() { + return [...document.querySelectorAll(`.${SCROLL_FIXED_CLASS}`)]; +} + +export function useScrollLock(options: ScrollLockOptions = {}) { + const { immediate = true } = options; + const lockTarget = shallowRef(null); + const isTargetLocked = _useScrollLock(lockTarget); + const scrollbarWidth = getScrollbarWidth(); + let hasScrollbarCompensation = false; + let hasScrollbarGutter = false; + + function applyScrollbarGutter(target: HTMLElement) { + if (target === document.body) { return; } - document.body.style.paddingRight = `${scrollbarWidth}px`; - const layoutFixedNodes = document.querySelectorAll( - `.${SCROLL_FIXED_CLASS}`, - ); - const nodes = [...layoutFixedNodes]; + target.dataset.scrollbarGutter = + target.style.getPropertyValue('scrollbar-gutter'); + target.style.setProperty('scrollbar-gutter', 'stable'); + hasScrollbarGutter = true; + } + + function resetScrollbarGutter(target: HTMLElement) { + if (!hasScrollbarGutter) { + return; + } + + const scrollbarGutter = target.dataset.scrollbarGutter; + if (scrollbarGutter) { + target.style.setProperty('scrollbar-gutter', scrollbarGutter); + } else { + target.style.removeProperty('scrollbar-gutter'); + } + delete target.dataset.scrollbarGutter; + hasScrollbarGutter = false; + } + + function applyScrollbarCompensation(target: HTMLElement) { + if (target !== document.body || !needsScrollbar()) { + return; + } + + target.style.paddingRight = `${scrollbarWidth}px`; + + const nodes = getLayoutFixedNodes(); if (nodes.length > 0) { nodes.forEach((node) => { node.dataset.transition = node.style.transition; @@ -29,18 +75,15 @@ export function useScrollLock() { node.style.paddingRight = `${scrollbarWidth}px`; }); } - isLocked.value = true; - }); + hasScrollbarCompensation = true; + } - tryOnBeforeUnmount(() => { - if (!needsScrollbar()) { + function resetScrollbarCompensation(target: HTMLElement) { + if (!hasScrollbarCompensation) { return; } - isLocked.value = false; - const layoutFixedNodes = document.querySelectorAll( - `.${SCROLL_FIXED_CLASS}`, - ); - const nodes = [...layoutFixedNodes]; + + const nodes = getLayoutFixedNodes(); if (nodes.length > 0) { nodes.forEach((node) => { node.style.paddingRight = ''; @@ -49,6 +92,49 @@ export function useScrollLock() { }); }); } - document.body.style.paddingRight = ''; + target.style.paddingRight = ''; + hasScrollbarCompensation = false; + } + + const isLocked = computed({ + get() { + return isTargetLocked.value; + }, + set(value: boolean) { + const target = lockTarget.value ?? getScrollLockTarget(); + lockTarget.value = target; + + if (value) { + if (isTargetLocked.value) { + return; + } + if (needsScrollbar(target)) { + applyScrollbarGutter(target); + applyScrollbarCompensation(target); + } + isTargetLocked.value = true; + return; + } + + isTargetLocked.value = false; + resetScrollbarCompensation(target); + resetScrollbarGutter(target); + }, }); + + tryOnMounted(async () => { + const target = getScrollLockTarget(); + lockTarget.value = target; + await nextTick(); + + if (immediate && needsScrollbar(target)) { + isLocked.value = true; + } + }); + + tryOnBeforeUnmount(() => { + isLocked.value = false; + }); + + return isLocked; } From aaf0da582c4f01f6cc138a3592e70af365b2ef06 Mon Sep 17 00:00:00 2001 From: Dream <1012377328@qq.com> Date: Fri, 24 Jul 2026 16:57:32 +0800 Subject: [PATCH 3/5] fix(@vben-core/layout-ui): stabilize internal layout scrolling --- .../src/__tests__/header-scroll-state.test.ts | 62 +++ .../src/components/layout-footer.vue | 3 +- .../src/components/layout-sidebar.vue | 5 +- .../layout-ui/src/header-scroll-state.ts | 28 ++ .../ui-kit/layout-ui/src/vben-layout.vue | 357 ++++++++++-------- 5 files changed, 304 insertions(+), 151 deletions(-) create mode 100644 packages/@core/ui-kit/layout-ui/src/__tests__/header-scroll-state.test.ts create mode 100644 packages/@core/ui-kit/layout-ui/src/header-scroll-state.ts diff --git a/packages/@core/ui-kit/layout-ui/src/__tests__/header-scroll-state.test.ts b/packages/@core/ui-kit/layout-ui/src/__tests__/header-scroll-state.test.ts new file mode 100644 index 00000000..180d0f03 --- /dev/null +++ b/packages/@core/ui-kit/layout-ui/src/__tests__/header-scroll-state.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from 'vitest'; + +import { resolveHeaderHiddenOnScroll } from '../header-scroll-state'; + +const baseOptions = { + arrivedTop: false, + currentHidden: false, + directionDown: false, + directionUp: false, + headerHeight: 90, + scrollTop: 120, +}; + +describe('resolveHeaderHiddenOnScroll', () => { + it('should show the header near the top', () => { + expect( + resolveHeaderHiddenOnScroll({ + ...baseOptions, + directionDown: true, + scrollTop: 89, + }), + ).toBe(false); + }); + + it('should show the header when the top is reached', () => { + expect( + resolveHeaderHiddenOnScroll({ + ...baseOptions, + arrivedTop: true, + currentHidden: true, + }), + ).toBe(false); + }); + + it('should show the header while scrolling up', () => { + expect( + resolveHeaderHiddenOnScroll({ + ...baseOptions, + currentHidden: true, + directionUp: true, + }), + ).toBe(false); + }); + + it('should hide the header while scrolling down', () => { + expect( + resolveHeaderHiddenOnScroll({ + ...baseOptions, + directionDown: true, + }), + ).toBe(true); + }); + + it('should preserve the current state without a direction', () => { + expect( + resolveHeaderHiddenOnScroll({ + ...baseOptions, + currentHidden: true, + }), + ).toBe(true); + }); +}); diff --git a/packages/@core/ui-kit/layout-ui/src/components/layout-footer.vue b/packages/@core/ui-kit/layout-ui/src/components/layout-footer.vue index 3793f922..1089facd 100644 --- a/packages/@core/ui-kit/layout-ui/src/components/layout-footer.vue +++ b/packages/@core/ui-kit/layout-ui/src/components/layout-footer.vue @@ -28,6 +28,7 @@ const style = computed((): CSSProperties => { height: `${height}px`, marginBottom: show ? '0' : `-${height}px`, position: fixed ? 'fixed' : 'static', + transform: show ? 'translateY(0)' : 'translateY(100%)', width, zIndex, }; @@ -37,7 +38,7 @@ const style = computed((): CSSProperties => { diff --git a/packages/effects/layouts/src/basic/use-layout-scroll.ts b/packages/effects/layouts/src/basic/use-layout-scroll.ts new file mode 100644 index 00000000..d4ee372c --- /dev/null +++ b/packages/effects/layouts/src/basic/use-layout-scroll.ts @@ -0,0 +1,85 @@ +import type { Router } from 'vue-router'; + +import { nextTick, onScopeDispose } from 'vue'; +import { useRouter } from 'vue-router'; + +import { getLayoutScrollElement } from '@vben-core/shared/utils'; + +type LayoutScrollRouter = Pick; + +function getHistoryPosition() { + if (typeof window === 'undefined') { + return undefined; + } + const position = (window.history.state as null | { position?: unknown }) + ?.position; + return typeof position === 'number' ? position : undefined; +} + +function getHashTarget(hash: string) { + if (typeof document === 'undefined' || !hash.startsWith('#')) { + return null; + } + + const id = hash.slice(1); + try { + return document.querySelector( + `#${CSS.escape(decodeURIComponent(id))}`, + ); + } catch { + return document.querySelector(`#${CSS.escape(id)}`); + } +} + +export function useLayoutScroll(router: LayoutScrollRouter = useRouter()) { + const scrollPositions = new Map(); + let currentHistoryPosition = getHistoryPosition(); + let isHistoryNavigation = false; + + const removeBeforeGuard = router.beforeEach(() => { + const scrollElement = getLayoutScrollElement(); + if (scrollElement && currentHistoryPosition !== undefined) { + scrollPositions.set(currentHistoryPosition, scrollElement.scrollTop); + } + + const nextHistoryPosition = getHistoryPosition(); + isHistoryNavigation = + currentHistoryPosition !== undefined && + nextHistoryPosition !== undefined && + currentHistoryPosition !== nextHistoryPosition; + }); + + const removeAfterHook = router.afterEach(async (to, _from, failure) => { + const nextHistoryPosition = getHistoryPosition(); + + if (!failure) { + await nextTick(); + const scrollElement = getLayoutScrollElement(); + if (scrollElement) { + const savedPosition = + isHistoryNavigation && nextHistoryPosition !== undefined + ? scrollPositions.get(nextHistoryPosition) + : undefined; + + if (savedPosition === undefined) { + const hashTarget = getHashTarget(to.hash); + if (hashTarget) { + hashTarget.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } else { + scrollElement.scrollTo({ top: 0 }); + } + } else { + scrollElement.scrollTo({ top: savedPosition }); + } + } + } + + currentHistoryPosition = nextHistoryPosition; + isHistoryNavigation = false; + }); + + onScopeDispose(() => { + removeBeforeGuard(); + removeAfterHook(); + }); +} From 6d5ccc7ac81f85e86fd42184111fa76b299f0bf0 Mon Sep 17 00:00:00 2001 From: Dream <1012377328@qq.com> Date: Fri, 24 Jul 2026 17:32:51 +0800 Subject: [PATCH 5/5] fix(@vben/layouts): scope hash targets to layout --- .../basic/__tests__/use-layout-scroll.test.ts | 25 +++++++++++++++++++ .../layouts/src/basic/use-layout-scroll.ts | 10 ++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/effects/layouts/src/basic/__tests__/use-layout-scroll.test.ts b/packages/effects/layouts/src/basic/__tests__/use-layout-scroll.test.ts index f4a12e3e..fdee67ee 100644 --- a/packages/effects/layouts/src/basic/__tests__/use-layout-scroll.test.ts +++ b/packages/effects/layouts/src/basic/__tests__/use-layout-scroll.test.ts @@ -116,6 +116,31 @@ describe('useLayoutScroll', () => { expect(element.scrollTo).not.toHaveBeenCalled(); }); + it('should ignore matching hash targets outside the layout', async () => { + window.history.replaceState({ position: 0 }, ''); + const hostTarget = document.createElement('div'); + hostTarget.id = 'section'; + hostTarget.scrollIntoView = vi.fn(); + document.body.append(hostTarget); + const element = createScrollElement(); + const layoutTarget = document.createElement('div'); + layoutTarget.id = 'section'; + layoutTarget.scrollIntoView = vi.fn(); + element.append(layoutTarget); + const routerMock = createRouterMock(); + mountLayoutScroll(routerMock.router); + const { afterHook } = routerMock.getHooks(); + + window.history.replaceState({ position: 1 }, ''); + await runAfterHook(afterHook, '#section'); + + expect(layoutTarget.scrollIntoView).toHaveBeenCalledWith({ + behavior: 'smooth', + block: 'start', + }); + expect(hostTarget.scrollIntoView).not.toHaveBeenCalled(); + }); + it('should restore a saved position on history navigation', async () => { window.history.replaceState({ position: 0 }, ''); const element = createScrollElement(); diff --git a/packages/effects/layouts/src/basic/use-layout-scroll.ts b/packages/effects/layouts/src/basic/use-layout-scroll.ts index d4ee372c..9aa89aca 100644 --- a/packages/effects/layouts/src/basic/use-layout-scroll.ts +++ b/packages/effects/layouts/src/basic/use-layout-scroll.ts @@ -16,18 +16,18 @@ function getHistoryPosition() { return typeof position === 'number' ? position : undefined; } -function getHashTarget(hash: string) { - if (typeof document === 'undefined' || !hash.startsWith('#')) { +function getHashTarget(scrollElement: HTMLElement, hash: string) { + if (!hash.startsWith('#')) { return null; } const id = hash.slice(1); try { - return document.querySelector( + return scrollElement.querySelector( `#${CSS.escape(decodeURIComponent(id))}`, ); } catch { - return document.querySelector(`#${CSS.escape(id)}`); + return scrollElement.querySelector(`#${CSS.escape(id)}`); } } @@ -62,7 +62,7 @@ export function useLayoutScroll(router: LayoutScrollRouter = useRouter()) { : undefined; if (savedPosition === undefined) { - const hashTarget = getHashTarget(to.hash); + const hashTarget = getHashTarget(scrollElement, to.hash); if (hashTarget) { hashTarget.scrollIntoView({ behavior: 'smooth', block: 'start' }); } else {