/* eslint-disable no-lonely-if */ import React, { createContext, ReactNode, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef } from 'react' import { debounce } from 'lodash' import { ColumnWithLooseAccessor } from 'react-table' import { useColumnWidths } from './ColumnWidthsProvider' import { useSafeState } from '../../utils/hooks' type ContextValue = { pinnedLeft: number | null pinnedRight: number | null setPinned(dom: HTMLDivElement, list: Array): void pinnedWidthList: Array registerHeadDom: (dom: HTMLDivElement | null) => void registerBodyDom: (dom: HTMLDivElement | null) => void } const context = createContext({ pinnedLeft: null, pinnedRight: null, setPinned: () => undefined, pinnedWidthList: [], registerHeadDom: () => undefined, registerBodyDom: () => undefined }) export function useHorizontalScroll() { return useContext(context) } type Props = { children: ReactNode columns: Array> fixColumn: { contentWidth: string } } export default function HorizontalScrollProvider({ children, columns, fixColumn }: Props) { const { colWidths } = useColumnWidths() const [pinnedLeft, setPinnedLeft] = useSafeState(null) const [pinnedRight, setPinnedRight] = useSafeState(null) const headRef = useRef() const bodyRef = useRef() const scrollLeftRef = useRef(0) // 计算固定列在滚动到哪里时会被钉住(pinned),用于计算侧边阴影应该出现在哪一列上 // 假设列宽度数组是[10, 30, 10, 20, 40],pinnedWidthList 的值为 [0, null, 30, 40, null], // 其中第一、三列左固定,第四列右固定,只要向左的滚动大于0,第一列被钉住,阴影出 // 现在第一列的右侧,如果向左滚动大于30,第三列被钉住,阴影改为出现在第三列的右侧, // 如果向右滚动大于40,第四列被钉住,阴影出现在第四列的左侧 const pinnedWidthList = useMemo(() => { const result = Array(columns.length).fill(null) let leftWidth = 0 let rightWidth = 0 const columnWidthList = columns.map(column => (column.id ? colWidths[column.id] : 'auto')) if (fixColumn.contentWidth && !columnWidthList.some(col => col === 'auto')) { const widthList = columnWidthList.map(width => (typeof width === 'string' ? Number.parseFloat(width.replace('px', '')) : width)) columns.forEach((column, index) => { if (column.meta.fixed === 'left') { result[index] = leftWidth } else { leftWidth += widthList[index] } }) for (let index = columns.length - 1; index >= 0; index--) { const column = columns[index] if (column.meta.fixed === 'right') { result[index] = rightWidth } else { rightWidth += widthList[index] } } } return result }, [colWidths, columns, fixColumn.contentWidth]) const setPinned = useCallback((dom: HTMLDivElement, list: Array) => { const { scrollLeft, scrollWidth, clientWidth } = dom const availableScrollWidth = scrollWidth - clientWidth let currentPinnedLeft: number | null = null let currentPinnedRight: number | null = null columns .forEach((column, index) => { const pinnedWidth = list[index] if (pinnedWidth === null) return if (column.meta.fixed === 'left') { if (scrollLeft > pinnedWidth) { currentPinnedLeft = index } } if (column.meta.fixed === 'right' && currentPinnedRight === null) { if (availableScrollWidth - scrollLeft > pinnedWidth) { currentPinnedRight = index } } }) setPinnedLeft(currentPinnedLeft) setPinnedRight(currentPinnedRight) }, [columns, setPinnedLeft, setPinnedRight]) const setPinnedRef = useRef(setPinned) setPinnedRef.current = setPinned const debouncedSetPinned = useMemo( () => debounce((dom: HTMLDivElement, list: Array) => setPinnedRef.current(dom, list), 100), [] ) const registerHeadDom = useCallback((dom: HTMLDivElement | null) => { headRef.current = dom ?? undefined }, []) const registerBodyDom = useCallback((dom: HTMLDivElement | null) => { bodyRef.current = dom ?? undefined }, []) useEffect(() => { if (headRef.current == null || bodyRef.current == null) return const headDom = headRef.current const bodyDom = bodyRef.current function onHeadScroll(event: WheelEvent) { headDom.scrollLeft += event.deltaX bodyDom.scrollLeft = headDom.scrollLeft debouncedSetPinned(headDom, pinnedWidthList) } headRef.current.addEventListener('wheel', onHeadScroll) function onBodyScroll() { headDom.scrollLeft = bodyDom.scrollLeft scrollLeftRef.current = bodyDom.scrollLeft debouncedSetPinned(bodyDom, pinnedWidthList) } bodyRef.current.addEventListener('scroll', onBodyScroll) debouncedSetPinned(bodyRef.current, pinnedWidthList) return () => { headDom.removeEventListener('wheel', onHeadScroll) bodyDom.removeEventListener('scroll', onBodyScroll) } }, [debouncedSetPinned, pinnedWidthList]) // 防止 rerender 的过程导致 scrollLeft 丢失 // 比如 react-dom 更新机制要更新某个节点不是直接 update(key 不一样),先删除一列后再 insert 回来 // 会导致 scrollLeft 变化 // 这里重新 reset 回来 useLayoutEffect(() => { if (headRef.current == null || bodyRef.current == null) return const headDom = headRef.current const bodyDom = bodyRef.current headDom.scrollLeft = scrollLeftRef.current bodyDom.scrollLeft = scrollLeftRef.current }) // 重置 scrollLeft 的情况 // 可能要包含翻页等情况 useEffect(() => { if (headRef.current == null || bodyRef.current == null) return const headDom = headRef.current const bodyDom = bodyRef.current headDom.scrollLeft = 0 bodyDom.scrollLeft = 0 scrollLeftRef.current = 0 }, [fixColumn.contentWidth]) const contextValue = useMemo( () => ({ pinnedLeft, pinnedRight, setPinned: debouncedSetPinned, pinnedWidthList, registerHeadDom, registerBodyDom }), [debouncedSetPinned, pinnedLeft, pinnedRight, pinnedWidthList, registerBodyDom, registerHeadDom] ) return ( {children} ) }