{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"data-grid-table-virtual","type":"registry:ui","title":"Data Grid Table Virtual","description":"","dependencies":["@tanstack/react-table","@tanstack/react-virtual"],"registryDependencies":["@neui/data-grid","@neui/data-grid-table","spinner"],"files":[{"path":"data-grid-table-virtual.tsx","type":"registry:ui","content":"\"use client\"\n\"use no memo\"\n\nimport { memo, useCallback, useEffect, useRef, useState } from \"react\"\nimport type { CSSProperties, ReactNode } from \"react\"\nimport { useDataGrid } from \"@/components/neui/data-grid/data-grid\"\nimport {\n DataGridTableBase,\n DataGridTableBody,\n DataGridTableEmpty,\n DataGridTableFillBodyCell,\n DataGridTableFillHeadCell,\n DataGridTableFoot,\n DataGridTableHead,\n DataGridTableHeadRow,\n DataGridTableHeadRowCell,\n DataGridTableHeadRowCellResize,\n DataGridTableRenderedRow,\n DataGridTableRowSpacer,\n DataGridTableViewport,\n getDataGridScrollAreaViewport,\n getDataGridTableMergedHeaderGroups,\n getDataGridTableRowSections,\n getPinningStyles,\n hasDataGridTableRightPinnedColumns,\n} from \"@/components/neui/data-grid/data-grid-table\"\nimport { flexRender } from \"@tanstack/react-table\"\nimport type { Column, Row, Table } from \"@tanstack/react-table\"\nimport { useVirtualizer } from \"@tanstack/react-virtual\"\nimport type {\n VirtualItem,\n Virtualizer,\n VirtualizerOptions,\n} from \"@tanstack/react-virtual\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Spinner } from \"@/components/ui/spinner\"\n\ntype DataGridTableVirtualScrollElements = {\n containerElement: HTMLDivElement | null\n scrollElement: HTMLElement | null\n}\n\ntype DataGridTableVirtualizerInstance = Virtualizer<\n HTMLElement,\n HTMLTableRowElement\n>\n\ntype DataGridTableVirtualScrollAlignment = \"auto\" | \"center\" | \"start\" | \"end\"\n\ntype DataGridTableVirtualScrollRequest = {\n align: DataGridTableVirtualScrollAlignment\n behavior: ScrollBehavior\n containerElement: HTMLDivElement\n headerSticky: boolean\n isVirtualizationEnabled: boolean\n rowId: string | undefined\n rowIndex: number\n scrollElement: HTMLElement\n}\n\nfunction isSameDataGridTableScrollRequest(\n previous: DataGridTableVirtualScrollRequest | null,\n next: DataGridTableVirtualScrollRequest\n) {\n return (\n previous?.align === next.align &&\n previous.behavior === next.behavior &&\n previous.containerElement === next.containerElement &&\n previous.headerSticky === next.headerSticky &&\n previous.isVirtualizationEnabled === next.isVirtualizationEnabled &&\n previous.rowId === next.rowId &&\n previous.rowIndex === next.rowIndex &&\n previous.scrollElement === next.scrollElement\n )\n}\n\nfunction getDataGridTableScrollTarget({\n align,\n clientHeight,\n rowBottom,\n rowHeight,\n rowTop,\n scrollHeight,\n scrollTop,\n viewportTopOffset = 0,\n}: {\n align: DataGridTableVirtualScrollAlignment\n clientHeight: number\n rowBottom: number\n rowHeight: number\n rowTop: number\n scrollHeight: number\n scrollTop: number\n viewportTopOffset?: number\n}) {\n const visibleHeight = Math.max(0, clientHeight - viewportTopOffset)\n const viewportTop = scrollTop + viewportTopOffset\n const viewportBottom = scrollTop + clientHeight\n\n const targetTop =\n align === \"auto\"\n ? rowTop < viewportTop\n ? rowTop - viewportTopOffset\n : rowBottom > viewportBottom\n ? rowBottom - clientHeight\n : null\n : align === \"start\"\n ? rowTop - viewportTopOffset\n : align === \"end\"\n ? rowBottom - clientHeight\n : rowTop -\n viewportTopOffset -\n Math.max(0, (visibleHeight - rowHeight) / 2)\n\n if (targetTop === null) return null\n\n return Math.min(\n Math.max(0, targetTop),\n Math.max(0, scrollHeight - clientHeight)\n )\n}\n\nfunction getDataGridTableHeaderOffset({\n containerElement,\n headerSticky,\n scrollElement,\n}: {\n containerElement: HTMLDivElement\n headerSticky: boolean\n scrollElement: HTMLElement\n}) {\n if (!headerSticky) return 0\n\n const headerElement = containerElement.querySelector(\n ':scope > [data-slot=\"data-grid-table\"] > thead'\n )\n\n if (!headerElement) return 0\n\n const scrollRect = scrollElement.getBoundingClientRect()\n const headerRect = headerElement.getBoundingClientRect()\n const headerBottomOffset = headerRect.bottom - scrollRect.top\n const overlapsViewportTop =\n headerRect.top <= scrollRect.top + 0.5 && headerBottomOffset > 0\n\n if (!overlapsViewportTop) return 0\n\n return Math.min(scrollElement.clientHeight, Math.max(0, headerBottomOffset))\n}\n\nfunction scrollDataGridTableToOffset({\n behavior,\n scrollElement,\n targetTop,\n virtualizer,\n}: {\n behavior: ScrollBehavior\n scrollElement: HTMLElement\n targetTop: number\n virtualizer?: DataGridTableVirtualizerInstance\n}) {\n if (virtualizer) {\n virtualizer.scrollToOffset(targetTop, { align: \"start\", behavior })\n } else if (typeof scrollElement.scrollTo === \"function\") {\n scrollElement.scrollTo({ behavior, top: targetTop })\n } else {\n scrollElement.scrollTop = targetTop\n }\n}\n\nfunction scrollDataGridTableRowIntoView({\n align,\n behavior,\n cancelPendingScroll = false,\n containerElement,\n headerSticky,\n rowIndex,\n scrollElement,\n virtualizer,\n}: {\n align: DataGridTableVirtualScrollAlignment\n behavior: ScrollBehavior\n cancelPendingScroll?: boolean\n containerElement: HTMLDivElement | null\n headerSticky: boolean\n rowIndex: number\n scrollElement: HTMLElement | null\n virtualizer?: DataGridTableVirtualizerInstance\n}) {\n if (!containerElement || !scrollElement) return false\n\n const rowElement = containerElement.querySelector(\n `:scope > [data-slot=\"data-grid-table\"] > tbody > tr[data-index=\"${rowIndex}\"]`\n )\n\n if (!rowElement) return false\n\n const scrollRect = scrollElement.getBoundingClientRect()\n const rowRect = rowElement.getBoundingClientRect()\n const viewportTopOffset = getDataGridTableHeaderOffset({\n containerElement,\n headerSticky,\n scrollElement,\n })\n const rowTop = scrollElement.scrollTop + rowRect.top - scrollRect.top\n const rowBottom = scrollElement.scrollTop + rowRect.bottom - scrollRect.top\n const targetTop = getDataGridTableScrollTarget({\n align,\n clientHeight: scrollElement.clientHeight,\n rowBottom,\n rowHeight: rowRect.height || rowElement.offsetHeight,\n rowTop,\n scrollHeight: scrollElement.scrollHeight,\n scrollTop: scrollElement.scrollTop,\n viewportTopOffset,\n })\n\n if (\n targetTop === null ||\n Math.abs(targetTop - scrollElement.scrollTop) < 0.5\n ) {\n if (cancelPendingScroll) {\n scrollDataGridTableToOffset({\n behavior: \"auto\",\n scrollElement,\n targetTop: scrollElement.scrollTop,\n virtualizer,\n })\n }\n\n return true\n }\n\n scrollDataGridTableToOffset({\n behavior,\n scrollElement,\n targetTop,\n virtualizer,\n })\n\n return true\n}\n\ntype DataGridTableVirtualizerOptions = Omit<\n VirtualizerOptions,\n \"count\" | \"estimateSize\" | \"getItemKey\" | \"getScrollElement\"\n> & {\n estimateSize?: (index: number, row: Row) => number\n getItemKey?: (index: number, row: Row) => string | number\n getScrollElement?: (\n elements: DataGridTableVirtualScrollElements\n ) => HTMLElement | null\n}\n\ninterface DataGridTableVirtualProps {\n height?: number | string\n estimateSize?: number\n overscan?: number\n /** Scroll animation used when revealing a controlled target row. */\n scrollBehavior?: ScrollBehavior\n /** Alignment used when revealing a controlled target row. Defaults to auto. */\n scrollToRowAlign?: DataGridTableVirtualScrollAlignment\n /** Index within the center (non-pinned) row section to reveal. */\n scrollToRowIndex?: number\n footerContent?: ReactNode\n renderHeader?: boolean\n onFetchMore?: () => void\n isFetchingMore?: boolean\n hasMore?: boolean\n fetchMoreOffset?: number\n virtualizerOptions?: DataGridTableVirtualizerOptions\n}\n\ninterface VirtualBodyProps {\n table: Table\n topRows: Row[]\n centerRows: Row[]\n bottomRows: Row[]\n virtualItems: VirtualItem[]\n totalSize: number\n isVirtualizationEnabled: boolean\n isInfiniteMode: boolean\n isFetchingMore: boolean\n hasMore?: boolean\n loadingMoreMessage: ReactNode\n allRowsLoadedMessage: ReactNode\n measureRowRef?: (element: HTMLTableRowElement | null) => void\n}\n\nfunction DataGridTableVirtualPinnedPlaceholderCell({\n column,\n}: {\n column: Column\n}) {\n const { props } = useDataGrid()\n const isPinned = column.getIsPinned()\n const isLastLeftPinned = isPinned === \"left\" && column.getIsLastColumn(\"left\")\n const isFirstRightPinned =\n isPinned === \"right\" && column.getIsFirstColumn(\"right\")\n\n return (\n \n )\n}\n\nfunction DataGridTableVirtualUtilityRow({\n table,\n children,\n centerCellClassName,\n centerCellStyle,\n rowClassName,\n ariaHidden,\n}: {\n table: Table\n children: ReactNode\n centerCellClassName?: string\n centerCellStyle?: CSSProperties\n rowClassName?: string\n ariaHidden?: boolean\n}) {\n const { props } = useDataGrid()\n const leftVisibleColumns = table.getLeftVisibleLeafColumns()\n const centerVisibleColumns = table.getCenterVisibleLeafColumns()\n const rightVisibleColumns = table.getRightVisibleLeafColumns()\n const hasRightPinnedColumns = hasDataGridTableRightPinnedColumns(table)\n\n return (\n \n {leftVisibleColumns.map((column) => (\n \n ))}\n \n {children}\n \n {props.tableLayout?.columnsResizable && hasRightPinnedColumns ? (\n \n ) : null}\n {rightVisibleColumns.map((column) => (\n \n ))}\n {props.tableLayout?.columnsResizable && !hasRightPinnedColumns ? (\n \n ) : null}\n \n )\n}\n\nfunction DataGridTableVirtualSpacer({\n table,\n height,\n}: {\n table: Table\n height: number\n}) {\n if (height <= 0) return null\n\n return (\n \n {null}\n \n )\n}\n\nfunction DataGridTableVirtualStatusRow({\n table,\n children,\n className,\n}: {\n table: Table\n children: ReactNode\n className?: string\n}) {\n return (\n \n {children}\n \n )\n}\n\nfunction DataGridTableVirtualBody({\n table,\n topRows,\n centerRows,\n bottomRows,\n virtualItems,\n totalSize,\n isVirtualizationEnabled,\n isInfiniteMode,\n isFetchingMore,\n hasMore,\n loadingMoreMessage,\n allRowsLoadedMessage,\n measureRowRef,\n}: VirtualBodyProps) {\n const { isLoading } = useDataGrid()\n const totalRows = topRows.length + centerRows.length + bottomRows.length\n\n if (!totalRows) {\n // Initial load must not flash the empty state as if the query returned\n // nothing.\n if (isLoading) {\n return (\n \n
\n \n {loadingMoreMessage}\n
\n
\n )\n }\n\n return \n }\n\n const hasCenterRows = centerRows.length > 0\n const showFetchingRow = isInfiniteMode && isFetchingMore\n const showCompleteRow = isInfiniteMode && hasMore === false && totalRows > 0\n const hasMiddleSection = hasCenterRows || showFetchingRow || showCompleteRow\n const leadingSpacerHeight =\n isVirtualizationEnabled && hasCenterRows && virtualItems.length > 0\n ? (virtualItems[0]?.start ?? 0)\n : 0\n const trailingSpacerHeight =\n isVirtualizationEnabled && hasCenterRows && virtualItems.length > 0\n ? Math.max(\n 0,\n totalSize - (virtualItems[virtualItems.length - 1]?.end ?? 0)\n )\n : 0\n\n const renderedRows: ReactNode[] = []\n\n topRows.forEach((row, index) => {\n renderedRows.push(\n \n )\n })\n\n if (isVirtualizationEnabled) {\n if (leadingSpacerHeight > 0) {\n renderedRows.push(\n \n )\n }\n\n virtualItems.forEach((virtualRow) => {\n const row = centerRows[virtualRow.index]\n\n if (!row) return\n\n renderedRows.push(\n \n )\n })\n\n if (trailingSpacerHeight > 0) {\n renderedRows.push(\n \n )\n }\n } else {\n centerRows.forEach((row, rowIndex) => {\n renderedRows.push(\n \n )\n })\n }\n\n if (showFetchingRow) {\n renderedRows.push(\n \n
\n \n {loadingMoreMessage}\n
\n
\n )\n }\n\n if (showCompleteRow) {\n renderedRows.push(\n \n {allRowsLoadedMessage}\n \n )\n }\n\n bottomRows.forEach((row, index) => {\n renderedRows.push(\n 0 || hasMiddleSection)\n ? \"bottom\"\n : undefined\n }\n />\n )\n })\n\n return <>{renderedRows}\n}\n\n/**\n * Memoized virtual body: skip re-renders during active column resize.\n * Column widths update via CSS variables on the element,\n * so the browser handles width changes without React re-renders.\n */\nconst MemoizedVirtualBody = memo(\n DataGridTableVirtualBody,\n (_prev, next) => !!next.table.getState().columnSizingInfo.isResizingColumn\n) as typeof DataGridTableVirtualBody\n\nfunction DataGridTableVirtual({\n height,\n estimateSize = 48,\n overscan = 10,\n scrollBehavior = \"auto\",\n scrollToRowAlign = \"auto\",\n scrollToRowIndex,\n footerContent,\n renderHeader = true,\n onFetchMore,\n isFetchingMore = false,\n hasMore,\n fetchMoreOffset = 0,\n virtualizerOptions,\n}: DataGridTableVirtualProps) {\n const { table, props } = useDataGrid()\n const mergedHeaderGroups = getDataGridTableMergedHeaderGroups(table)\n const hasRightPinnedColumns = hasDataGridTableRightPinnedColumns(table)\n const { topRows, centerRows, bottomRows } = getDataGridTableRowSections(\n table,\n props.tableLayout?.rowsPinnable\n )\n const isInfiniteMode = typeof onFetchMore === \"function\"\n const [viewportElements, setViewportElements] =\n useState({\n containerElement: null,\n scrollElement: null,\n })\n\n const {\n estimateSize: customEstimateSize,\n getItemKey: customGetItemKey,\n getScrollElement: customGetScrollElement,\n measureElement: customMeasureElement,\n overscan: customOverscan,\n ...virtualizerOptionsRest\n } = virtualizerOptions ?? {}\n\n const isVirtualizationEnabled = virtualizerOptions?.enabled !== false\n const loadingMoreMessage =\n props.fetchingMoreMessage || props.loadingMessage || \"Loading...\"\n const allRowsLoadedMessage =\n props.allRowsLoadedMessage || \"All records loaded\"\n\n const handleViewportRef = useCallback((node: HTMLDivElement | null) => {\n setViewportElements({\n containerElement: node,\n scrollElement: node\n ? (getDataGridScrollAreaViewport(node) ?? node)\n : null,\n })\n }, [])\n\n const usesExternalScrollArea =\n viewportElements.scrollElement !== null &&\n viewportElements.scrollElement !== viewportElements.containerElement\n\n const resolveScrollElement = useCallback(() => {\n if (customGetScrollElement) {\n return customGetScrollElement(viewportElements)\n }\n\n return viewportElements.scrollElement\n }, [customGetScrollElement, viewportElements])\n\n const resolveItemKey = useCallback(\n (index: number) => {\n const row = centerRows[index]\n\n if (!row) return index\n\n return customGetItemKey?.(index, row) ?? row.id ?? index\n },\n [centerRows, customGetItemKey]\n )\n\n const resolveEstimateSize = useCallback(\n (index: number) => {\n const row = centerRows[index]\n\n return row\n ? (customEstimateSize?.(index, row) ?? estimateSize)\n : estimateSize\n },\n [centerRows, customEstimateSize, estimateSize]\n )\n\n const virtualizer = useVirtualizer({\n count: centerRows.length,\n getScrollElement: resolveScrollElement,\n getItemKey: resolveItemKey,\n estimateSize: resolveEstimateSize,\n overscan: customOverscan ?? overscan,\n measureElement: customMeasureElement,\n ...virtualizerOptionsRest,\n }) as DataGridTableVirtualizerInstance\n\n const virtualItems = isVirtualizationEnabled\n ? virtualizer.getVirtualItems()\n : []\n const totalSize = isVirtualizationEnabled ? virtualizer.getTotalSize() : 0\n const measureRowRef =\n isVirtualizationEnabled && customMeasureElement\n ? virtualizer.measureElement\n : undefined\n const resolvedFetchMoreOffset = Math.max(0, fetchMoreOffset)\n const scrollToRowId =\n scrollToRowIndex !== undefined\n ? centerRows[scrollToRowIndex]?.id\n : undefined\n const scrollToRowVirtualItem =\n isVirtualizationEnabled && scrollToRowIndex !== undefined\n ? virtualItems.find((item) => item.index === scrollToRowIndex)\n : undefined\n const pendingScrollToRowIndexRef = useRef(null)\n const lastScrollRequestRef = useRef(\n null\n )\n // Latch onFetchMore per row count: virtualItems gets a new identity every\n // scroll frame, so without it the effect fires duplicate page requests\n // before the consumer flips isFetchingMore, and loops at end-of-data when\n // hasMore is never set.\n const fetchMoreFiredAtCountRef = useRef(null)\n\n // Resolve after every commit so a stable getter can expose a replaced ref;\n // the request signature prevents duplicate scrolling on ordinary renders.\n useEffect(() => {\n const previousRequest = lastScrollRequestRef.current\n\n if (\n scrollToRowIndex === undefined ||\n scrollToRowIndex < 0 ||\n scrollToRowIndex >= centerRows.length\n ) {\n pendingScrollToRowIndexRef.current = null\n lastScrollRequestRef.current = null\n\n if (previousRequest) {\n const scrollElement = resolveScrollElement()\n\n if (scrollElement) {\n scrollDataGridTableToOffset({\n behavior: \"auto\",\n scrollElement,\n targetTop: scrollElement.scrollTop,\n virtualizer: isVirtualizationEnabled ? virtualizer : undefined,\n })\n }\n }\n\n return\n }\n\n const scrollElement = resolveScrollElement()\n const containerElement = viewportElements.containerElement\n if (!containerElement || !scrollElement) return\n\n const headerSticky = renderHeader && !!props.tableLayout?.headerSticky\n const nextRequest: DataGridTableVirtualScrollRequest = {\n align: scrollToRowAlign,\n behavior: scrollBehavior,\n containerElement,\n headerSticky,\n isVirtualizationEnabled,\n rowId: scrollToRowId,\n rowIndex: scrollToRowIndex,\n scrollElement,\n }\n\n if (isSameDataGridTableScrollRequest(previousRequest, nextRequest)) return\n\n pendingScrollToRowIndexRef.current = null\n\n const rowWasHandled = scrollDataGridTableRowIntoView({\n align: scrollToRowAlign,\n behavior: scrollBehavior,\n cancelPendingScroll: previousRequest !== null,\n containerElement,\n headerSticky,\n rowIndex: scrollToRowIndex,\n scrollElement,\n virtualizer: isVirtualizationEnabled ? virtualizer : undefined,\n })\n\n if (rowWasHandled) {\n lastScrollRequestRef.current = nextRequest\n return\n }\n\n if (!isVirtualizationEnabled) return\n\n pendingScrollToRowIndexRef.current = scrollToRowIndex\n lastScrollRequestRef.current = nextRequest\n virtualizer.scrollToIndex(scrollToRowIndex, {\n align: scrollToRowAlign,\n behavior: scrollBehavior,\n })\n })\n\n useEffect(() => {\n if (\n !isVirtualizationEnabled ||\n scrollToRowIndex === undefined ||\n pendingScrollToRowIndexRef.current !== scrollToRowIndex ||\n !scrollToRowVirtualItem\n ) {\n return\n }\n\n const rowWasHandled = scrollDataGridTableRowIntoView({\n align: scrollToRowAlign,\n behavior: \"auto\",\n cancelPendingScroll: true,\n containerElement: viewportElements.containerElement,\n headerSticky: renderHeader && !!props.tableLayout?.headerSticky,\n rowIndex: scrollToRowIndex,\n scrollElement: resolveScrollElement(),\n virtualizer,\n })\n\n if (rowWasHandled) {\n pendingScrollToRowIndexRef.current = null\n }\n }, [\n isVirtualizationEnabled,\n props.tableLayout?.headerSticky,\n renderHeader,\n resolveScrollElement,\n scrollToRowAlign,\n scrollToRowIndex,\n scrollToRowVirtualItem,\n virtualizer,\n viewportElements.containerElement,\n ])\n\n useEffect(() => {\n if (\n !isVirtualizationEnabled ||\n !isInfiniteMode ||\n hasMore === false ||\n isFetchingMore\n ) {\n return\n }\n\n const lastItem = virtualItems[virtualItems.length - 1]\n if (!lastItem) return\n\n if (fetchMoreFiredAtCountRef.current === centerRows.length) return\n\n if (lastItem.index >= centerRows.length - 1 - resolvedFetchMoreOffset) {\n fetchMoreFiredAtCountRef.current = centerRows.length\n onFetchMore?.()\n }\n }, [\n centerRows.length,\n hasMore,\n isFetchingMore,\n isInfiniteMode,\n isVirtualizationEnabled,\n onFetchMore,\n resolvedFetchMoreOffset,\n virtualItems,\n ])\n\n return (\n \n \n {renderHeader && (\n \n {mergedHeaderGroups.map((headerGroup) => (\n \n {headerGroup.headers\n .filter((header) => header.column.getIsPinned() !== \"right\")\n .map((header) => {\n const { column } = header\n\n return (\n \n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext()\n )}\n {props.tableLayout?.columnsResizable &&\n column.getCanResize() && (\n \n )}\n \n )\n })}\n {props.tableLayout?.columnsResizable &&\n hasRightPinnedColumns ? (\n \n ) : null}\n {headerGroup.headers\n .filter((header) => header.column.getIsPinned() === \"right\")\n .map((header) => {\n const { column } = header\n\n return (\n \n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext()\n )}\n {props.tableLayout?.columnsResizable &&\n column.getCanResize() && (\n \n )}\n \n )\n })}\n {props.tableLayout?.columnsResizable &&\n !hasRightPinnedColumns ? (\n \n ) : null}\n \n ))}\n \n )}\n\n {renderHeader &&\n (props.tableLayout?.stripped || !props.tableLayout?.rowBorder) && (\n \n )}\n\n \n \n \n\n {footerContent && (\n {footerContent}\n )}\n \n \n )\n}\n\nexport { DataGridTableVirtual }\nexport type {\n DataGridTableVirtualScrollAlignment,\n DataGridTableVirtualProps,\n DataGridTableVirtualScrollElements,\n DataGridTableVirtualizerOptions,\n}","target":"components/neui/data-grid/data-grid-table-virtual.tsx"}]}