|
|
@@ -0,0 +1,98 @@
|
|
|
+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) + 1;
|
|
|
+ }, [itemHeight, wrapHeight]);
|
|
|
+
|
|
|
+ const [wrapElement, setWrapElement] = useState<HTMLDivElement | null>(null);
|
|
|
+
|
|
|
+ const handleScroll: UIEventHandler<HTMLDivElement> = (e) => {
|
|
|
+ setScrollY((e.target as HTMLDivElement).scrollTop);
|
|
|
+ };
|
|
|
+
|
|
|
+ const resize = useCallback(
|
|
|
+ (e: UIEvent) => {
|
|
|
+ if (wrapElement) setWrapHeight(wrapElement.clientHeight);
|
|
|
+ },
|
|
|
+ [wrapElement]
|
|
|
+ );
|
|
|
+
|
|
|
+ const setWrapRef = useCallback((ele: HTMLDivElement) => {
|
|
|
+ if (ele) {
|
|
|
+ setWrapElement(ele);
|
|
|
+ if (ele) setWrapHeight(ele.clientHeight);
|
|
|
+ } else {
|
|
|
+ setWrapElement(null);
|
|
|
+ }
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (window) {
|
|
|
+ window.addEventListener("resize", resize);
|
|
|
+ }
|
|
|
+
|
|
|
+ return () => {
|
|
|
+ window.removeEventListener("resize", resize);
|
|
|
+ };
|
|
|
+ }, [resize]);
|
|
|
+
|
|
|
+ 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>
|
|
|
+ );
|
|
|
+}
|