|
| 1 | +# UniversalDataGrid Design |
| 2 | + |
| 3 | +Implements issue #611: "Build a UniversalDataGrid Component" — a reusable |
| 4 | +enterprise-grade data grid with virtualization, sorting, filtering, column |
| 5 | +pinning, export, and keyboard accessibility. |
| 6 | + |
| 7 | +## Architecture |
| 8 | + |
| 9 | +Built on `@tanstack/react-table` (headless sorting/filtering/column-pinning |
| 10 | +state) and `@tanstack/react-virtual` (row virtualization). Both are new |
| 11 | +dependencies; `@tanstack/react-query` is already used elsewhere in this repo, |
| 12 | +so this keeps the grid ecosystem consistent. |
| 13 | + |
| 14 | +Self-contained, generic, typed component following the pattern established by |
| 15 | +`ConferenceFloorMap`/`EventCapacityIndicator`: its own folder, its own mock |
| 16 | +data, wired into `app/demo/page.tsx`. It does not replace or touch the |
| 17 | +existing `DataTable.tsx`/`ReusableTable.tsx` components — those are out of |
| 18 | +scope for this issue. |
| 19 | + |
| 20 | +## Files |
| 21 | + |
| 22 | +- `components/UniversalDataGrid/types.ts` — `UniversalDataGridColumn<T>` and |
| 23 | + `UniversalDataGridProps<T>` generic types. |
| 24 | +- `components/UniversalDataGrid/mockAttendees.ts` — 5,000 generated |
| 25 | + synthetic attendee rows (id, name, email, ticketType, status, |
| 26 | + registeredAt, checkedIn) plus the column definitions for the demo. |
| 27 | +- `components/UniversalDataGrid/useCsvExport.ts` — hook that serializes a |
| 28 | + given rowset to CSV and triggers a browser download. |
| 29 | +- `components/UniversalDataGrid/UniversalDataGrid.tsx` — the grid. |
| 30 | +- `components/UniversalDataGrid/index.ts` — barrel export. |
| 31 | + |
| 32 | +## Component API |
| 33 | + |
| 34 | +```ts |
| 35 | +interface UniversalDataGridColumn<T> { |
| 36 | + id: string; |
| 37 | + header: string; |
| 38 | + accessor: (row: T) => string | number; |
| 39 | + width?: number; |
| 40 | + enableSort?: boolean; // default true |
| 41 | + enableFilter?: boolean; // default true |
| 42 | +} |
| 43 | + |
| 44 | +interface UniversalDataGridProps<T> { |
| 45 | + data: T[]; |
| 46 | + columns: UniversalDataGridColumn<T>[]; |
| 47 | + rowHeight?: number; // default 40, used by the virtualizer |
| 48 | + height?: number; // default 480, px height of the scroll viewport |
| 49 | + getRowId: (row: T) => string; |
| 50 | + className?: string; |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +## Interaction Model |
| 55 | + |
| 56 | +- **Sorting**: clicking a header cycles asc → desc → none. Driven by |
| 57 | + TanStack Table's `sorting` state; only one column sorts at a time. |
| 58 | +- **Filtering**: a small text input under each filterable header does |
| 59 | + substring matching against that column's accessed value, debounced |
| 60 | + ~200ms, driven by TanStack Table's `columnFilters` state. |
| 61 | +- **Column pinning**: a pin icon in each header opens a small menu with |
| 62 | + "Pin left" / "Pin right" / "Unpin". Pinned columns use TanStack Table's |
| 63 | + pinning API and get `position: sticky` with a shadow divider at the |
| 64 | + pinned/unpinned boundary. |
| 65 | +- **Export**: a toolbar button exports the currently filtered+sorted |
| 66 | + rowset (respecting active filters and sort order, but not virtualization |
| 67 | + — all matching rows are exported, not just the rendered window) as a |
| 68 | + CSV file via `useCsvExport`. |
| 69 | +- **Virtualization**: the row body uses `@tanstack/react-virtual`'s |
| 70 | + `useVirtualizer` with a fixed `rowHeight`, rendering only the rows |
| 71 | + currently in (or near) the viewport inside a scrollable container of |
| 72 | + `height`. |
| 73 | +- **Keyboard accessibility**: the grid follows the WAI-ARIA grid pattern. |
| 74 | + `role="grid"` on the container, `role="row"`/`role="columnheader"`/ |
| 75 | + `role="gridcell"` on the relevant elements. Arrow keys move a |
| 76 | + roving-tabindex focused-cell indicator between cells (Up/Down/Left/ |
| 77 | + Right); Enter/Space on a focused header cell triggers sort; Tab reaches |
| 78 | + the toolbar (export button) and each column's filter input in DOM |
| 79 | + order, before/after the grid body. |
| 80 | + |
| 81 | +## Mock Data |
| 82 | + |
| 83 | +5,000 generated attendee rows, large enough that virtualization's |
| 84 | +performance benefit is visually obvious (an unvirtualized 5,000-row table |
| 85 | +would noticeably jank on scroll). Deterministic generation (seeded index-based |
| 86 | +values, not `Math.random()`), so the dataset is stable across renders/tests. |
| 87 | + |
| 88 | +## Testing Approach |
| 89 | + |
| 90 | +No `.test.tsx` file — this repo has no wired-up test runner (no `test` |
| 91 | +script, no jest/vitest config), matching the precedent set by |
| 92 | +`ConferenceFloorMap`. Verification is via `tsc --noEmit`, `eslint`, and a |
| 93 | +live browser check (Playwright-driven): sort a column, filter a column, |
| 94 | +pin a column, export CSV, and confirm keyboard arrow-key navigation moves |
| 95 | +focus between cells with no console errors. |
| 96 | + |
| 97 | +## Out of Scope |
| 98 | + |
| 99 | +- Replacing `DataTable.tsx` or `ReusableTable.tsx` call sites. |
| 100 | +- Server-side pagination/sorting/filtering (all data is client-side, in |
| 101 | + keeping with the mock-data-demo-component convention). |
| 102 | +- Row selection/editing. |
| 103 | +- Excel (.xlsx) export — CSV only. |
0 commit comments