index.tsx 2.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  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. if (wrapElement) setWrapHeight(wrapElement.clientHeight);
  45. }, [wrapElement]);
  46. const setWrapRef = useCallback((ele: HTMLDivElement) => {
  47. if (ele) {
  48. setWrapElement(ele);
  49. ele.scrollTo(0, scrollY);
  50. if (ele) setWrapHeight(ele.clientHeight);
  51. } else {
  52. setWrapElement(null);
  53. }
  54. }, []);
  55. useEffect(() => {
  56. if (window) {
  57. window.addEventListener("resize", resize);
  58. }
  59. return () => {
  60. window.removeEventListener("resize", resize);
  61. };
  62. }, [resize]);
  63. return (
  64. <div
  65. ref={setWrapRef}
  66. className={clsx(styles["wrap"], className)}
  67. onScroll={handleScroll}
  68. >
  69. <div
  70. className={styles["space"]}
  71. style={{ height: `${total * itemHeight}px` }}
  72. />
  73. <Component
  74. className={styles["inner"]}
  75. style={{
  76. transform: `translateY(${(scrollY % itemHeight) * -1}px)`,
  77. }}
  78. >
  79. {getItems(offset, limit)}
  80. </Component>
  81. </div>
  82. );
  83. }