import React, { useRef } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../constants/appScroll'; import { useElementClientHeightById } from '../hooks/useResizeClientHeight'; import { useCardGridMetrics } from '../hooks/useCardGridMetrics'; import { useRemeasureGridVirtualizer } from '../hooks/useRemeasureGridVirtualizer'; import type { CardGridRowHeightVariant } from '../utils/cardGridLayout'; export type VirtualCardGridProps = { items: readonly T[]; itemKey: (item: T, flatIndex: number) => string; renderItem: (item: T) => React.ReactNode; rowVariant: CardGridRowHeightVariant; disableVirtualization: boolean; /** Bumps layout when list shape changes (e.g. `items.length`). */ layoutSignal: number; wrapClassName?: string; /** Optional styles on the outer measurement wrapper (e.g. enter animation). */ wrapStyle?: React.CSSProperties; /** Defaults to `var(--space-4)`; composer grid uses `var(--space-2)`. */ gridGap?: string; }; /** * Album-/playlist-style card grids: at most six columns, proportional stretch, * optional row virtualization with scroll root `#APP_MAIN_SCROLL_VIEWPORT_ID`. */ export function VirtualCardGrid({ items, itemKey, renderItem, rowVariant, disableVirtualization, layoutSignal, wrapClassName = 'album-grid-wrap', wrapStyle, gridGap = 'var(--space-4)', }: VirtualCardGridProps): React.JSX.Element { const wrapRef = useRef(null); const { gridCols, rowHeightEst } = useCardGridMetrics(wrapRef, true, rowVariant, layoutSignal); const cols = Math.max(1, gridCols); const virtualRowCount = Math.max(0, Math.ceil(items.length / cols)); const mainScrollViewportHeight = useElementClientHeightById(APP_MAIN_SCROLL_VIEWPORT_ID); const overscan = Math.max(2, Math.ceil(mainScrollViewportHeight / Math.max(1, rowHeightEst))); const virtualizer = useVirtualizer({ count: disableVirtualization ? 0 : virtualRowCount, getScrollElement: () => document.getElementById(APP_MAIN_SCROLL_VIEWPORT_ID), estimateSize: () => rowHeightEst, overscan, }); useRemeasureGridVirtualizer(virtualizer, { active: !disableVirtualization && virtualRowCount > 0, gridCols: cols, rowHeightEst, virtualRowCount, }); if (disableVirtualization) { return (
{items.map((item, i) => ( {renderItem(item)} ))}
); } return (
{virtualizer.getVirtualItems().map(vRow => { const start = vRow.index * cols; const rowItems = items.slice(start, start + cols); return (
{rowItems.map((item, i) => ( {renderItem(item)} ))}
); })}
); }