All files / src/table/addons/scrollable ScrollableTable.tsx

80% Statements 24/30
40% Branches 6/15
75% Functions 6/8
80% Lines 24/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177                                                                            3x 3x                     4x   4x     4x     4x 4x 4x   4x 4x 2x                           4x     8x             4x                                           3x                     4x 4x 2x         4x 4x                                               3x         4x 4x                               3x  
import React, {
  createContext,
  useEffect,
  useRef,
  useState,
  forwardRef,
  useContext,
} 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";
 
/**
 * 滚动 Context
 */
interface ScrollContextValue {
  /**
   * 供表格内容区设置其 DOM Ref,拿到的 DOM 实例用于检测是否在滚动状态
   */
  setBodyRef: (element: HTMLDivElement) => void;
 
  /**
   * 确定的滚动状态放到上下文中
   */
  bodyHasScroll: boolean;
 
  /**
   * 滚动事件回调
   */
  onScrollCapture: (event: React.UIEvent<HTMLElement>) => void;
}
 
/**
 * 滚动 Context 实例
 */
const ScrollContext = createContext<ScrollContextValue>(null);
ScrollContext.displayName = "Scrollable";
 
/**
 * 注入到外层容器,提供滚动上下文
 */
export function ScrollableTable({
  table,
  scrollHeightFactor,
  onScrollBottom = () => null,
  ...props
}: ScrollableTableProps) {
  const { classPrefix } = useConfig();
  // 当前滚动区域是否在滚动状态
  const [bodyHasScroll, setBodyHasScroll] = useState();
 
  // 滚动区域的 DOM 引用
  const bodyRef = useRef<HTMLDivElement>(null);
 
  // 决定滚动高度的因子改变后,重新检测滚动状态,设置到 bodyHasScroll 中
  useEffect(() => {
    const bodyBox = bodyRef.current;
    Eif (bodyBox) {
      // 滚动状态下,scrollHeight > clientHeight
      const nextBodyHasScroll = bodyBox.scrollHeight > bodyBox.clientHeight;
      if (bodyHasScroll !== nextBodyHasScroll) {
        setBodyHasScroll(nextBodyHasScroll);
      }
    }
  }, [bodyHasScroll, scrollHeightFactor]);
 
  function handleBodyScroll(event: React.UIEvent<HTMLElement>) {
    const bodyBox = event.target as HTMLElement;
    const { scrollHeight, scrollTop, clientHeight } = bodyBox;
    if (bodyHasScroll && scrollHeight <= Math.round(clientHeight + scrollTop)) {
      onScrollBottom(event);
    }
  }
 
  // 滚动上下文
  const scrollContext: ScrollContextValue = {
    // 提供给 body 使用,提供 body DOM 实例
    setBodyRef: body => {
      bodyRef.current = body;
    },
    // 提供给 head 使用,通过滚动状态决定是否增加右侧间距
    bodyHasScroll,
    onScrollCapture: handleBodyScroll,
  };
 
  return (
    <ScrollContext.Provider value={scrollContext}>
      {React.cloneElement(table, {
        ...props,
        // 设计通过提供 tea-table--scrollable 类来开启滚动区域的 overflow: auto
        className: classNames(
          table.props.className,
          `${classPrefix}-table--scrollable`
        ),
      })}
    </ScrollContext.Provider>
  );
}
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<HTMLDivElement>
  ) => {
    const bodyRef = useRef<HTMLDivElement>(null);
    useEffect(() => {
      Iif (scrollToTopOnChange && bodyRef.current) {
        bodyRef.current.scrollTo(0, 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<HTMLDivElement>;
    style: React.CSSProperties;
    onScrollCapture: (event: React.UIEvent<HTMLDivElement>) => void;
  }>;
  style: React.CSSProperties;
  bodyDeps: any[];
  scrollToTopOnChange: boolean;
}
 
/**
 * 包装表格头部,通过滚动状态调整右侧间距
 */
export const ScrollableTableHead = forwardRef(
  (
    { head, ...props }: ScrollableTableHeadProps,
    ref: React.Ref<HTMLDivElement>
  ) => {
    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<HTMLDivElement>;
    style: React.CSSProperties;
  }>;
}
 
ScrollableTable.displayName = "ScrollableTable";