import React, { useState, useEffect, useMemo, forwardRef, useRef, useId, useCallback, } from 'react' import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent, DragOverlay, } from '@dnd-kit/core' import { arrayMove, SortableContext, sortableKeyboardCoordinates, horizontalListSortingStrategy, } from '@dnd-kit/sortable' import PrimitiveText from '../Primitives/PrimitiveText/PrimitiveText' import { DataTableProps, SortDirection, SortConfig, ColumnDefinition, SearchConfig, ColumnFilter, FilterType, ColumnType, RowActionsConfig, DataTableExportFormat, } from './types' import { TableTokenType } from './dataTable.tokens' import { sortData, searchData, applyColumnFilters, updateColumnFilter, exportSelectedRowsToCSV, getSelectedRowCount, createSearchConfig, clearAllFiltersAndSearch, getColumnStyles, haveSameFilterOptions, getDataTableBodyState, downloadDataTableExport, } from './utils' import DataTableHeader from './DataTableHeader' import TableHeader from './TableHeader' import TableBodyComponent from './TableBody' import TableFooter from './TableFooter' import BulkActionBar from './TableBody/BulkActionBar' import Block from '../Primitives/Block/Block' import Button from '../Button/Button' import { Spinner } from '../Spinner' import { EmptyState } from '../EmptyState' import { ButtonSize, ButtonType } from '../Button/types' import { Settings, Check, CircleAlert, Download } from 'lucide-react' import Menu from '../Menu/Menu' import { MenuGroupType, MenuAlignment } from '../Menu/types' import { useMobileDataTable } from './hooks/useMobileDataTable' import MobileColumnDrawer from './MobileColumnDrawer' // eslint-disable-next-line import-x/no-cycle -- intentional recursion: DataTable renders PivotTableModal whose preview renders a nested DataTable import PivotTableModal from './PivotTableModal' import type { PivotTableConfig } from './PivotTableModal/types' import { useResponsiveTokens } from '../../hooks/useResponsiveTokens' import { useTheme } from '../../context/ThemeContext' import styled from 'styled-components' import { FOUNDATION_THEME } from '../../tokens' const ScrollableContainer = styled(Block)` overflow-x: auto; scroll-behavior: smooth; -webkit-overflow-scrolling: touch; /* Hide scrollbars for Chrome, Safari and Opera */ &::-webkit-scrollbar { display: none; } /* Hide scrollbar for IE, Edge and Firefox */ -ms-overflow-style: none; scrollbar-width: none; ` const DefaultTableState = ({ onRetry, tableToken, isDarkTheme, }: { onRetry?: () => void tableToken: TableTokenType isDarkTheme: boolean }) => { const stateTextColor = tableToken.dataTable.table.body.cell.color const iconBackground = isDarkTheme ? FOUNDATION_THEME.colors.red[900] : FOUNDATION_THEME.colors.red[50] const iconColor = isDarkTheme ? FOUNDATION_THEME.colors.red[400] : FOUNDATION_THEME.colors.red[600] return ( Unable to load data {onRetry && ( ) : ( ({ label: format.toUpperCase(), onClick: () => void handleTableExport(format), })), showSeparator: false, }, ]} alignment={MenuAlignment.END} trigger={ } /> ) ) : null // Stable row ID list for the current page. Used for selection state and // as a cheap signal to remount the tbody when results change (e.g. server-side search). const currentPageRowIds = useMemo(() => { return currentData.map((row) => String(row[idField])) }, [currentData, idField]) // Monotonically increasing "dataVersion" for the current page's row IDs. // This avoids expensive per-character hashing in TableBody while still // forcing a remount when IDs change but length/first/last stay the same. const [tbodyDataVersion, setTbodyDataVersion] = useState(0) const prevPageRowIdsRef = useRef(null) useEffect(() => { const prev = prevPageRowIdsRef.current let changed = prev == null || prev.length !== currentPageRowIds.length if (!changed && prev) { for (let i = 0; i < prev.length; i++) { if (prev[i] !== currentPageRowIds[i]) { changed = true break } } } if (changed) { setTbodyDataVersion((v) => v + 1) } prevPageRowIdsRef.current = currentPageRowIds }, [currentPageRowIds]) const updateSelectAllState = ( selectedRowsState: Record ) => { const selectableRowIds = currentData .map((row, index) => ({ row, rowId: String(row[idField]), index, })) .filter( ({ row, index }) => !(isRowDisabledFn && isRowDisabledFn(row, index)) ) .map(({ rowId }) => rowId) const totalSelectableCount = selectableRowIds.length if (totalSelectableCount === 0) { setSelectAll(false) return } const selectedCurrentPageRows = selectableRowIds.filter( (rowId) => selectedRowsState[rowId] ) if (selectedCurrentPageRows.length === 0) { setSelectAll(false) } else if ( selectedCurrentPageRows.length === totalSelectableCount ) { setSelectAll(true) } else { setSelectAll('indeterminate') } } useEffect(() => { updateSelectAllState(selectedRows) }, [currentData, selectedRows]) const hasMountedScrollRef = useRef(false) useEffect(() => { const currentColumnCount = visibleColumns.length if (hasMountedScrollRef.current) { const el = scrollContainerRef.current const shouldScrollToEnd = currentColumnCount > previousColumnCount && !!el && el.scrollWidth - (el.scrollLeft + el.clientWidth) < 16 if (shouldScrollToEnd) { setTimeout(() => { if (scrollContainerRef.current) { scrollContainerRef.current.scrollLeft = scrollContainerRef.current.scrollWidth } }, 100) } } else { hasMountedScrollRef.current = true } setPreviousColumnCount(currentColumnCount) }, [visibleColumns.length, previousColumnCount]) const handleSelectAll = (checked: boolean | 'indeterminate') => { const newSelectAll = checked === true const newSelectedRows = { ...selectedRows } if (newSelectAll) { currentData.forEach((row, index) => { const rowId = String(row[idField]) // Only select rows that are not disabled if (!(isRowDisabledFn && isRowDisabledFn(row, index))) { newSelectedRows[rowId] = true } }) } else { currentData.forEach((row) => { const rowId = String(row[idField]) newSelectedRows[rowId] = false }) } setSelectedRows(newSelectedRows) updateSelectAllState(newSelectedRows) if (onRowSelectionChange) { const selectedRowIds = Object.entries(newSelectedRows) .filter(([, selected]) => selected) .map(([id]) => id) currentData.forEach((row) => { const rowId = String(row[idField]) const wasSelected = selectedRows[rowId] || false const isSelected = newSelectedRows[rowId] || false if (wasSelected !== isSelected) { const rawRowData = data.find( (d) => String(d[idField]) === rowId ) const rowDataToPass = (rawRowData || row) as T onRowSelectionChange( selectedRowIds, isSelected, rowId, rowDataToPass ) } }) } } const isRowDisabledFn = rowSelectionConfig?.isDisabled const handleRowSelect = (rowId: unknown, rowIndex?: number) => { const rowIdStr = String(rowId) // If no index provided, fall back to find (for backwards compatibility) if (rowIndex === undefined) { const row = currentData.find( (r) => String(r[idField]) === rowIdStr ) if (!row || (isRowDisabledFn && isRowDisabledFn(row, -1))) { return } // Continue with the found row const isSelected = !selectedRows[rowIdStr] const newSelectedRows = { ...selectedRows, [rowIdStr]: isSelected, } setSelectedRows(newSelectedRows) updateSelectAllState(newSelectedRows) if (onRowSelectionChange) { const selectedRowIds = Object.entries(newSelectedRows) .filter(([, selected]) => selected) .map(([id]) => id) const rawRowData = data.find( (d) => String(d[idField]) === rowIdStr ) const rowDataFromCurrent = currentData.find( (row) => String(row[idField]) === rowIdStr ) const rowDataToPass = (rawRowData || rowDataFromCurrent) as T if (rowDataToPass) { onRowSelectionChange( selectedRowIds, isSelected, rowIdStr, rowDataToPass ) } } return } // Bounds check and use index directly instead of scanning if ( rowIndex < 0 || rowIndex >= currentData.length || String(currentData[rowIndex][idField]) !== rowIdStr ) { return } const row = currentData[rowIndex] if (isRowDisabledFn && isRowDisabledFn(row, rowIndex)) { return } const isSelected = !selectedRows[rowIdStr] const newSelectedRows = { ...selectedRows, [rowIdStr]: isSelected, } setSelectedRows(newSelectedRows) updateSelectAllState(newSelectedRows) if (onRowSelectionChange) { const selectedRowIds = Object.entries(newSelectedRows) .filter(([, selected]) => selected) .map(([id]) => id) const rawRowData = data.find( (d) => String(d[idField]) === rowIdStr ) const rowDataFromCurrent = currentData.find( (row) => String(row[idField]) === rowIdStr ) const rowDataToPass = (rawRowData || rowDataFromCurrent) as T if (rowDataToPass) { onRowSelectionChange( selectedRowIds, isSelected, rowIdStr, rowDataToPass ) } } } const exportToCSV = () => { try { const dataForExport = isFormatEnabled ? processedData : processedData.map((row) => { const formattedRow = { ...row } as Record< string, unknown > visibleColumns.forEach((column) => { const fieldKey = String(column.field) if ( column.type === ColumnType.NUMBER || (typeof formattedRow[fieldKey] === 'string' && /^[$€£¥₹₽₪₩₦₡₵₸₴₺₻₼₽¢%,\d.\s]+$/.test( formattedRow[fieldKey] )) ) { formattedRow[fieldKey] = removeNumberFormatting( formattedRow[fieldKey] ) } }) return formattedRow as T }) exportSelectedRowsToCSV( dataForExport, selectedRows, visibleColumns, String(idField), `${title || 'data'}-export-${new Date().toISOString().split('T')[0]}.csv` ) } catch (error) { alert(error instanceof Error ? error.message : 'Export failed') } } const sortTimeoutRef = useRef(null) useEffect(() => { return () => { if (sortTimeoutRef.current) { clearTimeout(sortTimeoutRef.current) } } }, []) const applySortConfig = ( field: keyof T, newSortConfig: SortConfig | null ) => { setSortConfig(newSortConfig) if (onSortChange) { if (sortTimeoutRef.current) { clearTimeout(sortTimeoutRef.current) } sortTimeoutRef.current = setTimeout(() => { if (newSortConfig) { onSortChange(newSortConfig) } else { onSortChange({ field: field.toString(), direction: SortDirection.NONE, }) } sortTimeoutRef.current = null }, 10) } } const handleSort = (field: keyof T, sortType?: string) => { const column = visibleColumns.find((col) => col.field === field) if (!column || column.isSortable === false) { return } let direction: SortDirection let newSortConfig: SortConfig | null const isSameSort = sortConfig?.field === field && sortConfig?.sortType === sortType if (isSameSort) { if (sortConfig.direction === SortDirection.ASCENDING) { direction = SortDirection.DESCENDING newSortConfig = { field: field.toString(), direction, sortType, } } else if (sortConfig.direction === SortDirection.DESCENDING) { direction = SortDirection.NONE newSortConfig = null } else { direction = SortDirection.ASCENDING newSortConfig = { field: field.toString(), direction, sortType, } } } else { direction = SortDirection.ASCENDING newSortConfig = { field: field.toString(), direction, sortType, } } applySortConfig(field, newSortConfig) } const handleSortAscending = (field: keyof T, sortType?: string) => { const column = visibleColumns.find((col) => col.field === field) if (!column || column.isSortable === false) { return } const isCurrentlyAscending = sortConfig?.field === field && sortConfig.direction === SortDirection.ASCENDING && sortConfig?.sortType === sortType const newSortConfig = isCurrentlyAscending ? null : { field: field.toString(), direction: SortDirection.ASCENDING, sortType, } applySortConfig(field, newSortConfig) } const handleSortDescending = (field: keyof T, sortType?: string) => { const column = visibleColumns.find((col) => col.field === field) if (!column || column.isSortable === false) { return } const isCurrentlyDescending = sortConfig?.field === field && sortConfig.direction === SortDirection.DESCENDING && sortConfig?.sortType === sortType const newSortConfig = isCurrentlyDescending ? null : { field: field.toString(), direction: SortDirection.DESCENDING, sortType, } applySortConfig(field, newSortConfig) } const handlePageChange = (page: number) => { if (page !== currentPage) { setCurrentPage(page) if (serverSidePagination) { setInternalLoading(true) } if (onPageChange) { onPageChange(page) } } } const handlePageSizeChange = (size: number) => { if (size !== pageSize) { setPageSize(size) setCurrentPage(1) if (serverSidePagination) { setInternalLoading(true) } if (onPageSizeChange) { onPageSizeChange(size) } } } const handleSearch = (query: string) => { const newSearchConfig = createSearchConfig( query, searchConfig.caseSensitive, searchConfig.searchFields ) setSearchConfig(newSearchConfig) setCurrentPage(1) if (onSearchChange) { onSearchChange(newSearchConfig) } } const handleColumnFilter = ( field: string, type: FilterType, value: string | string[] | { min: number; max: number }, operator: | 'equals' | 'contains' | 'startsWith' | 'endsWith' | 'gt' | 'lt' | 'gte' | 'lte' | 'range' = 'contains' ) => { const updatedFilters = updateColumnFilter( columnFilters, field as keyof Record, type, value, operator ) setColumnFilters(updatedFilters) setCurrentPage(1) if (onFilterChange) { onFilterChange(updatedFilters) } } const clearAllFilters = () => { const { searchConfig: clearedSearchConfig } = clearAllFiltersAndSearch() setSearchConfig(clearedSearchConfig) setColumnFilters([]) setCurrentPage(1) if (onAdvancedFiltersChange) { onAdvancedFiltersChange([]) } if (onSearchChange) { onSearchChange(clearedSearchConfig) } if (onFilterChange) { onFilterChange([]) } } const selectedCount = getSelectedRowCount(selectedRows) const handleDeselectAll = () => { const newSelectedRows = { ...selectedRows } currentData.forEach((row) => { const rowId = String(row[idField]) newSelectedRows[rowId] = false }) setSelectedRows({ ...newSelectedRows }) setSelectAll(false) } const renderBulkActions = () => { if (!bulkActions?.customActions) return null return bulkActions.customActions } const getColumnWidth = ( column: ColumnDefinition ): React.CSSProperties => { return getColumnStyles(column) } const handleEditRow = (rowId: unknown) => { const rowIdStr = String(rowId) const row = currentData.find((r) => String(r[idField]) === rowIdStr) if (row) { setEditingRows((prev) => ({ ...prev, [rowIdStr]: true })) setEditValues((prev) => ({ ...prev, [rowIdStr]: { ...row } })) } } const handleSaveRow = (rowId: unknown) => { const rowIdStr = String(rowId) const updatedRow = editValues[rowIdStr] if (updatedRow && onRowSave) { onRowSave(rowId, updatedRow) } setEditingRows((prev) => ({ ...prev, [rowIdStr]: false })) setEditValues((prev) => { const newValues = { ...prev } delete newValues[rowIdStr] return newValues }) } const handleCancelEdit = (rowId: unknown) => { const rowIdStr = String(rowId) if (onRowCancel) { onRowCancel(rowId) } setEditingRows((prev) => ({ ...prev, [rowIdStr]: false })) setEditValues((prev) => { const newValues = { ...prev } delete newValues[rowIdStr] return newValues }) } const handleFieldChange = ( rowId: unknown, field: keyof T, value: unknown ) => { const rowIdStr = String(rowId) if (enableInlineEdit) { setEditValues((prev) => ({ ...prev, [rowIdStr]: { ...prev[rowIdStr], [field]: value, }, })) } else { if (onFieldChange) { onFieldChange(rowId, field, value) } } } const handleRowExpand = (rowId: unknown) => { const rowIdStr = String(rowId) const isCurrentlyExpanded = expandedRows[rowIdStr] const newExpandedRows: Record = {} if (!isCurrentlyExpanded) { newExpandedRows[rowIdStr] = true } setExpandedRows(newExpandedRows) if (onRowExpansionChange) { const rowData = currentData.find( (row) => row[idField] === rowId ) if (rowData) { onRowExpansionChange(rowId, !isCurrentlyExpanded, rowData) } } } const handleMobileOverflowClick = (row: T) => { const rowIndex = currentData.findIndex( (r) => r[idField] === row[idField] ) setSelectedRowForDrawer(row) setSelectedRowIndexForDrawer(rowIndex) setMobileDrawerOpen(true) } // const handleTableFocus = () => { // if (!focusedCell && currentData.length > 0) { // setFocusedCell({ rowIndex: 0, colIndex: 0 }) // setTimeout(() => { // const firstCell = document.querySelector( // '[data-row-index="0"][data-col-index="0"]' // ) as HTMLElement // if (firstCell) { // firstCell.focus() // } // }, 0) // } // } const totalColumnsT = effectiveVisibleColumns.length + (enableRowSelection ? 1 : 0) + (enableRowExpansion ? 1 : 0) + (showActionsColumn && (enableInlineEdit || rowActions) && !(mobileConfig.isMobile && mobileConfig.enableColumnOverflow) ? 1 : 0) + (mobileConfig.enableColumnOverflow && mobileOverflowColumns.length > 0 ? 1 : 0) const totalColumns = totalColumnsT > 0 && effectiveEnableColumnManager ? totalColumnsT + 1 : 0 const handleTableKeyDown = ( event: React.KeyboardEvent ) => { if (event.key === 'Tab') { return } const target = event.target as HTMLElement if ( target.closest('thead') || target.closest('[role="columnheader"]') || target.tagName === 'TH' ) { return } if ( target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT' || target.isContentEditable || target.closest('button') || target.closest('[role="button"]') || target.closest('[role="menuitem"]') ) { return } let newRowIndex = focusedCell?.rowIndex ?? 0 let newColIndex = focusedCell?.colIndex ?? 0 switch (event.key) { case 'ArrowRight': event.preventDefault() if (newColIndex < totalColumns - 1) { newColIndex++ } break case 'ArrowLeft': event.preventDefault() if (newColIndex > 0) { newColIndex-- } break case 'ArrowDown': event.preventDefault() if (newRowIndex < currentData.length - 1) { newRowIndex++ } break case 'ArrowUp': event.preventDefault() if (newRowIndex > 0) { newRowIndex-- } break case 'Home': event.preventDefault() if (event.ctrlKey || event.metaKey) { newRowIndex = 0 newColIndex = 0 } else { newColIndex = 0 } break case 'End': event.preventDefault() if (event.ctrlKey || event.metaKey) { newRowIndex = currentData.length - 1 newColIndex = totalColumns - 1 } else { newColIndex = totalColumns - 1 } break case 'PageDown': event.preventDefault() if (newRowIndex < currentData.length - 1) { newRowIndex = Math.min( newRowIndex + pageSize, currentData.length - 1 ) } break case 'PageUp': event.preventDefault() if (newRowIndex > 0) { newRowIndex = Math.max(newRowIndex - pageSize, 0) } break case 'Enter': case ' ': // Only trigger row click if focus is on a body cell, not header if ( onRowClick && focusedCell !== null && !target.closest('thead') && !target.closest('[role="columnheader"]') ) { const row = currentData[focusedCell.rowIndex] if (row) { onRowClick(row, focusedCell.rowIndex) } } return default: return } setFocusedCell({ rowIndex: newRowIndex, colIndex: newColIndex }) setTimeout(() => { const cellSelector = `[data-row-index="${newRowIndex}"][data-col-index="${newColIndex}"]` const cellElement = document.querySelector( cellSelector ) as HTMLElement if (cellElement) { cellElement.focus() cellElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest', }) } }, 0) } const containerRefCallback = useCallback( (node: HTMLDivElement | null) => { tableContainerRef.current = node if (typeof ref === 'function') { ref(node) } else if (ref) { ref.current = node } }, [ref] ) const pivotTriggerSlot = pivotTableConfig?.triggerSlot || 2 const pivotTriggerButton = useMemo(() => { if (!enablePivotTable || !pivotTableConfig?.triggerButton) { return null } const triggerNode = pivotTableConfig.triggerButton if (!React.isValidElement(triggerNode)) { return triggerNode } const existingOnClick = ( triggerNode.props as { onClick?: (event: unknown) => void } ).onClick return React.cloneElement( triggerNode as React.ReactElement<{ onClick?: (event: unknown) => void }>, { onClick: (event: unknown) => { existingOnClick?.(event) setIsPivotModalOpen(true) }, } ) }, [enablePivotTable, pivotTableConfig?.triggerButton]) const effectiveHeaderSlot1 = pivotTriggerSlot === 1 && pivotTriggerButton ? pivotTriggerButton : headerSlot1 const effectiveHeaderSlot2 = pivotTriggerSlot === 2 && pivotTriggerButton ? pivotTriggerButton : headerSlot2 const effectiveHeaderSlot3 = pivotTriggerSlot === 3 ? ( <> {headerSlot2} {pivotTriggerButton} ) : ( effectiveHeaderSlot2 ) return ( {title && ( )} {description && ( )} >[] } data={data} onSearch={handleSearch} onAdvancedFiltersChange={onAdvancedFiltersChange} onClearAllFilters={clearAllFilters} mobileToolbarSlot={exportButton} headerSlot1={ showSettings || (!mobileConfig.isMobile && exportButton) ? ( {showSettings && ( } size={ButtonSize.SMALL} aria-label="Table settings" > Settings } /> )} {!mobileConfig.isMobile && exportButton} ) : null } headerSlot2={effectiveHeaderSlot1} headerSlot3={effectiveHeaderSlot3} {...rest} /> {showBulkActionBar && !isErrorState && ( )} {isTableLoading ? 'Loading table data' : error ? 'Failed to load table data' : currentData.length === 0 ? 'No data available' : `Showing ${currentData.length} of ${totalRows} rows`} 0 ? tableBodyHeight ? typeof tableBodyHeight === 'number' ? `${tableBodyHeight}px` : tableBodyHeight : '668px' : undefined, overflowX: 'auto', overflowY: 'auto', }} > 0 ? totalRows : undefined } aria-colcount={totalColumns} aria-describedby={ [tableDescriptionId, statusRegionId] .filter(Boolean) .join(' ') || undefined } onKeyDown={handleTableKeyDown} // onFocus={handleTableFocus} tabIndex={-1} style={{ width: tableToken.dataTable.table .width, minWidth: 'auto', tableLayout: tableToken.dataTable.table .tableLayout, borderCollapse: tableToken.dataTable.table .borderCollapse, borderSpacing: tableToken.dataTable.table .borderSpacing, position: tableToken.dataTable.table .position, height: 'auto', minHeight: 'auto', backgroundColor: 'none', outline: 'none', }} > >[] } allVisibleColumns={ visibleColumns as ColumnDefinition< Record >[] } initialColumns={ initialColumns as ColumnDefinition< Record >[] } selectAll={selectAll} sortConfig={sortConfig} // serverSideFiltering filters via // onFilterChange without needing // enableFiltering, so it also // counts as filtering being on. enableFiltering={ enableFiltering || serverSideFiltering } enableInlineEdit={enableInlineEdit} showActionsColumn={ showActionsColumn } enableColumnManager={ effectiveEnableColumnManager } enableColumnReordering={ enableColumnReordering } showSkeleton={showSkeleton} isLoading={isTableLoading} onColumnReorder={(columns) => { setVisibleColumns( columns as ColumnDefinition[] ) onColumnReorder?.( columns as ColumnDefinition[] ) }} columnManagerMaxSelections={ columnManagerMaxSelections } columnManagerAlwaysSelected={columnManagerAlwaysSelected?.map( (field) => String(field) )} columnManagerPrimaryAction={ columnManagerPrimaryAction } columnManagerSecondaryAction={ columnManagerSecondaryAction } columnManagerWidth={ columnManagerWidth } enableRowExpansion={ enableRowExpansion } enableRowSelection={ enableRowSelection && !isErrorState } rowActions={ rowActions as | RowActionsConfig< Record< string, unknown > > | undefined } data={data} mobileConfig={mobileConfig} mobileOverflowColumns={ mobileOverflowColumns as ColumnDefinition< Record >[] } onMobileOverflowClick={(row) => handleMobileOverflowClick( row as T ) } onSort={handleSort} onSortAscending={ handleSortAscending } onSortDescending={ handleSortDescending } onOperations={onOperations} onInsertLeft={onInsertLeft} onInsertRight={onInsertRight} onDeleteColumn={onDeleteColumn} onSelectAll={handleSelectAll} onColumnChange={(columns) => setVisibleColumns( columns as ColumnDefinition[] ) } onColumnFilter={handleColumnFilter} columnFilters={columnFilters} getColumnWidth={ getColumnWidth as ( column: ColumnDefinition< Record >, index: number ) => React.CSSProperties } columnFreeze={effectiveColumnFreeze} columnFreezeRight={ columnFreezeRight } measuredFrozenWidths={ measuredFrozenWidths } onFrozenWidthsMeasured={ setMeasuredFrozenWidths } /> {shouldRenderRows && ( >[] } mobileConfig={mobileConfig} mobileOverflowColumns={ mobileOverflowColumns as ColumnDefinition< Record >[] } onMobileOverflowClick={(row) => handleMobileOverflowClick( row as T ) } idField={String(idField)} tableTitle={title} selectedRows={selectedRows} editingRows={editingRows} editValues={editValues} expandedRows={expandedRows} enableInlineEdit={ enableInlineEdit } showActionsColumn={ showActionsColumn } enableColumnManager={ effectiveEnableColumnManager } enableRowExpansion={ enableRowExpansion } enableRowSelection={ enableRowSelection } rowSelectionConfig={ rowSelectionConfig as | { isDisabled?: ( row: Record< string, unknown >, index: number ) => boolean disabledText?: ( row: Record< string, unknown >, index: number ) => string } | undefined } columnFreeze={ effectiveColumnFreeze } columnFreezeRight={ columnFreezeRight } measuredFrozenWidths={ measuredFrozenWidths } focusedCell={focusedCell} onCellFocus={( rowIndex, colIndex ) => setFocusedCell({ rowIndex, colIndex, }) } renderExpandedRow={ renderExpandedRow as | ((expandedData: { row: Record< string, unknown > index: number isExpanded: boolean toggleExpansion: () => void }) => React.ReactNode) | undefined } isRowExpandable={ isRowExpandable as | (( row: Record< string, unknown >, index: number ) => boolean) | undefined } onRowSelect={handleRowSelect} onEditRow={handleEditRow} onSaveRow={handleSaveRow} onCancelEdit={handleCancelEdit} onRowExpand={handleRowExpand} onFieldChange={ handleFieldChange } getColumnWidth={ getColumnWidth as ( column: ColumnDefinition< Record< string, unknown > >, index: number ) => React.CSSProperties } onRowClick={ onRowClick as | (( row: Record< string, unknown >, index: number ) => void) | undefined } getRowStyle={ getRowStyle as | (( row: Record< string, unknown >, index: number ) => React.CSSProperties) | undefined } getDisplayValue={( value: unknown, column: ColumnDefinition< Record > ) => getDisplayValue( value, column as ColumnDefinition ) } rowActions={ rowActions as | RowActionsConfig< Record< string, unknown > > | undefined } dateLabel={dateLabel} isLoading={isTableLoading} showSkeleton={showSkeleton} skeletonVariant={ skeletonVariant } isRowLoading={ isRowLoading as | (( row: Record< string, unknown >, index: number ) => boolean) | undefined } enableRowAnimation={ enableRowAnimation } rowAnimationConfig={ rowAnimationConfig } /> )}
{activeId ? ( { visibleColumns.find( (col) => String(col.field) === activeId )?.header } ) : null}
{bodyState !== 'rows' && ( {bodyState === 'loading' ? ( ) : bodyState === 'error' ? ( renderErrorState ? ( renderErrorState(onRetry) ) : ( ) ) : renderEmptyState ? ( renderEmptyState() ) : showEmptyState ? ( ) : ( No data available )} )}
{showFooter && ( 0} isNarrowContainer={isNarrowContainer} onPageChange={handlePageChange} onPageSizeChange={handlePageSizeChange} /> )}
{enablePivotTable && ( setIsPivotModalOpen(false)} data={processedData as Record[]} columns={ visibleColumns as ColumnDefinition< Record >[] } title={pivotTableConfig?.title} description={pivotTableConfig?.description} showExport={pivotTableConfig?.showExport} previewColumns={pivotTableConfig?.previewColumns} previewRows={ pivotTableConfig?.previewRows as | ({ __pivotId: string } & Record)[] | undefined } availableAggregations={ pivotTableConfig?.availableAggregations } initialConfig={ pivotTableConfig?.initialConfig as | Partial< PivotTableConfig> > | undefined } onConfigChange={ pivotTableConfig?.onConfigChange as | (( config: PivotTableConfig< Record > ) => void) | undefined } onExport={ pivotTableConfig?.onExport as | (( config: PivotTableConfig< Record > ) => void) | undefined } /> )} {mobileConfig.enableColumnOverflow && selectedRowForDrawer && ( { setMobileDrawerOpen(false) setSelectedRowForDrawer(null) setSelectedRowIndexForDrawer(-1) }} row={selectedRowForDrawer as Record} rowIndex={selectedRowIndexForDrawer} overflowColumns={ mobileOverflowColumns as ColumnDefinition< Record >[] } getDisplayValue={(value, column) => getDisplayValue( value, column as ColumnDefinition ) } onFieldChange={(field, value) => { setSelectedRowForDrawer((prev) => prev ? { ...prev, [field]: value } : null ) if (onFieldChange && selectedRowForDrawer) { const rowId = selectedRowForDrawer[idField] onFieldChange(rowId, field as keyof T, value) } }} rowActions={ rowActions as | RowActionsConfig> | undefined } /> )} ) } ) DataTable.displayName = 'DataTable' export default DataTable