index.tsx 2.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import clsx from "clsx";
  2. import {
  3. ElementType,
  4. UIEventHandler,
  5. useCallback,
  6. useEffect,
  7. useMemo,
  8. useRef,
  9. useState,
  10. } from "react";
  11. import styles from "./scroll.module.scss";
  12. interface VirtualScrollProps {
  13. component?: ElementType;
  14. className?: string | string[];
  15. itemHeight: number;
  16. total: number;
  17. tolerance?: number;
  18. startIndex?: number;
  19. getItems: (offset: number, limit: number) => JSX.Element | JSX.Element[];
  20. }
  21. export default function VirtualScroll(props: VirtualScrollProps) {
  22. const {
  23. component: Component = "ul",
  24. className,
  25. itemHeight,
  26. total,
  27. startIndex = 0,
  28. getItems,
  29. } = props;
  30. const [wrapHeight, setWrapHeight] = useState(0);
  31. const [scrollY, setScrollY] = useState(startIndex * itemHeight);
  32. const offset = useMemo(
  33. () => Math.max(0, Math.floor(scrollY / itemHeight)),
  34. [itemHeight, scrollY]
  35. );
  36. const limit = useMemo(() => {
  37. return Math.floor(wrapHeight / itemHeight) + 1;
  38. }, [itemHeight, wrapHeight]);
  39. const [wrapElement, setWrapElement] = useState<HTMLDivElement | null>(null);
  40. const handleScroll: UIEventHandler<HTMLDivElement> = (e) => {
  41. setScrollY((e.target as HTMLDivElement).scrollTop);
  42. };
  43. const resize = useCallback(
  44. (e: UIEvent) => {
  45. if (wrapElement) setWrapHeight(wrapElement.clientHeight);
  46. },
  47. [wrapElement]
  48. );
  49. const setWrapRef = useCallback((ele: HTMLDivElement) => {
  50. if (ele) {
  51. setWrapElement(ele);
  52. if (ele) setWrapHeight(ele.clientHeight);
  53. } else {
  54. setWrapElement(null);
  55. }
  56. }, []);
  57. useEffect(() => {
  58. if (window) {
  59. window.addEventListener("resize", resize);
  60. }
  61. return () => {
  62. window.removeEventListener("resize", resize);
  63. };
  64. }, [resize]);
  65. return (
  66. <div
  67. ref={setWrapRef}
  68. className={clsx(styles["wrap"], className)}
  69. onScroll={handleScroll}
  70. >
  71. <div
  72. className={styles["space"]}
  73. style={{ height: `${total * itemHeight}px` }}
  74. />
  75. <Component
  76. className={styles["inner"]}
  77. style={{
  78. transform: `translateY(${(scrollY % itemHeight) * -1}px)`,
  79. }}
  80. >
  81. {getItems(offset, limit)}
  82. </Component>
  83. </div>
  84. );
  85. }