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] 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;