"use client" import { memo, ReactNode, useCallback, useEffect, useMemo, useState, } from "react" import { useDataGrid } from "@/components/reui/data-grid/data-grid" import { DataGridTableBase, DataGridTableBody, DataGridTableEmpty, DataGridTableFoot, DataGridTableHead, DataGridTableHeadRow, DataGridTableHeadRowCell, DataGridTableHeadRowCellResize, DataGridTableRenderedRow, DataGridTableRowSpacer, DataGridTableViewport, getDataGridTableRowSections, } from "@/components/reui/data-grid/data-grid-table" import { flexRender, HeaderGroup, Row, Table } from "@tanstack/react-table" import { useVirtualizer, VirtualItem, Virtualizer, VirtualizerOptions, } from "@tanstack/react-virtual" import { cn } from "@/lib/utils" import { Spinner } from "@/components/ui/spinner" type DataGridTableVirtualScrollElements = { containerElement: HTMLDivElement | null scrollElement: HTMLElement | null } type DataGridTableVirtualizerInstance = Virtualizer< HTMLElement, HTMLTableRowElement > type DataGridTableVirtualizerOptions = Omit< VirtualizerOptions, "count" | "estimateSize" | "getItemKey" | "getScrollElement" > & { estimateSize?: (index: number, row: Row) => number getItemKey?: (index: number, row: Row) => string | number getScrollElement?: ( elements: DataGridTableVirtualScrollElements ) => HTMLElement | null } interface DataGridTableVirtualProps { height?: number | string estimateSize?: number overscan?: number footerContent?: ReactNode renderHeader?: boolean onFetchMore?: () => void isFetchingMore?: boolean hasMore?: boolean fetchMoreOffset?: number virtualizerOptions?: DataGridTableVirtualizerOptions } interface VirtualBodyProps { table: Table columnCount: number topRows: Row[] centerRows: Row[] bottomRows: Row[] virtualItems: VirtualItem[] totalSize: number isVirtualizationEnabled: boolean isInfiniteMode: boolean isFetchingMore: boolean hasMore?: boolean loadingMoreMessage: ReactNode allRowsLoadedMessage: ReactNode measureRowRef?: (element: HTMLTableRowElement | null) => void } function DataGridTableVirtualSpacer({ columnCount, height, }: { columnCount: number height: number }) { if (height <= 0) return null return ( ) } function DataGridTableVirtualStatusRow({ children, className, columnCount, }: { children: ReactNode className?: string columnCount: number }) { return ( {children} ) } function DataGridTableVirtualBody({ table, columnCount, topRows, centerRows, bottomRows, virtualItems, totalSize, isVirtualizationEnabled, isInfiniteMode, isFetchingMore, hasMore, loadingMoreMessage, allRowsLoadedMessage, measureRowRef, }: VirtualBodyProps) { const totalRows = topRows.length + centerRows.length + bottomRows.length if (!totalRows) return const hasCenterRows = centerRows.length > 0 const showFetchingRow = isInfiniteMode && isFetchingMore const showCompleteRow = isInfiniteMode && hasMore === false && totalRows > 0 const hasMiddleSection = hasCenterRows || showFetchingRow || showCompleteRow const leadingSpacerHeight = isVirtualizationEnabled && hasCenterRows && virtualItems.length > 0 ? (virtualItems[0]?.start ?? 0) : 0 const trailingSpacerHeight = isVirtualizationEnabled && hasCenterRows && virtualItems.length > 0 ? Math.max( 0, totalSize - (virtualItems[virtualItems.length - 1]?.end ?? 0) ) : 0 const renderedRows: ReactNode[] = [] topRows.forEach((row, index) => { renderedRows.push( ) }) if (isVirtualizationEnabled) { if (leadingSpacerHeight > 0) { renderedRows.push( ) } virtualItems.forEach((virtualRow) => { const row = centerRows[virtualRow.index] if (!row) return renderedRows.push( ) }) if (trailingSpacerHeight > 0) { renderedRows.push( ) } } else { centerRows.forEach((row) => { renderedRows.push() }) } if (showFetchingRow) { renderedRows.push(
{loadingMoreMessage}
) } if (showCompleteRow) { renderedRows.push( {allRowsLoadedMessage} ) } bottomRows.forEach((row, index) => { renderedRows.push( 0 || hasMiddleSection) ? "bottom" : undefined } /> ) }) return <>{renderedRows} } /** * Memoized virtual body: skip re-renders during active column resize. * Column widths update via CSS variables on the element, * so the browser handles width changes without React re-renders. */ const MemoizedVirtualBody = memo( DataGridTableVirtualBody, (_prev, next) => !!next.table.getState().columnSizingInfo.isResizingColumn ) as typeof DataGridTableVirtualBody function DataGridTableVirtual({ height, estimateSize = 48, overscan = 10, footerContent, renderHeader = true, onFetchMore, isFetchingMore = false, hasMore, fetchMoreOffset = 0, virtualizerOptions, }: DataGridTableVirtualProps) { const { table, props } = useDataGrid() const { topRows, centerRows, bottomRows } = getDataGridTableRowSections( table, props.tableLayout?.rowsPinnable ) const columnCount = table.getVisibleFlatColumns().length + (props.tableLayout?.columnsResizable ? 1 : 0) const isInfiniteMode = typeof onFetchMore === "function" const [viewportElements, setViewportElements] = useState({ containerElement: null, scrollElement: null, }) const { estimateSize: customEstimateSize, getItemKey: customGetItemKey, getScrollElement: customGetScrollElement, measureElement: customMeasureElement, overscan: customOverscan, ...virtualizerOptionsRest } = virtualizerOptions ?? {} const isVirtualizationEnabled = virtualizerOptions?.enabled !== false const loadingMoreMessage = props.fetchingMoreMessage || props.loadingMessage || "Loading..." const allRowsLoadedMessage = props.allRowsLoadedMessage || "All records loaded" const handleViewportRef = useCallback((node: HTMLDivElement | null) => { setViewportElements({ containerElement: node, scrollElement: (node?.closest( '[data-slot="scroll-area-viewport"]' ) as HTMLElement | null) ?? node, }) }, []) const usesExternalScrollArea = viewportElements.scrollElement !== null && viewportElements.scrollElement !== viewportElements.containerElement const resolveScrollElement = useCallback(() => { if (customGetScrollElement) { return customGetScrollElement(viewportElements) } return viewportElements.scrollElement }, [customGetScrollElement, viewportElements]) const resolveItemKey = useCallback( (index: number) => { const row = centerRows[index] if (!row) return index return customGetItemKey?.(index, row) ?? row.id ?? index }, [centerRows, customGetItemKey] ) const resolveEstimateSize = useCallback( (index: number) => { const row = centerRows[index] return row ? (customEstimateSize?.(index, row) ?? estimateSize) : estimateSize }, [centerRows, customEstimateSize, estimateSize] ) const virtualizer = useVirtualizer({ count: centerRows.length, getScrollElement: resolveScrollElement, getItemKey: resolveItemKey, estimateSize: resolveEstimateSize, overscan: customOverscan ?? overscan, measureElement: customMeasureElement, ...virtualizerOptionsRest, }) as DataGridTableVirtualizerInstance const virtualItems = isVirtualizationEnabled ? virtualizer.getVirtualItems() : [] const totalSize = isVirtualizationEnabled ? virtualizer.getTotalSize() : 0 const measureRowRef = isVirtualizationEnabled && customMeasureElement ? virtualizer.measureElement : undefined const resolvedFetchMoreOffset = useMemo( () => Math.max(0, fetchMoreOffset), [fetchMoreOffset] ) useEffect(() => { if ( !isVirtualizationEnabled || !isInfiniteMode || hasMore === false || isFetchingMore ) { return } const lastItem = virtualItems[virtualItems.length - 1] if (!lastItem) return if (lastItem.index >= centerRows.length - 1 - resolvedFetchMoreOffset) { onFetchMore?.() } }, [ centerRows.length, hasMore, isFetchingMore, isInfiniteMode, isVirtualizationEnabled, onFetchMore, resolvedFetchMoreOffset, virtualItems, ]) return ( {renderHeader && ( {table .getHeaderGroups() .map((headerGroup: HeaderGroup, index) => ( {headerGroup.headers.map((header, hIndex) => { const { column } = header return ( {header.isPlaceholder ? null : props.tableLayout ?.columnsResizable && column.getCanResize() ? (
{flexRender( header.column.columnDef.header, header.getContext() )}
) : ( flexRender( header.column.columnDef.header, header.getContext() ) )} {props.tableLayout?.columnsResizable && column.getCanResize() && ( )}
) })}
))}
)} {renderHeader && (props.tableLayout?.stripped || !props.tableLayout?.rowBorder) && ( )} {footerContent && ( {footerContent} )}
) } export { DataGridTableVirtual } export type { DataGridTableVirtualProps, DataGridTableVirtualScrollElements, DataGridTableVirtualizerOptions, }