index.tsx 2.4 KB

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