@@ -85,8 +85,10 @@ export interface DataTableProps<TData, TSectionMeta = unknown> {
8585 headerClassName ?: string ;
8686 /** Extra classes applied to the <thead> element (e.g. "file-browser-thead" for a shadow-based bottom divider) */
8787 theadClassName ?: string ;
88- rowClassName ?: string | ( ( item : TData ) => string ) ;
88+ rowClassName ?: string | ( ( item : TData , index : number ) => string ) ;
8989 sectionClassName ?: string | ( ( section : Section < TData , TSectionMeta > ) => string ) ;
90+ /** Whether a given row should show hover/pointer styles. Defaults to !!onRowClick for all rows. */
91+ isRowInteractive ?: ( row : TData ) => boolean ;
9092 columnSizeConfigs ?: readonly ColumnSizeConfig [ ] ;
9193 columnSizingPreferences ?: ColumnSizingPreferences ;
9294 onColumnSizingPreferenceChange ?: ( columnId : string , preference : ColumnSizingPreference ) => void ;
@@ -128,7 +130,6 @@ function DataTableInner<TData, TSectionMeta = unknown>({
128130 onLoadMore,
129131 isFetchingNextPage,
130132 totalCount,
131- // Row heights use canonical constants from @/lib/config
132133 rowHeight = TABLE_ROW_HEIGHTS . NORMAL ,
133134 sectionHeight = TABLE_ROW_HEIGHTS . SECTION ,
134135 className,
@@ -151,6 +152,7 @@ function DataTableInner<TData, TSectionMeta = unknown>({
151152 suspendResize,
152153 resizeCompleteEvent,
153154 registerLayoutStableCallback,
155+ isRowInteractive,
154156} : DataTableProps < TData , TSectionMeta > ) {
155157 const scrollRef = useRef < HTMLDivElement > ( null ) ;
156158 const tableElementRef = useRef < HTMLTableElement > ( null ) ;
@@ -178,7 +180,6 @@ function DataTableInner<TData, TSectionMeta = unknown>({
178180 return columns
179181 . map ( ( c ) => {
180182 if ( typeof c . id === "string" ) return c . id ;
181- // AccessorKeyColumnDef has accessorKey property
182183 if ( "accessorKey" in c && c . accessorKey ) return String ( c . accessorKey ) ;
183184 return "" ;
184185 } )
@@ -199,38 +200,20 @@ function DataTableInner<TData, TSectionMeta = unknown>({
199200
200201 const visibleColumnCount = visibleColumnIds . length ;
201202
202- const columnMinSizes = useMemo ( ( ) => {
203- const sizes : Record < string , number > = { } ;
204- for ( const col of columns ) {
205- const colId = col . id ?? ( "accessorKey" in col && col . accessorKey ? String ( col . accessorKey ) : "" ) ;
206- if ( colId && col . minSize != null ) {
207- sizes [ colId ] = col . minSize ;
208- }
209- }
210- return sizes ;
211- } , [ columns ] ) ;
203+ const { columnMinSizes, columnInitialSizes, columnResizability } = useMemo ( ( ) => {
204+ const mins : Record < string , number > = { } ;
205+ const initials : Record < string , number > = { } ;
206+ const resizability : Record < string , boolean > = { } ;
212207
213- const columnInitialSizes = useMemo ( ( ) => {
214- const sizes : Record < string , number > = { } ;
215208 for ( const col of columns ) {
216209 const colId = col . id ?? ( "accessorKey" in col && col . accessorKey ? String ( col . accessorKey ) : "" ) ;
217- if ( colId && col . size != null ) {
218- sizes [ colId ] = col . size ;
219- }
210+ if ( ! colId ) continue ;
211+ if ( col . minSize != null ) mins [ colId ] = col . minSize ;
212+ if ( col . size != null ) initials [ colId ] = col . size ;
213+ resizability [ colId ] = col . enableResizing !== false ;
220214 }
221- return sizes ;
222- } , [ columns ] ) ;
223215
224- const columnResizability = useMemo ( ( ) => {
225- const resizability : Record < string , boolean > = { } ;
226- for ( const col of columns ) {
227- const colId = col . id ?? ( "accessorKey" in col && col . accessorKey ? String ( col . accessorKey ) : "" ) ;
228- if ( colId ) {
229- // enableResizing defaults to true if not specified
230- resizability [ colId ] = col . enableResizing !== false ;
231- }
232- }
233- return resizability ;
216+ return { columnMinSizes : mins , columnInitialSizes : initials , columnResizability : resizability } ;
234217 } , [ columns ] ) ;
235218
236219 const showSkeleton = isLoading && allItems . length === 0 ;
@@ -252,19 +235,34 @@ function DataTableInner<TData, TSectionMeta = unknown>({
252235 registerLayoutStableCallback,
253236 } ) ;
254237
255- // Track previous data length to detect empty → populated transitions
238+ // Toggle `is-scrolling` class to suppress row-position transitions during scroll.
239+ // Removed 150ms after the last scroll event so expand/collapse animations work.
240+ useEffect ( ( ) => {
241+ const scrollEl = scrollRef . current ;
242+ if ( ! scrollEl ) return ;
243+ let timeoutId : ReturnType < typeof setTimeout > ;
244+ const onScroll = ( ) => {
245+ scrollEl . classList . add ( "is-scrolling" ) ;
246+ clearTimeout ( timeoutId ) ;
247+ timeoutId = setTimeout ( ( ) => scrollEl . classList . remove ( "is-scrolling" ) , 150 ) ;
248+ } ;
249+ scrollEl . addEventListener ( "scroll" , onScroll , { passive : true } ) ;
250+ return ( ) => {
251+ scrollEl . removeEventListener ( "scroll" , onScroll ) ;
252+ clearTimeout ( timeoutId ) ;
253+ } ;
254+ } , [ ] ) ;
255+
256+ // Recalculate column widths when data first arrives (empty -> populated)
256257 const prevDataLength = usePrevious ( allItems . length ) ;
257-
258- // When transitioning from empty (0 items) to populated (>0 items),
259- // trigger column recalculation to ensure columns fill available space
258+ const { recalculate } = columnSizingHook ;
260259 useEffect ( ( ) => {
261260 if ( prevDataLength === 0 && allItems . length > 0 ) {
262- // Use RAF to ensure DOM has updated before recalculating
263261 requestAnimationFrame ( ( ) => {
264- columnSizingHook . recalculate ( ) ;
262+ recalculate ( ) ;
265263 } ) ;
266264 }
267- } , [ prevDataLength , allItems . length , columnSizingHook ] ) ;
265+ } , [ prevDataLength , allItems . length , recalculate ] ) ;
268266
269267 // eslint-disable-next-line react-hooks/incompatible-library -- TanStack Table returns unstable functions by design
270268 const table = useReactTable ( {
@@ -316,7 +314,6 @@ function DataTableInner<TData, TSectionMeta = unknown>({
316314 useVirtualizedTable < TData , TSectionMeta > ( {
317315 items : sections ? undefined : data ,
318316 sections,
319- getRowId,
320317 scrollRef,
321318 rowHeight,
322319 sectionHeight,
@@ -369,7 +366,9 @@ function DataTableInner<TData, TSectionMeta = unknown>({
369366
370367 const rowNavigation = useRowNavigation ( {
371368 rowCount : virtualItemCount ,
372- visibleRowCount : Math . floor ( 600 / rowHeight ) ,
369+ visibleRowCount : scrollRef . current
370+ ? Math . max ( 1 , Math . floor ( scrollRef . current . clientHeight / rowHeight ) )
371+ : Math . floor ( 600 / rowHeight ) ,
373372 onRowActivate : useCallback (
374373 ( virtualIndex : number ) => {
375374 const item = getItem ( virtualIndex ) ;
@@ -511,7 +510,6 @@ function DataTableInner<TData, TSectionMeta = unknown>({
511510
512511 const colIndex = headerIndex + 1 ;
513512
514- // Get custom header className from column meta (dependency injection)
515513 const headerClassName = header . column . columnDef . meta ?. headerClassName ;
516514
517515 if ( isFixed ) {
@@ -577,6 +575,7 @@ function DataTableInner<TData, TSectionMeta = unknown>({
577575 onRowKeyDown = { rowNavigation . handleRowKeyDown }
578576 measureElement = { measureElement }
579577 compact = { compact }
578+ isRowInteractive = { isRowInteractive }
580579 />
581580 </ table >
582581
@@ -635,5 +634,4 @@ function DataTableInner<TData, TSectionMeta = unknown>({
635634 ) ;
636635}
637636
638- // Memoize with generic type preservation
639637export const DataTable = memo ( DataTableInner ) as typeof DataTableInner ;
0 commit comments