import React, { createContext, useEffect, useRef, useState, forwardRef, useContext, useMemo, } from "react"; import classNames from "classnames"; import { callBoth } from "../../../_util/call-both"; import { mergeStyle } from "../../../_util/merge-style"; import { mergeRefs } from "../../../_util/merge-refs"; import { getScrollBarSize } from "../../../_util/get-scrollbar-size"; import { useConfig } from "../../../_util/config-context"; import { throttle } from "../../../affix/util"; /** * 滚动 Context */ interface ScrollContextValue { /** * 供表格内容区设置其 DOM Ref,拿到的 DOM 实例用于检测是否在滚动状态 */ setBodyRef: (element: HTMLDivElement) => void; /** * 确定的滚动状态放到上下文中 */ bodyHasScroll: boolean; /** * 滚动事件回调 */ onScrollCapture: (event: React.UIEvent) => void; } /** * 滚动 Context 实例 */ const ScrollContext = createContext(null); ScrollContext.displayName = "Scrollable"; /** * 注入到外层容器,提供滚动上下文 */ export function ScrollableTable({ table, scrollHeightFactor, onScrollBottom = () => null, ...props }: ScrollableTableProps) { const { classPrefix } = useConfig(); // 当前滚动区域是否在滚动状态 const [bodyHasScroll, setBodyHasScroll] = useState(false); // 滚动区域的 DOM 引用 const bodyRef = useRef(null); // 决定滚动高度的因子改变后,重新检测滚动状态,设置到 bodyHasScroll 中 useEffect(() => { const bodyBox = bodyRef.current; if (bodyBox) { // 滚动状态下,scrollHeight > clientHeight const nextBodyHasScroll = bodyBox.scrollHeight > bodyBox.clientHeight; if (bodyHasScroll !== nextBodyHasScroll) { setBodyHasScroll(nextBodyHasScroll); } } }, [bodyHasScroll, scrollHeightFactor]); const scrollBottom = useMemo( () => throttle(event => { onScrollBottom(event); }), [onScrollBottom] ); function handleBodyScroll(event: React.UIEvent) { const bodyBox = event.target as HTMLElement; const { scrollHeight, scrollTop, clientHeight } = bodyBox; if ( bodyHasScroll && scrollHeight <= Math.ceil(clientHeight + scrollTop + 1) ) { scrollBottom(event); } } // 滚动上下文 const scrollContext: ScrollContextValue = { // 提供给 body 使用,提供 body DOM 实例 setBodyRef: body => { bodyRef.current = body; }, // 提供给 head 使用,通过滚动状态决定是否增加右侧间距 bodyHasScroll, onScrollCapture: handleBodyScroll, }; return ( {React.cloneElement(table, { ...props, // 设计通过提供 tea-table--scrollable 类来开启滚动区域的 overflow: auto className: classNames( table.props.className, `${classPrefix}-table--scrollable` ), })} ); } interface ScrollableTableProps { table: JSX.Element; scrollHeightFactor: any[]; onScrollBottom?: (event: React.UIEvent) => void; } /** * 包装表格内容区(滚动部分),回调内容区 DOM 的实例 */ export const ScrollableTableBody = forwardRef( ( { body, style, bodyDeps, scrollToTopOnChange, ...props }: ScrollableTableBodyProps, ref: React.Ref ) => { const bodyRef = useRef(null); useEffect(() => { if (scrollToTopOnChange && bodyRef.current) { bodyRef.current.scrollTop = 0; bodyRef.current.scrollLeft = 0; } }, [scrollToTopOnChange, ...bodyDeps]); // eslint-disable-line react-hooks/exhaustive-deps const { setBodyRef, onScrollCapture } = useContext(ScrollContext); return React.cloneElement(body, { ...props, // 使用 mergeRefs 可以使得其他插件的 ref 也能被调用到 ref: mergeRefs(setBodyRef, body.ref, ref, bodyRef), style: mergeStyle(body.props.style, style), onScrollCapture: callBoth(body.props.onScrollCapture, onScrollCapture), }); } ); interface ScrollableTableBodyProps { body: React.FunctionComponentElement<{ ref: React.Ref; style: React.CSSProperties; onScrollCapture: (event: React.UIEvent) => void; }>; style: React.CSSProperties; bodyDeps: any[]; scrollToTopOnChange: boolean; } /** * 包装表格头部,通过滚动状态调整右侧间距 */ export const ScrollableTableHead = forwardRef( ( { head, ...props }: ScrollableTableHeadProps, ref: React.Ref ) => { const { bodyHasScroll } = useContext(ScrollContext); return React.cloneElement(head, { ...props, ref: mergeRefs(head.ref, ref), style: mergeStyle(head.props.style, { marginRight: bodyHasScroll ? getScrollBarSize() : null, }), }); } ); interface ScrollableTableHeadProps { head: React.FunctionComponentElement<{ ref: React.Ref; style: React.CSSProperties; }>; } ScrollableTable.displayName = "ScrollableTable";