/** * @file 水平滚动位置相关逻辑 * @desc fixColumn: true 时需要水平滚动位置以维护 fixed column 的阴影效果、sticky 偏移等 */ import { debounce } from 'lodash' import React, { createContext, ReactNode, useContext, useEffect, useMemo, useState } from 'react' import { addListener as addResizeListener, removeListener as removeResizeListener } from 'resize-detector' export type HorizontalScrollContextValue = { scrollLeft: number scrollRight: number setScrollContainer: (el: HTMLElement | null) => void setSideScrollContainer: (el: HTMLElement | null) => void } const horizontalScrollContext = createContext({ scrollLeft: 0, scrollRight: 0, setScrollContainer: () => undefined, setSideScrollContainer: () => undefined }) export function useHorizontalScroll() { return useContext(horizontalScrollContext) } export function HorizontalScrollProvider({ children }: { children: ReactNode }) { const [scrollContainer, setScrollContainer] = useState(null) const [sideScrollContainer, setSideScrollContainer] = useState(null) const [scrollLeft, setScrollLeft] = useState(0) const [scrollRight, setScrollRight] = useState(0) const ctxValue = useMemo(() => ({ scrollLeft, scrollRight, setScrollContainer, setSideScrollContainer }), [scrollLeft, scrollRight, setScrollContainer]) // scrollContainer 发生滚动时同步到 sideScrollContainer,并更新 scrollLeft & setScrollRight useEffect(() => { if (scrollContainer == null) return const syncScrollSize = debounce(() => { const { scrollLeft: currentScrollLeft, scrollWidth, clientWidth } = scrollContainer setScrollLeft(currentScrollLeft) setScrollRight(scrollWidth - clientWidth - currentScrollLeft) }, 100) const handleScroll = () => { if (sideScrollContainer != null) { sideScrollContainer.scrollLeft = scrollContainer.scrollLeft } syncScrollSize() } syncScrollSize() scrollContainer.addEventListener('scroll', handleScroll) addResizeListener(scrollContainer, syncScrollSize) return () => { scrollContainer.removeEventListener('scroll', handleScroll) removeResizeListener(scrollContainer, syncScrollSize) } }, [scrollContainer, sideScrollContainer]) // sideScrollContainer 发生 wheel 事件时手动触发其滚动,并同步到 sideScrollContainer // 注意 sideScrollContainer 我们会处理成 overflow-x: hidden(以避免滚动条),所以需要手动触发其滚动来模拟 overflow-x: scroll 的效果 useEffect(() => { if (sideScrollContainer == null) return const handleWheel = (e: WheelEvent) => { const originalScrollLeft = sideScrollContainer.scrollLeft sideScrollContainer.scrollLeft += e.deltaX const newScrollLeft = sideScrollContainer.scrollLeft if (newScrollLeft !== originalScrollLeft) { // 触发了滚动 if (scrollContainer != null) { scrollContainer.scrollLeft = newScrollLeft } e.preventDefault() } } sideScrollContainer.addEventListener('wheel', handleWheel) return () => sideScrollContainer.removeEventListener('wheel', handleWheel) }, [scrollContainer, sideScrollContainer]) return ( {children} ) }