背景
用户反馈列表横向太长,横向滚动的体验不好,无法快速定位到想要的列,因此决定在列表上方提供一个快速定位列的组件帮助用户快速定位到某列
实现
效果图
核心思路
找到 antd table 的滚动容器,利用 colgroup 下 td 计算出对应列的宽度并做响应式监听,最后调用容器的 scrollTo 方法即可
代码
import { useState, useCallback, useEffect } from 'react'
const useHorizontalScroll = (scrollRef: React.RefObject<HTMLDivElement>, stickyNum: number) => {
const [columnWidths, setColumnWidths] = useState<number[]>([])
// 计算前缀和
const cumulativeWidths = columnWidths.reduce(
(acc, curr, i) => [...acc, (acc[i - 1] || 0) + curr],
[] as number[]
)
const getContainer = useCallback(() => {
if (!scrollRef.current) return null
return scrollRef.current.querySelector('.ant-table-body')
}, [scrollRef])
// 用于初始化及重新计算每列的宽度
const calculateColumnWidths = useCallback(() => {
const container = getContainer()
if (!container) return false
try {
// HACK FOR ANTD ant-table-measure-row
const thElements = container.querySelectorAll('.ant-table-measure-row td')
if (thElements.length === 0) return false
const widths = Array.from(thElements).map((th: any) => th.offsetWidth)
setColumnWidths(widths.slice(stickyNum))
return true
} catch (e) {
console.error('Failed to calculate column widths:', e)
return false
}
}, [stickyNum, getContainer])
useEffect(() => {
let resizeObserver: ResizeObserver
const initializeObserver = () => {
const container = getContainer()
if (!container) {
return
}
try {
calculateColumnWidths()
resizeObserver = new ResizeObserver(() => {
calculateColumnWidths()
})
resizeObserver.observe(container)
} catch (e) {
console.error('Failed to initialize observer:', e)
}
}
initializeObserver()
return () => {
if (resizeObserver) {
resizeObserver.disconnect()
}
}
}, [calculateColumnWidths, getContainer])
// 滚动到指定方向的列
const scrollToColumn = useCallback((direction: 'left' | 'right', step = 1) => {
const container = getContainer()
if (!container) return
const currentScroll = container.scrollLeft
// 找到当前可视区域内最接近左侧(或右侧)的列索引
let targetColIndex = cumulativeWidths.findIndex(
(cumulativeWidth) => cumulativeWidth > currentScroll
)
if (direction === 'left') {
// 向左滚动,索引减去 1
targetColIndex = Math.max(targetColIndex - step, 0)
} else if (direction === 'right') {
// 向右滚动,索引加上 1
// 需要注意不要超过最后一个列的索引
targetColIndex = Math.min(targetColIndex + step, columnWidths.length - 1)
}
// 确定滚动位置:对于向左,直接使用前缀和;向右,使用当前列的前缀和
const targetScrollPosition = cumulativeWidths[targetColIndex - 1] || 0
container.scrollTo({
left:
targetColIndex >= cumulativeWidths.length - 1
? container.scrollWidth
: targetScrollPosition,
behavior: step === 1 ? 'smooth' : 'auto'
})
}, [cumulativeWidths, getContainer])
const scrollToIndexColumn = useCallback((targetColIndex: number) => {
const container = getContainer()
if (!container) return
const targetScrollPosition = cumulativeWidths[targetColIndex - 1] || 0
container.scrollTo({
left:
targetColIndex >= cumulativeWidths.length - 1
? container.scrollWidth
: targetScrollPosition,
behavior: 'smooth'
})
}, [cumulativeWidths, getContainer])
return { scrollRef, scrollToColumn, scrollToIndexColumn }
}
export default useHorizontalScroll