index.tsx 2.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  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. // eslint-disable-next-line react-hooks/exhaustive-deps
  55. }, []);
  56. useEffect(() => {
  57. if (window) {
  58. window.addEventListener("resize", resize);
  59. }
  60. return () => {
  61. window.removeEventListener("resize", resize);
  62. };
  63. }, [resize]);
  64. return (
  65. <div
  66. ref={setWrapRef}
  67. className={clsx(styles["wrap"], className)}
  68. onScroll={handleScroll}
  69. >
  70. <div
  71. className={styles["space"]}
  72. style={{ height: `${total * itemHeight}px` }}
  73. />
  74. <Component
  75. className={styles["inner"]}
  76. style={{
  77. transform: `translateY(${(scrollY % itemHeight) * -1}px)`,
  78. }}
  79. >
  80. {getItems(offset, limit)}
  81. </Component>
  82. </div>
  83. );
  84. }