From cc03a2b48c7cbc500a972bef44fef2dc476a4203 Mon Sep 17 00:00:00 2001 From: Yangfan Wu <39647285+wyf027@users.noreply.github.com> Date: Wed, 8 Jul 2026 04:35:12 +0800 Subject: [PATCH 1/5] feat(ui): support BackTop scroll target --- .../components/navigation/BackTop/BackTop.tsx | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/components/navigation/BackTop/BackTop.tsx b/packages/ui/src/components/navigation/BackTop/BackTop.tsx index 372bdf8..fcadd02 100644 --- a/packages/ui/src/components/navigation/BackTop/BackTop.tsx +++ b/packages/ui/src/components/navigation/BackTop/BackTop.tsx @@ -1,14 +1,25 @@ import { forwardRef, type ButtonHTMLAttributes, type MouseEvent, useEffect, useState } from 'react' import { cn } from '../../../utils/cn' +type BackTopTarget = HTMLElement | Window + export interface BackTopProps extends ButtonHTMLAttributes { visibilityHeight?: number + /** 自定义滚动容器,默认监听并滚动 window */ + target?: () => BackTopTarget | null +} + +const getScrollTop = (target: BackTopTarget) => (target === window ? window.scrollY : target.scrollTop) + +const scrollToTop = (target: BackTopTarget) => { + target.scrollTo({ top: 0, behavior: 'smooth' }) } export const BackTop = forwardRef(function BackTop( { className, visibilityHeight = 200, + target, children = '↑ Top', onClick, 'aria-label': ariaLabel = 'Back to top', @@ -19,13 +30,16 @@ export const BackTop = forwardRef(function Back const [visible, setVisible] = useState(false) useEffect(() => { + const scrollTarget = target?.() ?? window const onScroll = () => { - setVisible(window.scrollY > visibilityHeight) + setVisible(getScrollTop(scrollTarget) > visibilityHeight) } - window.addEventListener('scroll', onScroll) + + scrollTarget.addEventListener('scroll', onScroll) onScroll() - return () => window.removeEventListener('scroll', onScroll) - }, [visibilityHeight]) + + return () => scrollTarget.removeEventListener('scroll', onScroll) + }, [target, visibilityHeight]) if (!visible) return null @@ -35,7 +49,7 @@ export const BackTop = forwardRef(function Back return } - window.scrollTo({ top: 0, behavior: 'smooth' }) + scrollToTop(target?.() ?? window) } return ( From ca98a73da8cbf5b365ff38d2ef06ec533dda2aed Mon Sep 17 00:00:00 2001 From: Yangfan Wu <39647285+wyf027@users.noreply.github.com> Date: Wed, 8 Jul 2026 04:35:46 +0800 Subject: [PATCH 2/5] test(ui): cover BackTop custom target --- .../navigation/BackTop/BackTop.test.tsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/ui/src/components/navigation/BackTop/BackTop.test.tsx b/packages/ui/src/components/navigation/BackTop/BackTop.test.tsx index 42721ce..55622d9 100644 --- a/packages/ui/src/components/navigation/BackTop/BackTop.test.tsx +++ b/packages/ui/src/components/navigation/BackTop/BackTop.test.tsx @@ -13,6 +13,13 @@ const setScrollY = (value: number) => { }) } +const setTargetScrollTop = (target: HTMLElement, value: number) => { + target.scrollTop = value + act(() => { + target.dispatchEvent(new Event('scroll')) + }) +} + beforeEach(() => { Object.defineProperty(window, 'scrollTo', { configurable: true, @@ -48,6 +55,25 @@ describe('BackTop', () => { expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' }) }) + it('uses a custom scroll target for visibility and scrolling', () => { + const scrollTarget = document.createElement('div') + const scrollTo = vi.fn() + Object.defineProperty(scrollTarget, 'scrollTo', { + configurable: true, + value: scrollTo, + }) + + render( scrollTarget} />) + + expect(screen.queryByRole('button')).not.toBeInTheDocument() + + setTargetScrollTop(scrollTarget, 101) + fireEvent.click(screen.getByRole('button', { name: 'Back to top' })) + + expect(scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' }) + expect(window.scrollTo).not.toHaveBeenCalled() + }) + it('lets callers prevent the default scroll behavior', () => { render( Date: Wed, 8 Jul 2026 04:36:04 +0800 Subject: [PATCH 3/5] docs: document BackTop target --- docs/components/back-top.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/components/back-top.md b/docs/components/back-top.md index d5f9c96..238a90d 100644 --- a/docs/components/back-top.md +++ b/docs/components/back-top.md @@ -15,17 +15,40 @@ } `" /> +### 自定义滚动容器 + + + ## 可访问性 `BackTop` 默认渲染为带有 `aria-label="Back to top"` 的按钮,避免辅助技术读取装饰性箭头符号。可以通过 `aria-label` 提供本地化或更贴合业务语境的按钮名称。 -点击按钮时组件会先调用传入的 `onClick`,如果事件未被 `preventDefault()` 阻止,再执行平滑滚动到页面顶部。 +点击按钮时组件会先调用传入的 `onClick`,如果事件未被 `preventDefault()` 阻止,再执行平滑滚动到页面顶部或 `target` 指定容器顶部。 ## API | 属性 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | | visibilityHeight | 滚动高度达到此参数值才出现 | `number` | `200` | +| target | 自定义滚动容器 | `() => HTMLElement \| Window \| null` | `() => window` | | children | 自定义按钮内容 | `ReactNode` | `'↑ Top'` | | aria-label | 按钮可访问名称 | `string` | `'Back to top'` | | onClick | 点击回调;可通过 `event.preventDefault()` 阻止默认滚动 | `(event: MouseEvent) => void` | - | From 3a857bc042573996e799102065de523ad583d95b Mon Sep 17 00:00:00 2001 From: Yangfan Wu <39647285+wyf027@users.noreply.github.com> Date: Wed, 8 Jul 2026 04:37:14 +0800 Subject: [PATCH 4/5] fix(ui): narrow BackTop target type --- packages/ui/src/components/navigation/BackTop/BackTop.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/navigation/BackTop/BackTop.tsx b/packages/ui/src/components/navigation/BackTop/BackTop.tsx index fcadd02..5b0b89e 100644 --- a/packages/ui/src/components/navigation/BackTop/BackTop.tsx +++ b/packages/ui/src/components/navigation/BackTop/BackTop.tsx @@ -9,7 +9,9 @@ export interface BackTopProps extends ButtonHTMLAttributes { target?: () => BackTopTarget | null } -const getScrollTop = (target: BackTopTarget) => (target === window ? window.scrollY : target.scrollTop) +const isWindowTarget = (target: BackTopTarget): target is Window => target === window + +const getScrollTop = (target: BackTopTarget) => (isWindowTarget(target) ? target.scrollY : target.scrollTop) const scrollToTop = (target: BackTopTarget) => { target.scrollTo({ top: 0, behavior: 'smooth' }) From eaf1897fcea93415b9866b58c371372a6f042330 Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Wed, 8 Jul 2026 07:11:04 +0800 Subject: [PATCH 5/5] fix(ui): narrow BackTop scroll target --- packages/ui/src/components/navigation/BackTop/BackTop.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/navigation/BackTop/BackTop.tsx b/packages/ui/src/components/navigation/BackTop/BackTop.tsx index 5b0b89e..16684b7 100644 --- a/packages/ui/src/components/navigation/BackTop/BackTop.tsx +++ b/packages/ui/src/components/navigation/BackTop/BackTop.tsx @@ -9,7 +9,8 @@ export interface BackTopProps extends ButtonHTMLAttributes { target?: () => BackTopTarget | null } -const isWindowTarget = (target: BackTopTarget): target is Window => target === window +const isWindowTarget = (target: BackTopTarget): target is Window => + target === window || (typeof Window !== 'undefined' && target instanceof Window) const getScrollTop = (target: BackTopTarget) => (isWindowTarget(target) ? target.scrollY : target.scrollTop)