[feature-107] create generic component for virtual scrolling

This commit is contained in:
MathieuG-P
2024-05-28 22:10:02 +02:00
parent 0196206ec1
commit 7562b07832
10 changed files with 178 additions and 177 deletions
@@ -0,0 +1,24 @@
import equal from "fast-deep-equal";
import { CSSProperties, memo } from "react";
import { cn } from "renderer/helpers/css-class.helpers";
type Props<T> = {
className?: string;
style?: CSSProperties;
items: T[];
renderItem: (item: T) => JSX.Element;
}
function VirtualRowComponent<T>({ className, style, items, renderItem }: Props<T>) {
return (
<ul className={cn("h-fit w-full flex flex-nowrap", className)} style={style}>
{items.map((item) => (
renderItem(item)
))}
</ul>
);
}
const typedMemo: <T, P>(c: T, propsAreEqual?: (prevProps: Readonly<P>, nextProps: Readonly<P>) => boolean) => T = memo;
export const VirtualRow = typedMemo(VirtualRowComponent, equal);
@@ -0,0 +1,71 @@
import { useLayoutEffect, useRef, useState } from "react";
import { VariableSizeList } from "react-window";
import { cn } from "renderer/helpers/css-class.helpers";
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
import { VirtualRow } from "./virtual-row.component";
import { splitIntoChunk } from "shared/helpers/array.helpers";
import { useConstant } from "renderer/hooks/use-constant.hook";
import { BehaviorSubject, debounceTime } from "rxjs";
import { useObservable } from "renderer/hooks/use-observable.hook";
type ClassNames = {
mainDiv?: string;
variableList?: string;
rows?: string;
}
type Props<T = unknown> = {
className?: string;
classNames?: ClassNames;
minItemWidth: number;
maxColumns: number;
minColumns?: number;
itemHeight: number;
items: T[];
renderItem: (item: T) => JSX.Element;
}
export function VirtualScroll<T = unknown>({ className, classNames, minItemWidth, maxColumns, minColumns, itemHeight, items, renderItem}: Props<T>) {
console.log("omg les classes", className);
const ref = useRef(null);
const [itemPerRow, setItemPerRow] = useState(1);
const [itemsToRender, setItemsToRender] = useState<T[][]>([]);
const listHeight$ = useConstant(() => new BehaviorSubject<number>(0));
const listHeight = useObservable(() => listHeight$.pipe(debounceTime(100)), 0);
useLayoutEffect(() => {
const updateItemPerRow = (listWidth: number) => {
if (!listWidth) return;
const calculatedColumns = Math.floor(listWidth / minItemWidth);
const newColumns = Math.max((minColumns || 1), Math.min(maxColumns, calculatedColumns));
setItemPerRow(() => newColumns);
};
const observer = new ResizeObserver(() => {
updateItemPerRow(ref.current?.clientWidth || 0);
listHeight$.next(ref.current?.clientHeight || 0);
console.log("list height", ref.current?.clientHeight);
});
observer.observe(ref.current);
return () => observer.disconnect();
}, [minItemWidth, maxColumns]);
useOnUpdate(() => {
const splitedItems = splitIntoChunk(items, itemPerRow);
setItemsToRender(() => splitedItems);
}, [itemPerRow, items])
return (
<div ref={ref} className={cn(className, classNames?.mainDiv)}>
<VariableSizeList className={cn("scrollbar-default", classNames?.variableList)} width="100%" height={listHeight} layout="vertical" itemCount={itemsToRender.length} itemKey={i => i} itemSize={() => itemHeight} itemData={itemsToRender} style={{ scrollbarGutter: "stable both-edges" }}>
{props => <VirtualRow key={props.index} items={props.data[props.index]} renderItem={renderItem} className={classNames?.rows} style={props.style}/>}
</VariableSizeList>
</div>
)
}