| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102 |
- import clsx from "clsx";
- import {
- ElementType,
- UIEventHandler,
- useCallback,
- useEffect,
- useMemo,
- useRef,
- useState,
- } from "react";
- import styles from "./scroll.module.scss";
- interface VirtualScrollProps {
- component?: ElementType;
- className?: string | string[];
- itemHeight: number;
- total: number;
- tolerance?: number;
- startIndex?: number;
- getItems: (offset: number, limit: number) => JSX.Element | JSX.Element[];
- }
- export default function VirtualScroll(props: VirtualScrollProps) {
- const {
- component: Component = "ul",
- className,
- itemHeight,
- total,
- startIndex = 0,
- getItems,
- } = props;
- const [wrapHeight, setWrapHeight] = useState(0);
- const [scrollY, setScrollY] = useState(startIndex * itemHeight);
- const offset = useMemo(
- () => Math.max(0, Math.floor(scrollY / itemHeight)),
- [itemHeight, scrollY]
- );
- const limit = useMemo(() => {
- return Math.floor(wrapHeight / itemHeight) + 2;
- }, [itemHeight, wrapHeight]);
- const [wrapElement, setWrapElement] = useState<HTMLDivElement | null>(null);
- const handleScroll: UIEventHandler<HTMLDivElement> = (e) => {
- setScrollY((e.target as HTMLDivElement).scrollTop);
- };
- const resize = useCallback(() => {
- if (wrapElement) setWrapHeight(wrapElement.clientHeight);
- }, [wrapElement]);
- const setWrapRef = useCallback((ele: HTMLDivElement) => {
- if (ele) {
- setWrapElement(ele);
- // ele.scrollTo(0, scrollY);
- if (ele) setWrapHeight(ele.clientHeight);
- } else {
- setWrapElement(null);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
- useEffect(() => {
- if (window) {
- window.addEventListener("resize", resize);
- }
- return () => {
- window.removeEventListener("resize", resize);
- };
- }, [resize]);
- useEffect(() => {
- if (wrapElement) {
- const y = startIndex * itemHeight;
- setScrollY(y);
- wrapElement.scrollTo({ left: 0, top: y, behavior: "auto" });
- }
- return () => {};
- }, [itemHeight, startIndex, wrapElement]);
- return (
- <div
- ref={setWrapRef}
- className={clsx(styles["wrap"], className)}
- onScroll={handleScroll}
- >
- <div
- className={styles["space"]}
- style={{ height: `${total * itemHeight}px` }}
- />
- <Component
- className={styles["inner"]}
- style={{
- transform: `translateY(${(scrollY % itemHeight) * -1}px)`,
- }}
- >
- {getItems(offset, limit)}
- </Component>
- </div>
- );
- }
|