import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react" import type { CSSProperties, ReactNode } from "react" import { useDataGrid } from "@/components/reui/data-grid/data-grid" import type { DataGridFeatures, DataGridTableInstance, } from "@/components/reui/data-grid/data-grid" import { DataGridTableAddRow, DataGridTableBase, DataGridTableBody, DataGridTableEmpty, DataGridTableFillBodyCell, DataGridTableFillHeadCell, DataGridTableFoot, DataGridTableHead, DataGridTableHeadRow, DataGridTableHeadRowCell, DataGridTableHeadRowCellResize, DataGridTableRenderedRow, DataGridTableRowSpacer, DataGridTableViewport, getDataGridScrollAreaViewport, getDataGridTableMergedHeaderGroups, getDataGridTableRowSections, getPinningStyles, hasDataGridTableRightPinnedColumns, } from "@/components/reui/data-grid/data-grid-table" import { flexRender } from "@tanstack/react-table" import type { Column, Row, Table } from "@tanstack/react-table" import { useVirtualizer } from "@tanstack/react-virtual" import type { VirtualItem, Virtualizer, VirtualizerOptions, } from "@tanstack/react-virtual" import { cn } from "@evobgp/ui/lib/utils" import { Spinner } from "@evobgp/ui/components/spinner" type DataGridTableVirtualScrollElements = { containerElement: HTMLDivElement | null scrollElement: HTMLElement | null } type DataGridTableVirtualizerInstance = Virtualizer< HTMLElement, HTMLTableRowElement > type DataGridTableVirtualScrollAlignment = "auto" | "center" | "start" | "end" interface DataGridTableColumnVirtualizerOptions { /** Off unless explicitly true; a column window is opt-in. */ enabled?: boolean overscan?: number } type DataGridTableColumnScrollRequest = { align: DataGridTableVirtualScrollAlignment behavior: ScrollBehavior columnId: string | undefined columnIndex: number scrollElement: HTMLElement } function isSameDataGridTableColumnScrollRequest( previous: DataGridTableColumnScrollRequest | null, next: DataGridTableColumnScrollRequest ) { return ( previous?.align === next.align && previous.behavior === next.behavior && previous.columnId === next.columnId && previous.columnIndex === next.columnIndex && previous.scrollElement === next.scrollElement ) } type DataGridTableVirtualScrollRequest = { align: DataGridTableVirtualScrollAlignment behavior: ScrollBehavior containerElement: HTMLDivElement headerSticky: boolean isVirtualizationEnabled: boolean rowId: string | undefined rowIndex: number scrollElement: HTMLElement } function isSameDataGridTableScrollRequest( previous: DataGridTableVirtualScrollRequest | null, next: DataGridTableVirtualScrollRequest ) { return ( previous?.align === next.align && previous.behavior === next.behavior && previous.containerElement === next.containerElement && previous.headerSticky === next.headerSticky && previous.isVirtualizationEnabled === next.isVirtualizationEnabled && previous.rowId === next.rowId && previous.rowIndex === next.rowIndex && previous.scrollElement === next.scrollElement ) } function getDataGridTableScrollTarget({ align, clientHeight, rowBottom, rowHeight, rowTop, scrollHeight, scrollTop, viewportTopOffset = 0, }: { align: DataGridTableVirtualScrollAlignment clientHeight: number rowBottom: number rowHeight: number rowTop: number scrollHeight: number scrollTop: number viewportTopOffset?: number }) { const visibleHeight = Math.max(0, clientHeight - viewportTopOffset) const viewportTop = scrollTop + viewportTopOffset const viewportBottom = scrollTop + clientHeight const targetTop = align === "auto" ? rowTop < viewportTop ? rowTop - viewportTopOffset : rowBottom > viewportBottom ? rowBottom - clientHeight : null : align === "start" ? rowTop - viewportTopOffset : align === "end" ? rowBottom - clientHeight : rowTop - viewportTopOffset - Math.max(0, (visibleHeight - rowHeight) / 2) if (targetTop === null) return null return Math.min( Math.max(0, targetTop), Math.max(0, scrollHeight - clientHeight) ) } function getDataGridTableHeaderOffset({ containerElement, headerSticky, scrollElement, }: { containerElement: HTMLDivElement headerSticky: boolean scrollElement: HTMLElement }) { if (!headerSticky) return 0 const headerElement = containerElement.querySelector( ':scope > [data-slot="data-grid-table"] > thead' ) if (!headerElement) return 0 const scrollRect = scrollElement.getBoundingClientRect() const headerRect = headerElement.getBoundingClientRect() const headerBottomOffset = headerRect.bottom - scrollRect.top const overlapsViewportTop = headerRect.top <= scrollRect.top + 0.5 && headerBottomOffset > 0 if (!overlapsViewportTop) return 0 return Math.min(scrollElement.clientHeight, Math.max(0, headerBottomOffset)) } function scrollDataGridTableToOffset({ behavior, scrollElement, targetTop, virtualizer, }: { behavior: ScrollBehavior scrollElement: HTMLElement targetTop: number virtualizer?: DataGridTableVirtualizerInstance }) { if (virtualizer) { virtualizer.scrollToOffset(targetTop, { align: "start", behavior }) } else if (typeof scrollElement.scrollTo === "function") { scrollElement.scrollTo({ behavior, top: targetTop }) } else { scrollElement.scrollTop = targetTop } } function scrollDataGridTableRowIntoView({ align, behavior, cancelPendingScroll = false, containerElement, headerSticky, rowIndex, scrollElement, virtualizer, }: { align: DataGridTableVirtualScrollAlignment behavior: ScrollBehavior cancelPendingScroll?: boolean containerElement: HTMLDivElement | null headerSticky: boolean rowIndex: number scrollElement: HTMLElement | null virtualizer?: DataGridTableVirtualizerInstance }) { if (!containerElement || !scrollElement) return false const rowElement = containerElement.querySelector( `:scope > [data-slot="data-grid-table"] > tbody > tr[data-index="${rowIndex}"]` ) if (!rowElement) return false const scrollRect = scrollElement.getBoundingClientRect() const rowRect = rowElement.getBoundingClientRect() const viewportTopOffset = getDataGridTableHeaderOffset({ containerElement, headerSticky, scrollElement, }) const rowTop = scrollElement.scrollTop + rowRect.top - scrollRect.top const rowBottom = scrollElement.scrollTop + rowRect.bottom - scrollRect.top const targetTop = getDataGridTableScrollTarget({ align, clientHeight: scrollElement.clientHeight, rowBottom, rowHeight: rowRect.height || rowElement.offsetHeight, rowTop, scrollHeight: scrollElement.scrollHeight, scrollTop: scrollElement.scrollTop, viewportTopOffset, }) if ( targetTop === null || Math.abs(targetTop - scrollElement.scrollTop) < 0.5 ) { if (cancelPendingScroll) { scrollDataGridTableToOffset({ behavior: "auto", scrollElement, targetTop: scrollElement.scrollTop, virtualizer, }) } return true } scrollDataGridTableToOffset({ behavior, scrollElement, targetTop, virtualizer, }) return true } 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 /** Scroll animation used when revealing a controlled target row. */ scrollBehavior?: ScrollBehavior /** Alignment used when revealing a controlled target row. Defaults to auto. */ scrollToRowAlign?: DataGridTableVirtualScrollAlignment /** Index within the center (non-pinned) row section to reveal. */ scrollToRowIndex?: number /** * Opt-in horizontal virtualization of the CENTER columns. Activates only * under a fixed table layout with a single ungrouped header row; pinned * columns stay mounted, and anything else falls back to full-column * rendering. `scrollBehavior` is shared with controlled row scrolling. */ columnVirtualizerOptions?: DataGridTableColumnVirtualizerOptions /** Alignment used when revealing a controlled target column. */ scrollToColumnAlign?: DataGridTableVirtualScrollAlignment /** Index within the center (non-pinned) visible leaf columns to reveal. */ scrollToColumnIndex?: number footerContent?: ReactNode renderHeader?: boolean onFetchMore?: () => void isFetchingMore?: boolean hasMore?: boolean fetchMoreOffset?: number virtualizerOptions?: DataGridTableVirtualizerOptions } interface VirtualBodyProps { table: DataGridTableInstance 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 centerColumnWindow?: { start: number; end: number } } function DataGridTableVirtualPinnedPlaceholderCell({ column, }: { column: Column }) { const { props } = useDataGrid() const isPinned = column.getIsPinned() const isLastStartPinned = isPinned === "start" && column.getIsLastColumn("start") const isFirstEndPinned = isPinned === "end" && column.getIsFirstColumn("end") return ( ) } function DataGridTableVirtualUtilityRow({ table, children, centerCellClassName, centerCellStyle, rowClassName, ariaHidden, }: { table: DataGridTableInstance children: ReactNode centerCellClassName?: string centerCellStyle?: CSSProperties rowClassName?: string ariaHidden?: boolean }) { const { props } = useDataGrid() const leftVisibleColumns = table.getStartVisibleLeafColumns() const centerVisibleColumns = table.getCenterVisibleLeafColumns() const rightVisibleColumns = table.getEndVisibleLeafColumns() const hasRightPinnedColumns = hasDataGridTableRightPinnedColumns(table) return ( {leftVisibleColumns.map((column) => ( ))} {children} {props.tableLayout?.columnsResizable && hasRightPinnedColumns ? ( ) : null} {rightVisibleColumns.map((column) => ( ))} {props.tableLayout?.columnsResizable && !hasRightPinnedColumns ? ( ) : null} ) } function DataGridTableVirtualSpacer({ table, height, }: { table: DataGridTableInstance height: number }) { if (height <= 0) return null return ( {null} ) } function DataGridTableVirtualStatusRow({ table, children, className, }: { table: DataGridTableInstance children: ReactNode className?: string }) { return ( {children} ) } /** * A scroll frame only shifts the window, so every surviving row's inputs are * identical and its last render is reused; the per-frame cost is the rows * entering the window, not all mounted rows. Cell-level state (selection, * focus, row checks) repaints through each cell's own Subscribe, and any real * data change rebuilds the row wrappers, so identity comparison is safe. */ const MemoizedRenderedRow = memo( DataGridTableRenderedRow, (prev, next) => prev.row === next.row && prev.rowIndex === next.rowIndex && prev.rowRef === next.rowRef && prev.pinnedBoundary === next.pinnedBoundary && prev.centerWindow?.start === next.centerWindow?.start && prev.centerWindow?.end === next.centerWindow?.end ) as typeof DataGridTableRenderedRow function DataGridTableVirtualBody({ table, topRows, centerRows, bottomRows, virtualItems, totalSize, isVirtualizationEnabled, isInfiniteMode, isFetchingMore, hasMore, loadingMoreMessage, allRowsLoadedMessage, measureRowRef, centerColumnWindow, }: VirtualBodyProps) { const { isLoading } = useDataGrid() const totalRows = topRows.length + centerRows.length + bottomRows.length if (!totalRows) { // Initial load must not flash the empty state as if the query returned // nothing. if (isLoading) { return (
{loadingMoreMessage}
) } 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, rowIndex) => { 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. * A cell-selection drag gets the same treatment: painting goes through each * cell's own Subscribe, and the virtualizer re-renders itself from inside the * memo boundary, so parent-driven reconciliation during the drag is waste. */ const MemoizedVirtualBody = memo( DataGridTableVirtualBody, (_prev, next) => !!next.table.state.columnResizing.isResizingColumn || next.table._isSelectingCells === true ) as typeof DataGridTableVirtualBody function DataGridTableVirtual({ height, estimateSize = 48, overscan = 10, scrollBehavior = "auto", scrollToRowAlign = "auto", scrollToRowIndex, columnVirtualizerOptions, scrollToColumnAlign = "auto", scrollToColumnIndex, footerContent, renderHeader = true, onFetchMore, isFetchingMore = false, hasMore, fetchMoreOffset = 0, virtualizerOptions, }: DataGridTableVirtualProps) { const { i18n, table, props } = useDataGrid() const mergedHeaderGroups = getDataGridTableMergedHeaderGroups(table) const hasRightPinnedColumns = hasDataGridTableRightPinnedColumns(table) const centerVisibleColumns = table.getCenterVisibleLeafColumns() // Column windows only where the geometry is provable: a fixed table // layout (undefined defaults to fixed; the colgroup then owns every // width, so colSpan spacers cannot drift) and a single ungrouped header // row (a colSpan group cannot be windowed). Anything else falls back to // full-column rendering silently, the same posture as row virtualization // toward unsupported shapes. const columnVirtualizationActive = columnVirtualizerOptions?.enabled === true && props.tableLayout?.width !== "auto" && mergedHeaderGroups.length === 1 && (mergedHeaderGroups[0]?.headers.every((header) => header.colSpan <= 1) ?? false) const { topRows, centerRows, bottomRows } = getDataGridTableRowSections( table, props.tableLayout?.rowsPinnable ) 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 || i18n.labels.loading const allRowsLoadedMessage = props.allRowsLoadedMessage || i18n.labels.allRowsLoaded const handleViewportRef = useCallback((node: HTMLDivElement | null) => { setViewportElements({ containerElement: node, scrollElement: node ? (getDataGridScrollAreaViewport(node) ?? node) : null, }) }, []) 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 // Horizontal offsets invert in RTL; the virtualizer must be told. const isRtl = useMemo( () => viewportElements.containerElement ? getComputedStyle(viewportElements.containerElement).direction === "rtl" : false, [viewportElements.containerElement] ) const resolveColumnKey = useCallback( (index: number) => centerVisibleColumns[index]?.id ?? index, [centerVisibleColumns] ) const resolveColumnEstimateSize = useCallback( (index: number) => centerVisibleColumns[index]?.getSize() ?? 0, [centerVisibleColumns] ) const columnVirtualizer = useVirtualizer({ horizontal: true, isRtl, count: columnVirtualizationActive ? centerVisibleColumns.length : 0, getScrollElement: resolveScrollElement, getItemKey: resolveColumnKey, estimateSize: resolveColumnEstimateSize, overscan: columnVirtualizerOptions?.overscan ?? 3, }) // Column sizes are exact reads of getSize(), never DOM-measured, so a // resize commit or a visibility/order/pinning change must resync the // virtualizer's cached sizes by hand. useEffect(() => { if (columnVirtualizationActive) columnVirtualizer.measure() // eslint-disable-next-line react-hooks/exhaustive-deps }, [ columnVirtualizationActive, columnVirtualizer, // eslint-disable-next-line react-hooks/exhaustive-deps table.state.columnSizing, // eslint-disable-next-line react-hooks/exhaustive-deps table.state.columnVisibility, // eslint-disable-next-line react-hooks/exhaustive-deps table.state.columnOrder, // eslint-disable-next-line react-hooks/exhaustive-deps table.state.columnPinning, ]) const virtualColumns = columnVirtualizationActive ? columnVirtualizer.getVirtualItems() : [] // The window is a contiguous inclusive [start, end] over the center // columns; before the scroll element resolves the item list is empty and // the grid renders full-width for that first frame. const centerColumnWindow = columnVirtualizationActive && virtualColumns.length > 0 ? { start: virtualColumns[0]!.index, end: virtualColumns[virtualColumns.length - 1]!.index, } : undefined const virtualItems = isVirtualizationEnabled ? virtualizer.getVirtualItems() : [] const totalSize = isVirtualizationEnabled ? virtualizer.getTotalSize() : 0 const measureRowRef = isVirtualizationEnabled && customMeasureElement ? virtualizer.measureElement : undefined const resolvedFetchMoreOffset = Math.max(0, fetchMoreOffset) const scrollToRowId = scrollToRowIndex !== undefined ? centerRows[scrollToRowIndex]?.id : undefined const scrollToRowVirtualItem = isVirtualizationEnabled && scrollToRowIndex !== undefined ? virtualItems.find((item) => item.index === scrollToRowIndex) : undefined const pendingScrollToRowIndexRef = useRef(null) const lastScrollRequestRef = useRef( null ) // Latch onFetchMore per row count: virtualItems gets a new identity every // scroll frame, so without it the effect fires duplicate page requests // before the consumer flips isFetchingMore, and loops at end-of-data when // hasMore is never set. const fetchMoreFiredAtCountRef = useRef(null) const lastColumnScrollRequestRef = useRef(null) // Controlled column reveal: same dedupe posture as the row request - the // signature keeps ordinary renders from re-scrolling, and exact column // sizes mean no post-measure follow-up pass is needed. useEffect(() => { if ( !columnVirtualizationActive || scrollToColumnIndex === undefined || scrollToColumnIndex < 0 || scrollToColumnIndex >= centerVisibleColumns.length || !viewportElements.scrollElement ) { return } const request: DataGridTableColumnScrollRequest = { align: scrollToColumnAlign, behavior: scrollBehavior, columnId: centerVisibleColumns[scrollToColumnIndex]?.id, columnIndex: scrollToColumnIndex, scrollElement: viewportElements.scrollElement, } if ( isSameDataGridTableColumnScrollRequest( lastColumnScrollRequestRef.current, request ) ) { return } lastColumnScrollRequestRef.current = request columnVirtualizer.scrollToIndex(scrollToColumnIndex, { align: scrollToColumnAlign, behavior: scrollBehavior === "smooth" ? "smooth" : "auto", }) }, [ columnVirtualizationActive, columnVirtualizer, centerVisibleColumns, scrollToColumnAlign, scrollToColumnIndex, scrollBehavior, viewportElements.scrollElement, ]) // Resolve after every commit so a stable getter can expose a replaced ref; // the request signature prevents duplicate scrolling on ordinary renders. useEffect(() => { const previousRequest = lastScrollRequestRef.current if ( scrollToRowIndex === undefined || scrollToRowIndex < 0 || scrollToRowIndex >= centerRows.length ) { pendingScrollToRowIndexRef.current = null lastScrollRequestRef.current = null if (previousRequest) { const scrollElement = resolveScrollElement() if (scrollElement) { scrollDataGridTableToOffset({ behavior: "auto", scrollElement, targetTop: scrollElement.scrollTop, virtualizer: isVirtualizationEnabled ? virtualizer : undefined, }) } } return } const scrollElement = resolveScrollElement() const containerElement = viewportElements.containerElement if (!containerElement || !scrollElement) return const headerSticky = renderHeader && !!props.tableLayout?.headerSticky const nextRequest: DataGridTableVirtualScrollRequest = { align: scrollToRowAlign, behavior: scrollBehavior, containerElement, headerSticky, isVirtualizationEnabled, rowId: scrollToRowId, rowIndex: scrollToRowIndex, scrollElement, } if (isSameDataGridTableScrollRequest(previousRequest, nextRequest)) return pendingScrollToRowIndexRef.current = null const rowWasHandled = scrollDataGridTableRowIntoView({ align: scrollToRowAlign, behavior: scrollBehavior, cancelPendingScroll: previousRequest !== null, containerElement, headerSticky, rowIndex: scrollToRowIndex, scrollElement, virtualizer: isVirtualizationEnabled ? virtualizer : undefined, }) if (rowWasHandled) { lastScrollRequestRef.current = nextRequest return } if (!isVirtualizationEnabled) return pendingScrollToRowIndexRef.current = scrollToRowIndex lastScrollRequestRef.current = nextRequest virtualizer.scrollToIndex(scrollToRowIndex, { align: scrollToRowAlign, behavior: scrollBehavior, }) }) useEffect(() => { if ( !isVirtualizationEnabled || scrollToRowIndex === undefined || pendingScrollToRowIndexRef.current !== scrollToRowIndex || !scrollToRowVirtualItem ) { return } const rowWasHandled = scrollDataGridTableRowIntoView({ align: scrollToRowAlign, behavior: "auto", cancelPendingScroll: true, containerElement: viewportElements.containerElement, headerSticky: renderHeader && !!props.tableLayout?.headerSticky, rowIndex: scrollToRowIndex, scrollElement: resolveScrollElement(), virtualizer, }) if (rowWasHandled) { pendingScrollToRowIndexRef.current = null } }, [ isVirtualizationEnabled, props.tableLayout?.headerSticky, renderHeader, resolveScrollElement, scrollToRowAlign, scrollToRowIndex, scrollToRowVirtualItem, virtualizer, viewportElements.containerElement, ]) useEffect(() => { if ( !isVirtualizationEnabled || !isInfiniteMode || hasMore === false || isFetchingMore ) { return } const lastItem = virtualItems[virtualItems.length - 1] if (!lastItem) return if (fetchMoreFiredAtCountRef.current === centerRows.length) return if (lastItem.index >= centerRows.length - 1 - resolvedFetchMoreOffset) { fetchMoreFiredAtCountRef.current = centerRows.length onFetchMore?.() } }, [ centerRows.length, hasMore, isFetchingMore, isInfiniteMode, isVirtualizationEnabled, onFetchMore, resolvedFetchMoreOffset, virtualItems, ]) // The header re-renders only when its real inputs move: the table // wrapper (any table state change recreates it), the layout props, and // the column window. A scroll frame changes none of them, so the whole // sortable-header subtree is reused instead of rebuilt per frame. // eslint-disable-next-line react-hooks/exhaustive-deps const headerNode = useMemo( () => renderHeader && ( {mergedHeaderGroups.map((headerGroup) => ( {/* Under an active column window the single ungrouped header row is bucketed start / windowed center / end, with each off-window flank one colSpan spacer sized by the intact colgroup - the same shape the body rows take. */} {centerColumnWindow ? headerGroup.headers .filter((header) => header.column.getIsPinned() === "start") .map((header) => ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} {props.tableLayout?.columnsResizable && header.column.getCanResize() && ( )} )) : null} {centerColumnWindow && centerColumnWindow.start > 0 ? (