Files
cadence-ui/docs/rfcs/data-table.md
T

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