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` | - |
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(
{
visibilityHeight?: number
+ /** 自定义滚动容器,默认监听并滚动 window */
+ target?: () => BackTopTarget | null
+}
+
+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)
+
+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 +33,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 +52,7 @@ export const BackTop = forwardRef(function Back
return
}
- window.scrollTo({ top: 0, behavior: 'smooth' })
+ scrollToTop(target?.() ?? window)
}
return (