440 lines
13 KiB
Markdown
440 lines
13 KiB
Markdown
# RFC: DataTable
|
|
|
|
## Status
|
|
|
|
Proposed
|
|
|
|
## Summary
|
|
|
|
`DataTable` is the next high-value advanced pattern for `@ai-ui/ui`, but it should not ship as a giant "kitchen sink" component.
|
|
|
|
The recommended path is:
|
|
|
|
1. adopt a headless row-model dependency for the hard table state problems
|
|
2. keep the public API source-owned and design-system specific
|
|
3. ship a narrow core table first
|
|
4. layer richer behaviors only after the base contract stabilizes
|
|
|
|
The recommended dependency choice is `@tanstack/react-table`, but only as an internal implementation detail. Consumers should build against `@ai-ui/ui`, not against TanStack APIs directly.
|
|
|
|
## Why now
|
|
|
|
The repo has already completed most of the preconditions that the roadmap called out before advanced patterns:
|
|
|
|
- token system is in place
|
|
- core component layer is in place
|
|
- Storybook coverage exists across the current primitives
|
|
- interaction and smoke coverage are present
|
|
- `Sheet` and `EmptyState` are now available as adjacent workflow patterns
|
|
|
|
Relevant current building blocks already exist in `packages/ui`:
|
|
|
|
- input and form controls: `Input`, `Select`, `Checkbox`, `RadioGroup`, `Switch`, `Form`
|
|
- structure and feedback: `Card`, `Badge`, `Alert`, `Separator`, `Skeleton`, `Progress`
|
|
- overflow and contextual actions: `DropdownMenu`, `Popover`, `Tooltip`, `Sheet`, `Dialog`, `Toast`
|
|
- empty and first-run states: `EmptyState`
|
|
|
|
That means the main missing piece for list-heavy product surfaces is not another primitive. It is a stable data presentation pattern.
|
|
|
|
## Problem statement
|
|
|
|
Today the design system can express forms, overlays, command/search, and empty states, but it cannot yet express a reusable operational list surface such as:
|
|
|
|
- release queues
|
|
- approval backlogs
|
|
- rollout logs
|
|
- audit result lists
|
|
- environment health tables
|
|
- reviewer assignments
|
|
|
|
Without a `DataTable` pattern, application code will drift into one-off table wrappers with inconsistent:
|
|
|
|
- toolbar layout
|
|
- filter/search interactions
|
|
- row selection behavior
|
|
- loading and empty states
|
|
- keyboard behavior
|
|
- sticky headers and scrolling decisions
|
|
- bulk action affordances
|
|
- pagination treatment
|
|
|
|
This is exactly the kind of "parallel wrapper" problem the roadmap warns against.
|
|
|
|
## Non-goals
|
|
|
|
The first implementation should **not** attempt to solve every table problem.
|
|
|
|
Explicit non-goals for the first slice:
|
|
|
|
- virtualization
|
|
- column resizing
|
|
- column reordering
|
|
- nested tree rows
|
|
- grouped/aggregated rows
|
|
- drag and drop
|
|
- inline cell editing
|
|
- Excel-style spreadsheet behavior
|
|
- server transport concerns
|
|
- generic charting or pivot-table behavior
|
|
|
|
Those can come later if real product surfaces prove they are needed.
|
|
|
|
## Current repo constraints
|
|
|
|
This RFC is anchored in the current repo, not an idealized future state.
|
|
|
|
### Architectural constraints
|
|
|
|
- Components are source-owned and exported from `packages/ui/src/index.ts`
|
|
- Styling is token-first and stateful styling flows through stable `data-*` attributes
|
|
- Slot naming is already standardized in `packages/ui/src/lib/contracts.ts`
|
|
- Motion should be purposeful and reduced-motion safe
|
|
- Stories are expected to explain anatomy and behavior, not just render a demo
|
|
- QA already uses `Vitest`, Storybook interaction coverage, and Playwright smoke
|
|
|
|
### Dependency constraints
|
|
|
|
Current `packages/ui/package.json` has no table model dependency, no date library, and no grid engine.
|
|
|
|
That means the first `DataTable` implementation must either:
|
|
|
|
- hand-roll row modeling, sorting, selection, and visibility management, or
|
|
- adopt a focused headless dependency
|
|
|
|
Hand-rolling is not recommended for this repo stage.
|
|
|
|
## Recommendation
|
|
|
|
Use `@tanstack/react-table` internally for the first `DataTable` implementation.
|
|
|
|
### Why TanStack Table
|
|
|
|
It solves the complex headless data problems we do not want to rediscover:
|
|
|
|
- row modeling
|
|
- sorting state
|
|
- filtering state
|
|
- pagination state
|
|
- selection state
|
|
- column visibility
|
|
- stable cell/header modeling
|
|
|
|
This matches the repo's principle of using a strong interaction/state base under a source-owned UI layer, the same way Radix underpins overlays and controls.
|
|
|
|
### Why not build it from scratch
|
|
|
|
Building even a "simple" table pattern from scratch will immediately force the repo to own:
|
|
|
|
- sorting semantics
|
|
- accessor APIs
|
|
- row identity rules
|
|
- selection bookkeeping
|
|
- controlled vs uncontrolled state design
|
|
- column metadata normalization
|
|
|
|
That is too much API design surface for a first table release.
|
|
|
|
### Why not expose TanStack directly
|
|
|
|
Exposing TanStack types and concepts as the public API would weaken the repo's current direction:
|
|
|
|
- it would leak a third-party mental model into consumer code
|
|
- it would make docs look like a wrapper around someone else's library
|
|
- it would make future refactors harder
|
|
|
|
The public contract should stay `@ai-ui/ui` first. TanStack should remain an implementation detail wherever possible, with any unavoidable type exposure deliberately minimized.
|
|
|
|
## Proposed scope: Stage 1 core
|
|
|
|
The first shipping slice should cover the operational table use case, not the spreadsheet use case.
|
|
|
|
### Must-have behaviors
|
|
|
|
- typed columns
|
|
- sortable columns
|
|
- optional row selection
|
|
- optional client-side text filter
|
|
- empty state rendering
|
|
- loading state rendering
|
|
- pagination controls
|
|
- column-level cell formatting
|
|
- row-level actions slot
|
|
|
|
### Must-have supporting surfaces
|
|
|
|
- toolbar for search, filters, and view actions
|
|
- sticky or visually distinct header treatment
|
|
- bulk selection affordance when selection is enabled
|
|
- responsive overflow strategy
|
|
|
|
## Proposed public API shape
|
|
|
|
The API should be compositional and stable, following the repo's current component contract.
|
|
|
|
### Root component
|
|
|
|
```tsx
|
|
<DataTable
|
|
columns={columns}
|
|
rows={rows}
|
|
getRowId={(row) => row.id}
|
|
loading={loading}
|
|
empty={<DataTableEmpty ... />}
|
|
searchValue={search}
|
|
onSearchValueChange={setSearch}
|
|
selection={selection}
|
|
onSelectionChange={setSelection}
|
|
sorting={sorting}
|
|
onSortingChange={setSorting}
|
|
/>
|
|
```
|
|
|
|
### Suggested slot family
|
|
|
|
- `DataTable`
|
|
- `DataTableToolbar`
|
|
- `DataTableSearch`
|
|
- `DataTableFilters`
|
|
- `DataTableContent`
|
|
- `DataTableTable`
|
|
- `DataTableHeader`
|
|
- `DataTableHeaderCell`
|
|
- `DataTableBody`
|
|
- `DataTableRow`
|
|
- `DataTableCell`
|
|
- `DataTableEmpty`
|
|
- `DataTableLoading`
|
|
- `DataTablePagination`
|
|
- `DataTableSelectionBar`
|
|
|
|
This is intentionally a pattern family, not a single monolith.
|
|
|
|
### Suggested stable slots
|
|
|
|
The following `data-slot` names should exist in the first implementation:
|
|
|
|
- `root`
|
|
- `toolbar`
|
|
- `input`
|
|
- `content`
|
|
- `table`
|
|
- `header`
|
|
- `row`
|
|
- `cell`
|
|
- `empty`
|
|
- `pagination`
|
|
- `actions`
|
|
|
|
Additional slot names should only be added when they create a durable styling or testing hook.
|
|
|
|
### Suggested stable state surface
|
|
|
|
The following public state hooks are likely worth exposing:
|
|
|
|
- `data-loading`
|
|
- `data-empty`
|
|
- `data-selected`
|
|
- `data-sort`
|
|
- `data-density`
|
|
|
|
`data-state` should still be used for finite row/header states where that is the clearest representation.
|
|
|
|
## Proposed column model
|
|
|
|
The public API should accept a narrow column definition owned by this repo, even if it is internally adapted to TanStack.
|
|
|
|
Example direction:
|
|
|
|
```ts
|
|
type DataTableColumn<TData> = {
|
|
id: string;
|
|
header: ReactNode | ((column: DataTableColumnContext<TData>) => ReactNode);
|
|
cell: (row: TData) => ReactNode;
|
|
accessor?: keyof TData | ((row: TData) => unknown);
|
|
sortable?: boolean;
|
|
align?: "start" | "center" | "end";
|
|
width?: number | string;
|
|
priority?: "primary" | "supporting" | "low";
|
|
};
|
|
```
|
|
|
|
Key point:
|
|
|
|
- keep the first public column shape small
|
|
- do not surface every TanStack option
|
|
- do not make users learn a second full configuration language on day one
|
|
|
|
## Composition model
|
|
|
|
The table should integrate naturally with the patterns the repo already ships.
|
|
|
|
### DataTable should compose with
|
|
|
|
- `Input` for search
|
|
- `Select` and `DropdownMenu` for filters and view options
|
|
- `Checkbox` for row selection
|
|
- `Button` for primary and secondary actions
|
|
- `Badge` for row metadata
|
|
- `Tooltip` for truncated or status-heavy cells
|
|
- `Popover` for compact detail reveal
|
|
- `Sheet` for row inspection or bulk edit side panels
|
|
- `EmptyState` for no-results and first-run moments
|
|
- `Skeleton` for loading rows
|
|
|
|
### DataTable should not try to replace
|
|
|
|
- `Sheet` detail workflows
|
|
- `Dialog` destructive confirmations
|
|
- `EmptyState` standalone onboarding/empty moments
|
|
- `Form` validation and edit flows
|
|
|
|
The table is the list surface. Other patterns handle adjacent work.
|
|
|
|
## Accessibility requirements
|
|
|
|
The first release should treat accessibility as a primary contract, not a follow-up.
|
|
|
|
### Baseline requirements
|
|
|
|
- semantic table structure for the standard grid use case
|
|
- keyboard reachable sort controls
|
|
- keyboard reachable row selection controls
|
|
- visible focus styling on row actions and interactive header controls
|
|
- accessible labeling for search, filters, and pagination controls
|
|
- empty and loading states that remain understandable to screen readers
|
|
- no motion dependency for critical state changes
|
|
|
|
### First-release accessibility decisions
|
|
|
|
- Prefer semantic HTML table markup for the default implementation
|
|
- Do not start with `role="grid"` unless we truly need spreadsheet-like keyboard control
|
|
- Treat sorting controls as buttons inside header cells
|
|
- Keep row selection explicit with `Checkbox`, not hidden click-to-select rows
|
|
|
|
This fits the repo's current accessibility posture better than jumping straight to a complex grid interaction model.
|
|
|
|
## QA expectations
|
|
|
|
The table should meet the repo's current QA bar from the first release.
|
|
|
|
### Unit and interaction coverage
|
|
|
|
Minimum expected coverage:
|
|
|
|
- renders header, rows, and cells with stable slots
|
|
- sortable columns update controlled and uncontrolled sort state
|
|
- selection toggles update row state and bulk action surface
|
|
- loading state renders skeleton or loading rows
|
|
- empty state renders when no rows remain after filtering
|
|
- search/filter plumbing updates the rendered row set
|
|
- pagination changes page and respects bounds
|
|
|
|
### Storybook coverage
|
|
|
|
Minimum story recipe:
|
|
|
|
- `Playground`
|
|
- `States`
|
|
- `Anatomy`
|
|
- `Accessibility`
|
|
|
|
Likely additional stories:
|
|
|
|
- `Selection`
|
|
- `Empty and Loading`
|
|
|
|
### Playwright smoke
|
|
|
|
At least one smoke scenario should cover:
|
|
|
|
- table renders
|
|
- search narrows rows
|
|
- sorting changes row order
|
|
- selecting rows reveals a bulk-action affordance
|
|
- opening row detail in `Sheet` still works
|
|
|
|
## Release plan
|
|
|
|
### Stage 0: RFC and composition rehearsal
|
|
|
|
- finalize API direction in this RFC
|
|
- build one realistic docs composition page that uses a table-like operational surface
|
|
- identify missing supporting primitives before table code starts
|
|
|
|
### Stage 1: Headless core
|
|
|
|
Ship a narrow `DataTable` that supports:
|
|
|
|
- typed columns
|
|
- sorting
|
|
- selection
|
|
- search
|
|
- pagination
|
|
- loading and empty states
|
|
|
|
No virtualization, resizing, pinning, or editing.
|
|
|
|
### Stage 2: Workflow integration
|
|
|
|
Add only after Stage 1 stabilizes:
|
|
|
|
- row action menu patterns
|
|
- bulk action bar
|
|
- column visibility menu
|
|
- row details opening in `Sheet`
|
|
|
|
### Stage 3: Scale features
|
|
|
|
Only if justified by real product usage:
|
|
|
|
- server-driven pagination hooks
|
|
- column pinning
|
|
- virtualization
|
|
- advanced filters
|
|
|
|
## Likely file layout
|
|
|
|
When implementation begins, the initial table slice should probably live in:
|
|
|
|
```txt
|
|
packages/ui/src/components/data-table.tsx
|
|
packages/ui/src/components/data-table.variants.ts
|
|
packages/ui/src/components/data-table.test.tsx
|
|
apps/docs/src/components/data-table.stories.tsx
|
|
```
|
|
|
|
If the implementation grows into a larger family, the repo may eventually want a dedicated component folder, but the first slice should stay consistent with the current flat component layout.
|
|
|
|
## Dependency recommendation
|
|
|
|
### Recommended
|
|
|
|
- add `@tanstack/react-table` to `packages/ui`
|
|
|
|
### Not recommended for the first slice
|
|
|
|
- virtualization package
|
|
- drag and drop package
|
|
- date package for table filters
|
|
- full export/import package support
|
|
|
|
The first implementation should minimize dependency expansion and keep the complexity budget focused on a single new capability.
|
|
|
|
## Open questions
|
|
|
|
1. Should the public column type stay fully source-owned, or should the first release expose a thin alias around TanStack column defs?
|
|
2. Should the first release include client-side search/filter state inside `DataTable`, or should filtering remain externally controlled?
|
|
3. Do we want a density variant in the first release, or should row height stay fixed until real product usage appears?
|
|
4. Should pagination UI be part of the root component family, or remain a separate companion component?
|
|
5. Should row selection be opt-in only, or should the root always reserve structure for it?
|
|
|
|
## Decision
|
|
|
|
The next `DataTable` implementation should:
|
|
|
|
- be built as an advanced pattern, not a primitive
|
|
- be source-owned at the public API layer
|
|
- use TanStack Table internally from the first implementation
|
|
- ship as a narrow, headless/core-first operational table
|
|
- defer scale features until real usage proves they are necessary
|