Phase 2: Code-Splitting + Virtual Scrolling (Tasks 2.1-2.7)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo, useRef, useCallback } from 'react';
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
SortingState,
|
||||
ColumnFiltersState,
|
||||
} from '@tanstack/react-table';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import clsx from 'clsx';
|
||||
import { Pagination } from '@/components/ui/Pagination';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -30,6 +31,7 @@ export interface DataGridProps<T> {
|
||||
globalFilter?: string;
|
||||
onGlobalFilterChange?: (value: string) => void;
|
||||
testId?: string;
|
||||
virtualized?: boolean;
|
||||
}
|
||||
|
||||
export function DataGrid<T extends Record<string, any>>({
|
||||
@@ -44,9 +46,11 @@ export function DataGrid<T extends Record<string, any>>({
|
||||
loading = false,
|
||||
enableSorting = true,
|
||||
testId,
|
||||
virtualized = true,
|
||||
}: DataGridProps<T>) {
|
||||
const { t } = useTranslation();
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
@@ -60,11 +64,59 @@ export function DataGrid<T extends Record<string, any>>({
|
||||
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
|
||||
// Auto-skip virtualization for small datasets
|
||||
const shouldVirtualize = virtualized && data.length >= 50;
|
||||
|
||||
const rows = table.getRowModel().rows;
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: shouldVirtualize ? rows.length : 0,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: () => 53, // approx row height
|
||||
overscan: 10,
|
||||
enabled: shouldVirtualize,
|
||||
});
|
||||
|
||||
const virtualRows = shouldVirtualize ? rowVirtualizer.getVirtualItems() : [];
|
||||
const totalHeight = shouldVirtualize ? rowVirtualizer.getTotalSize() : 0;
|
||||
const paddingTop = virtualRows.length > 0 ? virtualRows[0].start : 0;
|
||||
const paddingBottom = virtualRows.length > 0 ? totalHeight - virtualRows[virtualRows.length - 1].end : 0;
|
||||
|
||||
const renderRow = useCallback(
|
||||
(row: typeof rows[number]) => (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={clsx(
|
||||
'hover:bg-secondary-50 motion-safe:transition-colors',
|
||||
onRowClick && 'cursor-pointer'
|
||||
)}
|
||||
onClick={onRowClick ? () => onRowClick(row.original) : undefined}
|
||||
tabIndex={onRowClick ? 0 : undefined}
|
||||
onKeyDown={onRowClick ? (e) => {
|
||||
if (e.key === 'Enter') onRowClick(row.original);
|
||||
} : undefined}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td key={cell.id} className="px-6 py-4 text-sm text-secondary-900">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
),
|
||||
[onRowClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-secondary-200 overflow-hidden" data-testid={testId}>
|
||||
<div className="overflow-x-auto" role="region" aria-label="Data grid">
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="overflow-x-auto"
|
||||
role="region"
|
||||
aria-label="Data grid"
|
||||
style={shouldVirtualize ? { maxHeight: '70vh', overflowY: 'auto' } : undefined}
|
||||
>
|
||||
<table className="min-w-full divide-y divide-secondary-200">
|
||||
<thead className="bg-secondary-50">
|
||||
<thead className="bg-secondary-50 sticky top-0 z-10">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
@@ -114,33 +166,31 @@ export function DataGrid<T extends Record<string, any>>({
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
) : table.getRowModel().rows.length === 0 ? (
|
||||
) : rows.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="px-6 py-8 text-center text-secondary-500">
|
||||
{emptyMessage || t('table.empty')}
|
||||
</td>
|
||||
</tr>
|
||||
) : shouldVirtualize ? (
|
||||
<>
|
||||
{paddingTop > 0 && (
|
||||
<tr key="__padding-top" style={{ height: paddingTop }}>
|
||||
<td colSpan={columns.length} style={{ padding: 0, border: 'none' }} />
|
||||
</tr>
|
||||
)}
|
||||
{virtualRows.map((virtualRow) => {
|
||||
const row = rows[virtualRow.index];
|
||||
return renderRow(row);
|
||||
})}
|
||||
{paddingBottom > 0 && (
|
||||
<tr key="__padding-bottom" style={{ height: paddingBottom }}>
|
||||
<td colSpan={columns.length} style={{ padding: 0, border: 'none' }} />
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={clsx(
|
||||
'hover:bg-secondary-50 motion-safe:transition-colors',
|
||||
onRowClick && 'cursor-pointer'
|
||||
)}
|
||||
onClick={onRowClick ? () => onRowClick(row.original) : undefined}
|
||||
tabIndex={onRowClick ? 0 : undefined}
|
||||
onKeyDown={onRowClick ? (e) => {
|
||||
if (e.key === 'Enter') onRowClick(row.original);
|
||||
} : undefined}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td key={cell.id} className="px-6 py-4 text-sm text-secondary-900">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
rows.map((row) => renderRow(row))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user