2025-09-23 23:30:27 +02:00
import React , { useRef , useState , useEffect , useMemo , useCallback } from 'react' ;
import { useVirtualizer } from '@tanstack/react-virtual' ;
import { ArrowUp , ArrowDown , ArrowDownUp } from 'lucide-react' ;
import {
useReactTable ,
getCoreRowModel ,
flexRender ,
type SortingState ,
type VisibilityState ,
type ColumnDef ,
type Row ,
2025-09-26 22:04:25 +02:00
type Table as TTable ,
2025-09-23 23:30:27 +02:00
} from '@tanstack/react-table' ;
2025-09-26 18:51:45 +02:00
import type { DataTableProps , ProcessedDataRow } from './DataTable.types' ;
2025-09-23 23:30:27 +02:00
import { SelectionCheckbox , MemoizedTableRow , SkeletonRows } from './DataTableComponents' ;
2025-09-26 18:20:43 +02:00
import { Table , TableBody , TableHead , TableHeader , TableCell , TableRow } from '../Table' ;
2025-09-23 23:30:27 +02:00
import { useDebounced , useOptimizedRowSelection } from './DataTable.hooks' ;
import { DataTableErrorBoundary } from './DataTableErrorBoundary' ;
import { useMediaQuery , useLocalize } from '~/hooks' ;
2025-09-26 18:20:43 +02:00
import { DataTableSearch } from './DataTableSearch' ;
2025-09-23 23:30:27 +02:00
import { cn , logger } from '~/utils' ;
2025-09-26 18:20:43 +02:00
import { Button } from '../Button' ;
import { Label } from '../Label' ;
2025-09-23 23:30:27 +02:00
import { Spinner } from '~/svgs' ;
function DataTable < TData extends Record < string , unknown > , TValue > ( {
columns ,
data ,
className = '' ,
isLoading = false ,
isFetching = false ,
config ,
filterValue = '' ,
onFilterChange ,
defaultSort = [ ] ,
isFetchingNextPage = false ,
hasNextPage = false ,
fetchNextPage ,
onReset ,
sorting ,
onSortingChange ,
customActionsRenderer ,
} : DataTableProps < TData , TValue > ) {
const localize = useLocalize ( ) ;
const isSmallScreen = useMediaQuery ( '(max-width: 768px)' ) ;
2025-09-26 18:20:43 +02:00
2025-09-23 23:30:27 +02:00
const tableContainerRef = useRef < HTMLDivElement > ( null ) ;
const scrollTimeoutRef = useRef < number | null > ( null ) ;
const scrollRAFRef = useRef < number | null > ( null ) ;
const {
selection : { enableRowSelection = true , showCheckboxes = true } = { } ,
search : { enableSearch = true , debounce : debounceDelay = 300 } = { } ,
skeleton : { count : skeletonCount = 10 } = { } ,
2025-09-27 16:31:02 +02:00
virtualization : {
overscan = 10 ,
minRows = 50 ,
rowHeight = 56 ,
fastOverscanMultiplier = 4 ,
} = { } ,
2025-09-23 23:30:27 +02:00
} = config || { } ;
2025-09-27 16:31:02 +02:00
const virtualizationActive = data . length >= minRows ;
// Dynamic overscan adjustment for fast scroll bursts (state kept stable, minimal updates)
const [ dynamicOverscan , setDynamicOverscan ] = useState ( overscan ) ;
const lastScrollTopRef = useRef ( 0 ) ;
const lastScrollTimeRef = useRef ( performance . now ( ) ) ;
const fastScrollTimeoutRef = useRef < number | null > ( null ) ;
// Sync overscan prop changes
useEffect ( ( ) = > {
setDynamicOverscan ( overscan ) ;
} , [ overscan ] ) ;
// Cleanup timeout on unmount
useEffect ( ( ) = > {
return ( ) = > {
if ( fastScrollTimeoutRef . current ) {
clearTimeout ( fastScrollTimeoutRef . current ) ;
}
} ;
} , [ ] ) ;
2025-09-23 23:30:27 +02:00
const [ columnVisibility , setColumnVisibility ] = useState < VisibilityState > ( { } ) ;
const [ optimizedRowSelection , setOptimizedRowSelection ] = useOptimizedRowSelection ( ) ;
const [ error , setError ] = useState < Error | null > ( null ) ;
const [ searchTerm , setSearchTerm ] = useState ( filterValue ) ;
const [ internalSorting , setInternalSorting ] = useState < SortingState > ( defaultSort ) ;
2025-09-26 22:04:25 +02:00
const selectedCount = Object . keys ( optimizedRowSelection ) . length ;
const isAllSelected = useMemo (
( ) = > data . length > 0 && selectedCount === data . length ,
[ data . length , selectedCount ] ,
) ;
const isIndeterminate = selectedCount > 0 && ! isAllSelected ;
const getRowId = useCallback (
( row : TData , index? : number ) = > String ( row . id ? ? ` row- ${ index ? ? 0 } ` ) ,
[ ] ,
) ;
const selectedRows = useMemo ( ( ) = > {
if ( Object . keys ( optimizedRowSelection ) . length === 0 ) return [ ] ;
const dataMap = new Map ( data . map ( ( item , index ) = > [ getRowId ( item , index ) , item ] ) ) ;
return Object . keys ( optimizedRowSelection )
. map ( ( id ) = > dataMap . get ( id ) )
. filter ( Boolean ) as TData [ ] ;
} , [ optimizedRowSelection , data , getRowId ] ) ;
2025-09-23 23:30:27 +02:00
2025-09-26 18:20:43 +02:00
const cleanupTimers = useCallback ( ( ) = > {
if ( scrollRAFRef . current ) {
cancelAnimationFrame ( scrollRAFRef . current ) ;
scrollRAFRef . current = null ;
}
if ( scrollTimeoutRef . current ) {
clearTimeout ( scrollTimeoutRef . current ) ;
scrollTimeoutRef . current = null ;
}
} , [ ] ) ;
2025-09-23 23:30:27 +02:00
const debouncedTerm = useDebounced ( searchTerm , debounceDelay ) ;
const finalSorting = sorting ? ? internalSorting ;
const calculatedVisibility = useMemo ( ( ) = > {
const newVisibility : VisibilityState = { } ;
2025-09-26 18:20:43 +02:00
columns . forEach ( ( col ) = > {
const meta = ( col as { meta ? : { desktopOnly? : boolean } } ) . meta ;
if ( ! meta ? . desktopOnly ) return ;
const rawId =
( col as { id? : string | number ; accessorKey? : string | number } ) . id ? ?
( col as { accessorKey? : string | number } ) . accessorKey ;
if ( ( typeof rawId === 'string' || typeof rawId === 'number' ) && String ( rawId ) . length > 0 ) {
newVisibility [ String ( rawId ) ] = ! isSmallScreen ;
} else {
logger . warn (
'DataTable: A desktopOnly column is missing id/accessorKey; cannot control header visibility automatically.' ,
col ,
) ;
}
} ) ;
2025-09-23 23:30:27 +02:00
return newVisibility ;
} , [ isSmallScreen , columns ] ) ;
useEffect ( ( ) = > {
2025-09-26 18:20:43 +02:00
setColumnVisibility ( ( prev ) = > ( { . . . prev , . . . calculatedVisibility } ) ) ;
2025-09-23 23:30:27 +02:00
} , [ calculatedVisibility ] ) ;
2025-09-26 18:51:45 +02:00
const hasWarnedAboutMissingIds = useRef ( false ) ;
2025-09-23 23:30:27 +02:00
2025-09-26 18:51:45 +02:00
useEffect ( ( ) = > {
if ( data . length > 0 && ! hasWarnedAboutMissingIds . current ) {
const missing = data . filter ( ( item ) = > item . id === null || item . id === undefined ) ;
if ( missing . length > 0 ) {
logger . warn (
` DataTable Warning: ${ missing . length } data rows are missing a unique "id" property. Using index as a fallback. This can lead to unexpected behavior with selection and sorting. ` ,
{ missingCount : missing.length , sample : missing.slice ( 0 , 3 ) } ,
) ;
hasWarnedAboutMissingIds . current = true ;
}
}
} , [ data ] ) ;
2025-09-23 23:30:27 +02:00
2025-09-26 22:04:25 +02:00
const tableColumns = useMemo ( ( ) : ColumnDef < TData , TValue > [ ] = > {
2025-09-23 23:30:27 +02:00
if ( ! enableRowSelection || ! showCheckboxes ) {
2025-09-26 22:04:25 +02:00
return columns . map ( ( col ) = > col as unknown as ColumnDef < TData , TValue > ) ;
2025-09-23 23:30:27 +02:00
}
2025-09-26 22:04:25 +02:00
const selectColumn : ColumnDef < TData , TValue > = {
2025-09-23 23:30:27 +02:00
id : 'select' ,
2025-09-26 22:04:25 +02:00
header : ( ) = > {
const extraCheckboxProps = ( isIndeterminate ? { indeterminate : true } : { } ) as Record <
string ,
unknown
> ;
return (
< div
className = "flex h-full items-center justify-center"
aria - label = { localize ( 'com_ui_select_all' ) }
>
< SelectionCheckbox
checked = { isAllSelected }
onChange = { ( value ) = > {
if ( isAllSelected || ! value ) {
setOptimizedRowSelection ( { } ) ;
} else {
const allSelection = data . reduce < Record < string , boolean > > ( ( acc , item , index ) = > {
acc [ getRowId ( item , index ) ] = true ;
return acc ;
} , { } ) ;
setOptimizedRowSelection ( allSelection ) ;
}
} }
ariaLabel = { localize ( 'com_ui_select_all' ) }
{ . . . extraCheckboxProps }
/ >
< / div >
) ;
} ,
2025-09-26 19:08:25 +02:00
cell : ( { row } ) = > {
const rowDescription = row . original . name
? ` named ${ row . original . name } `
: ` at position ${ row . index + 1 } ` ;
return (
< div
className = "flex h-full items-center justify-center"
role = "button"
tabIndex = { 0 }
aria - label = { localize ( ` com_ui_select_row ` , { rowDescription } ) }
>
< SelectionCheckbox
checked = { row . getIsSelected ( ) }
onChange = { ( value ) = > row . toggleSelected ( value ) }
ariaLabel = { localize ( ` com_ui_select_row ` , { rowDescription } ) }
/ >
< / div >
) ;
} ,
2025-09-23 23:30:27 +02:00
meta : {
className : 'w-12' ,
} ,
2025-09-26 18:51:45 +02:00
} ;
2025-09-23 23:30:27 +02:00
2025-09-26 22:04:25 +02:00
return [ selectColumn , . . . columns . map ( ( col ) = > col as unknown as ColumnDef < TData , TValue > ) ] ;
2025-09-26 18:20:43 +02:00
} , [ columns , enableRowSelection , showCheckboxes , localize ] ) ;
2025-09-23 23:30:27 +02:00
2025-09-26 22:04:25 +02:00
const table = useReactTable < TData > ( {
data ,
2025-09-23 23:30:27 +02:00
columns : tableColumns ,
2025-09-26 22:04:25 +02:00
getRowId : getRowId ,
2025-09-23 23:30:27 +02:00
getCoreRowModel : getCoreRowModel ( ) ,
enableRowSelection ,
enableMultiRowSelection : true ,
manualSorting : true ,
manualFiltering : true ,
state : {
sorting : finalSorting ,
columnVisibility ,
rowSelection : optimizedRowSelection ,
} ,
onSortingChange : onSortingChange ? ? setInternalSorting ,
onColumnVisibilityChange : setColumnVisibility ,
onRowSelectionChange : setOptimizedRowSelection ,
} ) ;
const rowVirtualizer = useVirtualizer ( {
2025-09-27 16:31:02 +02:00
enabled : virtualizationActive ,
2025-09-26 22:04:25 +02:00
count : data.length ,
2025-09-23 23:30:27 +02:00
getScrollElement : ( ) = > tableContainerRef . current ,
2025-09-27 16:31:02 +02:00
getItemKey : ( index ) = > getRowId ( data [ index ] as TData , index ) ,
estimateSize : useCallback ( ( ) = > rowHeight , [ rowHeight ] ) ,
overscan : dynamicOverscan ,
2025-09-23 23:30:27 +02:00
} ) ;
const virtualRows = rowVirtualizer . getVirtualItems ( ) ;
const totalSize = rowVirtualizer . getTotalSize ( ) ;
2025-09-26 18:29:10 +02:00
const paddingTop = virtualRows [ 0 ] ? . start ? ? 0 ;
2025-09-23 23:30:27 +02:00
const paddingBottom =
virtualRows . length > 0 ? totalSize - ( virtualRows [ virtualRows . length - 1 ] ? . end ? ? 0 ) : 0 ;
const { rows } = table . getRowModel ( ) ;
const headerGroups = table . getHeaderGroups ( ) ;
const showSkeletons = isLoading || ( isFetching && ! isFetchingNextPage ) ;
const shouldShowSearch = enableSearch && onFilterChange ;
2025-09-26 22:04:25 +02:00
// useEffect(() => {
// if (data.length > 1000) {
// const cleanup = setTimeout(() => {
// rowVirtualizer.scrollToIndex(0, { align: 'start' });
// rowVirtualizer.measure();
// }, 1000);
// return () => clearTimeout(cleanup);
// }
// }, [data.length, rowVirtualizer]);
2025-09-23 23:30:27 +02:00
useEffect ( ( ) = > {
setSearchTerm ( filterValue ) ;
} , [ filterValue ] ) ;
useEffect ( ( ) = > {
if ( debouncedTerm !== filterValue && onFilterChange ) {
onFilterChange ( debouncedTerm ) ;
setOptimizedRowSelection ( { } ) ;
}
} , [ debouncedTerm , filterValue , onFilterChange , setOptimizedRowSelection ] ) ;
2025-09-27 16:31:02 +02:00
// Re-measure on key state changes that can affect layout
useEffect ( ( ) = > {
if ( ! virtualizationActive ) return ;
// With fixed rowHeight, just ensure the range recalculates
rowVirtualizer . calculateRange ( ) ;
} , [ data . length , finalSorting , columnVisibility , virtualizationActive , rowVirtualizer ] ) ;
// ResizeObserver to re-measure when container size changes
useEffect ( ( ) = > {
if ( ! virtualizationActive ) return ;
const container = tableContainerRef . current ;
if ( ! container ) return ;
const ro = new ResizeObserver ( ( ) = > {
rowVirtualizer . calculateRange ( ) ;
} ) ;
ro . observe ( container ) ;
return ( ) = > ro . disconnect ( ) ;
} , [ virtualizationActive , rowVirtualizer ] ) ;
2025-09-26 22:04:25 +02:00
const handleScroll = useMemo ( ( ) = > {
let rafId : number | null = null ;
let timeoutId : number | null = null ;
2025-09-23 23:30:27 +02:00
2025-09-26 22:04:25 +02:00
return ( ) = > {
if ( rafId ) cancelAnimationFrame ( rafId ) ;
2025-09-23 23:30:27 +02:00
2025-09-26 22:04:25 +02:00
rafId = requestAnimationFrame ( ( ) = > {
2025-09-27 16:31:02 +02:00
const container = tableContainerRef . current ;
if ( container ) {
const now = performance . now ( ) ;
const delta = Math . abs ( container . scrollTop - lastScrollTopRef . current ) ;
const dt = now - lastScrollTimeRef . current ;
if ( dt > 0 ) {
const velocity = delta / dt ; // px per ms
if (
velocity > 2 &&
virtualizationActive &&
dynamicOverscan === overscan /* only expand if not already expanded */
) {
if ( fastScrollTimeoutRef . current ) {
window . clearTimeout ( fastScrollTimeoutRef . current ) ;
}
setDynamicOverscan ( Math . min ( overscan * fastOverscanMultiplier , overscan * 8 ) ) ;
fastScrollTimeoutRef . current = window . setTimeout ( ( ) = > {
setDynamicOverscan ( ( current ) = > ( current !== overscan ? overscan : current ) ) ;
} , 160 ) ;
}
}
lastScrollTopRef . current = container . scrollTop ;
lastScrollTimeRef . current = now ;
}
2025-09-26 22:04:25 +02:00
if ( timeoutId ) clearTimeout ( timeoutId ) ;
2025-09-23 23:30:27 +02:00
2025-09-26 22:04:25 +02:00
timeoutId = window . setTimeout ( ( ) = > {
2025-09-27 16:31:02 +02:00
const loaderContainer = tableContainerRef . current ;
if ( ! loaderContainer || ! fetchNextPage || ! hasNextPage || isFetchingNextPage ) return ;
2025-09-23 23:30:27 +02:00
2025-09-27 16:31:02 +02:00
const { scrollTop , scrollHeight , clientHeight } = loaderContainer ;
2025-09-26 22:04:25 +02:00
if ( scrollTop + clientHeight >= scrollHeight - 200 ) {
fetchNextPage ( ) . finally ( ) ;
}
} , 100 ) ;
} ) ;
} ;
2025-09-27 16:31:02 +02:00
} , [
fetchNextPage ,
hasNextPage ,
isFetchingNextPage ,
overscan ,
fastOverscanMultiplier ,
virtualizationActive ,
dynamicOverscan ,
] ) ;
2025-09-23 23:30:27 +02:00
useEffect ( ( ) = > {
const scrollElement = tableContainerRef . current ;
if ( ! scrollElement ) return ;
scrollElement . addEventListener ( 'scroll' , handleScroll , { passive : true } ) ;
return ( ) = > {
scrollElement . removeEventListener ( 'scroll' , handleScroll ) ;
2025-09-26 18:20:43 +02:00
cleanupTimers ( ) ;
2025-09-23 23:30:27 +02:00
} ;
2025-09-26 18:20:43 +02:00
} , [ handleScroll , cleanupTimers ] ) ;
2025-09-23 23:30:27 +02:00
const handleReset = useCallback ( ( ) = > {
setError ( null ) ;
setOptimizedRowSelection ( { } ) ;
setSearchTerm ( '' ) ;
onReset ? . ( ) ;
} , [ onReset , setOptimizedRowSelection ] ) ;
if ( error ) {
return (
< DataTableErrorBoundary onReset = { handleReset } >
< div className = "flex flex-col items-center justify-center p-8" >
< p className = "mb-4 text-red-500" > { error . message } < / p >
2025-09-26 18:20:43 +02:00
< Button onClick = { handleReset } > { localize ( 'com_ui_retry' ) } < / Button >
2025-09-23 23:30:27 +02:00
< / div >
< / DataTableErrorBoundary >
) ;
}
return (
< div
className = { cn (
'relative flex w-full flex-col overflow-hidden rounded-lg border border-border-light bg-background' ,
'h-[calc(100vh-8rem)] max-h-[80vh]' ,
className ,
) }
2025-09-26 19:08:25 +02:00
role = "region"
aria - label = { localize ( 'com_ui_data_table' ) }
2025-09-23 23:30:27 +02:00
>
< div className = "flex w-full shrink-0 items-center gap-3 border-b border-border-light" >
{ shouldShowSearch && < DataTableSearch value = { searchTerm } onChange = { setSearchTerm } / > }
{ customActionsRenderer &&
customActionsRenderer ( {
selectedCount ,
2025-09-26 22:04:25 +02:00
selectedRows ,
table : table as unknown as TTable < ProcessedDataRow < TData > > ,
2025-09-23 23:30:27 +02:00
} ) }
< / div >
< div
ref = { tableContainerRef }
className = "overflow-anchor-none relative min-h-0 flex-1 overflow-auto will-change-scroll"
style = {
{
WebkitOverflowScrolling : 'touch' ,
overscrollBehavior : 'contain' ,
} as React . CSSProperties
}
2025-09-26 19:08:25 +02:00
role = "region"
aria - label = { localize ( 'com_ui_data_table_scroll_area' ) }
aria - describedby = { showSkeletons ? 'loading-status' : undefined }
2025-09-23 23:30:27 +02:00
>
2025-09-27 16:31:02 +02:00
< Table role = "table" aria-label = { localize ( 'com_ui_data_table' ) } aria-rowcount = { data . length } >
2025-09-23 23:30:27 +02:00
< TableHeader className = "sticky top-0 z-10 bg-surface-secondary" >
{ headerGroups . map ( ( headerGroup ) = > (
< TableRow key = { headerGroup . id } >
{ headerGroup . headers . map ( ( header ) = > {
2025-09-26 18:20:43 +02:00
const isDesktopOnly =
( header . column . columnDef . meta as { desktopOnly? : boolean } | undefined )
? . desktopOnly ? ? false ;
if ( ! header . column . getIsVisible ( ) || ( isSmallScreen && isDesktopOnly ) ) {
return null ;
}
2025-09-23 23:30:27 +02:00
const isSelectHeader = header . id === 'select' ;
const meta = header . column . columnDef . meta as { className? : string } | undefined ;
2025-09-26 19:08:25 +02:00
const canSort = header . column . getCanSort ( ) ;
2025-09-26 22:04:25 +02:00
let sortAriaLabel : string | undefined ;
if ( canSort ) {
const sortState = header . column . getIsSorted ( ) ;
let sortStateLabel = 'sortable' ;
if ( sortState === 'asc' ) {
sortStateLabel = 'ascending' ;
} else if ( sortState === 'desc' ) {
sortStateLabel = 'descending' ;
}
const headerLabel =
typeof header . column . columnDef . header === 'string'
? header . column . columnDef . header
: header . column . id ;
sortAriaLabel = ` ${ headerLabel ? ? '' } column, ${ sortStateLabel } ` ;
}
2025-09-26 19:08:25 +02:00
const handleSortingKeyDown = ( e : React.KeyboardEvent ) = > {
if ( canSort && ( e . key === 'Enter' || e . key === ' ' ) ) {
e . preventDefault ( ) ;
header . column . toggleSorting ( ) ;
}
} ;
2025-09-23 23:30:27 +02:00
return (
< TableHead
key = { header . id }
className = { cn (
'border-b border-border-light py-2' ,
isSelectHeader ? 'px-0 text-center' : 'px-3' ,
2025-09-26 19:08:25 +02:00
canSort && 'cursor-pointer hover:bg-surface-tertiary' ,
2025-09-23 23:30:27 +02:00
meta ? . className ,
) }
onClick = { header . column . getToggleSortingHandler ( ) }
2025-09-26 19:08:25 +02:00
onKeyDown = { handleSortingKeyDown }
role = { canSort ? 'button' : undefined }
tabIndex = { canSort ? 0 : undefined }
aria - label = { sortAriaLabel }
aria - sort = {
header . column . getIsSorted ( ) as
| 'ascending'
| 'descending'
| 'none'
| undefined
}
2025-09-23 23:30:27 +02:00
>
{ isSelectHeader ? (
flexRender ( header . column . columnDef . header , header . getContext ( ) )
) : (
< div className = "flex items-center gap-2" >
{ flexRender ( header . column . columnDef . header , header . getContext ( ) ) }
2025-09-26 19:08:25 +02:00
{ canSort && (
< span className = "text-text-primary" aria-hidden = "true" >
2025-09-23 23:30:27 +02:00
{ {
asc : < ArrowUp className = "size-4 text-text-primary" / > ,
desc : < ArrowDown className = "size-4 text-text-primary" / > ,
} [ header . column . getIsSorted ( ) as string ] ? ? (
< ArrowDownUp className = "size-4 text-text-primary" / >
) }
< / span >
) }
< / div >
) }
< / TableHead >
) ;
} ) }
< / TableRow >
) ) }
< / TableHeader >
< TableBody >
{ showSkeletons ? (
< SkeletonRows
count = { skeletonCount }
2025-09-26 18:20:43 +02:00
columns = { tableColumns as ColumnDef < Record < string , unknown > > [ ] }
2025-09-23 23:30:27 +02:00
/ >
2025-09-27 16:31:02 +02:00
) : virtualizationActive ? (
2025-09-23 23:30:27 +02:00
< >
{ paddingTop > 0 && (
< TableRow aria-hidden = "true" >
< TableCell
colSpan = { tableColumns . length }
style = { { height : paddingTop , padding : 0 , border : 0 } }
/ >
< / TableRow >
) }
{ virtualRows . map ( ( virtualRow ) = > {
const row = rows [ virtualRow . index ] ;
if ( ! row ) return null ;
return (
< MemoizedTableRow
2025-09-27 16:31:02 +02:00
key = { virtualRow . key }
2025-09-26 18:51:45 +02:00
row = { row as unknown as Row < TData > }
2025-09-23 23:30:27 +02:00
virtualIndex = { virtualRow . index }
2025-09-27 16:31:02 +02:00
selected = { row . getIsSelected ( ) }
style = { { height : rowHeight } }
2025-09-23 23:30:27 +02:00
/ >
) ;
} ) }
{ paddingBottom > 0 && (
< TableRow aria-hidden = "true" >
< TableCell
colSpan = { tableColumns . length }
style = { { height : paddingBottom , padding : 0 , border : 0 } }
/ >
< / TableRow >
) }
< / >
2025-09-27 16:31:02 +02:00
) : (
rows . map ( ( row ) = > (
< MemoizedTableRow
key = { getRowId ( row . original as TData , row . index ) }
row = { row as unknown as Row < TData > }
virtualIndex = { row . index }
selected = { row . getIsSelected ( ) }
style = { { height : rowHeight } }
/ >
) )
2025-09-23 23:30:27 +02:00
) }
{ isFetchingNextPage && (
< TableRow >
2025-09-26 19:08:25 +02:00
< TableCell
colSpan = { tableColumns . length }
className = "p-4 text-center"
id = "loading-status"
role = "status"
aria - live = "polite"
>
2025-09-23 23:30:27 +02:00
< div className = "flex items-center justify-center gap-2" >
2025-09-26 19:08:25 +02:00
< Spinner className = "h-5 w-5" aria-hidden = "true" / >
< span className = "sr-only" > { localize ( 'com_ui_loading_more_data' ) } < / span >
2025-09-23 23:30:27 +02:00
< / div >
< / TableCell >
< / TableRow >
) }
< / TableBody >
< / Table >
{ ! isLoading && ! showSkeletons && rows . length === 0 && (
2025-09-26 19:08:25 +02:00
< div
className = "flex flex-col items-center justify-center py-12"
role = "status"
aria - live = "polite"
>
2025-09-23 23:30:27 +02:00
< Label className = "text-center text-text-secondary" >
2025-09-26 19:08:25 +02:00
{ searchTerm ? localize ( 'com_ui_no_search_results' ) : localize ( 'com_ui_no_data' ) }
2025-09-23 23:30:27 +02:00
< / Label >
< / div >
) }
< / div >
< / div >
) ;
}
export default DataTable ;