[ENG-1384] Replace GridList with @virtual-grid/react (#1707)

replace GridList with @virtual-grid/react
This commit is contained in:
nikec
2023-10-30 16:56:03 +01:00
committed by GitHub
parent 577a9e0709
commit f3a2eefe25
5 changed files with 14 additions and 295 deletions

View File

@@ -1,3 +1,4 @@
import { Grid, useGrid } from '@virtual-grid/react';
import {
createContext,
useCallback,
@@ -11,7 +12,6 @@ import {
import Selecto from 'react-selecto';
import { useKey } from 'rooks';
import { type ExplorerItem } from '@sd/client';
import { GridList, useGridList } from '~/components';
import { useMouseNavigate, useOperatingSystem } from '~/hooks';
import { useExplorerContext } from '../Context';
@@ -111,7 +111,6 @@ export default ({ children }: { children: RenderItem }) => {
const explorer = useExplorerContext();
const settings = explorer.useSettingsSnapshot();
const explorerStore = useExplorerStore();
const explorerView = useExplorerViewContext();
const selecto = useRef<Selecto>(null);
@@ -127,17 +126,15 @@ export default ({ children }: { children: RenderItem }) => {
const padding = settings.layoutMode === 'grid' ? 12 : 0;
const grid = useGridList({
ref: explorerView.ref,
const grid = useGrid({
scrollRef: explorer.scrollRef,
count: explorer.items?.length ?? 0,
totalCount: explorer.count,
overscan: explorer.overscan,
...(settings.layoutMode === 'grid'
? { size: { width: settings.gridItemSize, height: itemHeight } }
: { columns: settings.mediaColumns }),
rowVirtualizer: { overscan: explorer.overscan ?? 5 },
onLoadMore: explorer.loadMore,
size:
settings.layoutMode === 'grid'
? { width: settings.gridItemSize, height: itemHeight }
: undefined,
columns: settings.layoutMode === 'media' ? settings.mediaColumns : undefined,
getItemId: useCallback(
(index: number) => {
const item = explorer.items?.[index];
@@ -154,8 +151,7 @@ export default ({ children }: { children: RenderItem }) => {
x: padding,
y: padding
},
gap: explorerView.gap || (settings.layoutMode === 'grid' ? settings.gridGap : undefined),
top: explorerView.top
gap: explorerView.gap || (settings.layoutMode === 'grid' ? settings.gridGap : undefined)
});
function getElementId(element: Element) {
@@ -552,7 +548,7 @@ export default ({ children }: { children: RenderItem }) => {
let itemsInDragCount =
(dragHeight - grid.gap.y) /
(grid.virtualItemHeight + grid.gap.y);
(grid.virtualItemSize.height + grid.gap.y);
if (itemsInDragCount > 1) {
itemsInDragCount = Math.ceil(itemsInDragCount);
@@ -619,7 +615,7 @@ export default ({ children }: { children: RenderItem }) => {
/>
)}
<GridList grid={grid} scrollRef={explorer.scrollRef}>
<Grid grid={grid}>
{(index) => {
const item = explorer.items?.[index];
if (!item) return null;
@@ -653,7 +649,7 @@ export default ({ children }: { children: RenderItem }) => {
</GridListItem>
);
}}
</GridList>
</Grid>
</SelectoContext.Provider>
);
};

View File

@@ -1,277 +0,0 @@
import { useVirtualizer } from '@tanstack/react-virtual';
import React, {
ReactNode,
RefObject,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState
} from 'react';
import { useMutationObserver } from 'rooks';
import useResizeObserver from 'use-resize-observer';
import { ExplorerViewPadding } from '~/app/$libraryId/Explorer/View';
import { useExplorerViewPadding } from '~/app/$libraryId/Explorer/View/util';
type ItemData = any | undefined;
type ItemId = number | string;
export interface GridListItem<IdT extends ItemId = number, DataT extends ItemData = undefined> {
index: number;
id: IdT;
row: number;
column: number;
rect: Omit<DOMRect, 'toJSON'>;
data: DataT;
}
export interface UseGridListProps<IdT extends ItemId = number, DataT extends ItemData = undefined> {
count: number;
totalCount?: number;
ref: RefObject<HTMLElement>;
padding?: number | ExplorerViewPadding;
gap?: number | { x?: number; y?: number };
overscan?: number;
top?: number;
onLoadMore?: () => void;
getItemId?: (index: number) => IdT | undefined;
getItemData?: (index: number) => DataT;
size?: number | { width: number; height: number };
columns?: number;
}
export const useGridList = <IdT extends ItemId = number, DataT extends ItemData = undefined>({
padding,
gap,
size,
columns,
ref,
getItemId,
getItemData,
...props
}: UseGridListProps<IdT, DataT>) => {
const { width } = useResizeObserver({ ref });
const count = !props.totalCount ? props.count : Math.max(props.count, props.totalCount);
const gridPadding = useExplorerViewPadding(padding);
const paddingTop = gridPadding.top ?? 0;
const paddingBottom = gridPadding.bottom ?? 0;
const paddingLeft = gridPadding.left ?? 0;
const paddingRight = gridPadding.right ?? 0;
const gapX = (typeof gap === 'object' ? gap.x : gap) || 0;
const gapY = (typeof gap === 'object' ? gap.y : gap) || 0;
const itemWidth = size ? (typeof size === 'object' ? size.width : size) : undefined;
const itemHeight = size ? (typeof size === 'object' ? size.height : size) : undefined;
const gridWidth = width ? width - (paddingLeft + paddingRight) : 0;
let columnCount = columns || 0;
if (!columns && itemWidth) {
let columns = Math.floor(gridWidth / itemWidth);
if (gapX) columns = Math.floor((gridWidth - (columns - 1) * gapX) / itemWidth);
columnCount = columns;
}
const rowCount = columnCount > 0 ? Math.ceil(props.count / columnCount) : 0;
const totalRowCount = columnCount > 0 ? Math.ceil(count / columnCount) : 0;
const virtualItemWidth =
columnCount > 0 ? (gridWidth - (columnCount - 1) * gapX) / columnCount : 0;
const virtualItemHeight = itemHeight || virtualItemWidth;
const getItem = useCallback(
(index: number) => {
if (index < 0 || index >= count) return;
const id = getItemId?.(index) || index;
const data = getItemData?.(index) as DataT;
const column = index % columnCount;
const row = Math.floor(index / columnCount);
const x = paddingLeft + (column !== 0 ? gapX : 0) * column + virtualItemWidth * column;
const y = paddingTop + (row !== 0 ? gapY : 0) * row + virtualItemHeight * row;
const item: GridListItem<typeof id, DataT> = {
index,
id,
data,
row,
column,
rect: {
height: virtualItemHeight,
width: virtualItemWidth,
x,
y,
top: y,
bottom: y + virtualItemHeight,
left: x,
right: x + virtualItemWidth
}
};
return item;
},
[
columnCount,
count,
gapX,
gapY,
getItemId,
getItemData,
paddingLeft,
paddingTop,
virtualItemHeight,
virtualItemWidth
]
);
return {
columnCount,
rowCount,
totalRowCount,
width: gridWidth,
padding: { top: paddingTop, bottom: paddingBottom, left: paddingLeft, right: paddingRight },
gap: { x: gapX, y: gapY },
itemHeight,
itemWidth,
virtualItemHeight,
virtualItemWidth,
getItem,
...props
};
};
export interface GridListProps {
grid: ReturnType<typeof useGridList>;
scrollRef: RefObject<HTMLElement>;
children: (index: number) => ReactNode;
}
export const GridList = ({ grid, children, scrollRef }: GridListProps) => {
const ref = useRef<HTMLDivElement>(null);
const [listOffset, setListOffset] = useState(0);
const getHeight = useCallback(
(index: number) => grid.virtualItemHeight + (index !== 0 ? grid.gap.y : 0),
[grid.virtualItemHeight, grid.gap.y]
);
const getWidth = useCallback(
(index: number) => grid.virtualItemWidth + (index !== 0 ? grid.gap.x : 0),
[grid.virtualItemWidth, grid.gap.x]
);
const rowVirtualizer = useVirtualizer({
count: grid.totalRowCount,
getScrollElement: () => scrollRef.current,
estimateSize: getHeight,
paddingStart: grid.padding.top,
paddingEnd: grid.padding.bottom,
overscan: grid.overscan ?? 5,
scrollMargin: listOffset
});
const columnVirtualizer = useVirtualizer({
horizontal: true,
count: grid.columnCount,
getScrollElement: () => scrollRef.current,
estimateSize: getWidth,
paddingStart: grid.padding.left,
paddingEnd: grid.padding.right
});
const virtualRows = rowVirtualizer.getVirtualItems();
const virtualColumns = columnVirtualizer.getVirtualItems();
// Measure virtual item on size change
useEffect(() => {
rowVirtualizer.measure();
columnVirtualizer.measure();
}, [rowVirtualizer, columnVirtualizer, grid.virtualItemWidth, grid.virtualItemHeight]);
// Force recalculate range
// https://github.com/TanStack/virtual/issues/485
useMemo(() => {
rowVirtualizer.calculateRange();
columnVirtualizer.calculateRange();
return null;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [rowVirtualizer, columnVirtualizer, grid.columnCount, grid.rowCount]);
useEffect(() => {
if (!grid.onLoadMore) return;
const lastRow = virtualRows[virtualRows.length - 1];
if (!lastRow) return;
const loadMoreFromRow = Math.ceil(grid.rowCount * 0.75);
if (lastRow.index >= loadMoreFromRow - 1) grid.onLoadMore();
}, [virtualRows, grid.rowCount, grid.onLoadMore, grid]);
useMutationObserver(scrollRef, () => setListOffset(ref.current?.offsetTop ?? 0));
useLayoutEffect(() => setListOffset(ref.current?.offsetTop ?? 0), []);
return (
<div
ref={ref}
className="relative w-full overflow-x-hidden"
style={{
height: `${rowVirtualizer.getTotalSize()}px`
}}
>
{grid.width > 0 &&
virtualRows.map((virtualRow) => (
<React.Fragment key={virtualRow.index}>
{virtualColumns.map((virtualColumn) => {
const index = virtualRow.index * grid.columnCount + virtualColumn.index;
if (index >= grid.count) return null;
return (
<div
key={virtualColumn.index}
style={{
position: 'absolute',
top: 0,
left: 0,
width: `${virtualColumn.size}px`,
height: `${virtualRow.size}px`,
transform: `translateX(${
virtualColumn.start
}px) translateY(${
virtualRow.start - rowVirtualizer.options.scrollMargin
}px)`,
paddingLeft: virtualColumn.index !== 0 ? grid.gap.x : 0,
paddingTop: virtualRow.index !== 0 ? grid.gap.y : 0
}}
>
<div
className="m-auto"
style={{
width: grid.itemWidth || '100%',
height: grid.itemHeight || '100%'
}}
>
{children(index)}
</div>
</div>
);
})}
</React.Fragment>
))}
</div>
);
};

View File

@@ -4,7 +4,6 @@ export * from './ColorPicker';
export * from './DismissibleNotice';
export * from './DragRegion';
export * from './Folder';
export * from './GridList';
export * from './Icon';
export * from './Loader';
export * from './PDFViewer';

View File

@@ -22,6 +22,7 @@
"@tanstack/react-query-devtools": "^4.36.1",
"@tanstack/react-table": "^8.10.7",
"@tanstack/react-virtual": "3.0.0-beta.66",
"@virtual-grid/react": "^1.0.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"crypto-random-string": "^5.0.0",
@@ -53,14 +54,14 @@
"valtio": "^1.11.2"
},
"devDependencies": {
"tailwindcss": "^3.3.3",
"type-fest": "^4.5.0",
"@sd/config": "workspace:*",
"@types/node": "~18.17.19",
"@types/react": "^18.2.31",
"@types/react-dom": "^18.2.14",
"@types/uuid": "^9.0.6",
"@vitejs/plugin-react": "^4.1.0",
"tailwindcss": "^3.3.3",
"type-fest": "^4.5.0",
"typescript": "^5.2.2",
"vite": "^4.5.0",
"vite-plugin-svgr": "^3.3.0"

BIN
pnpm-lock.yaml generated
View File

Binary file not shown.