Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/data-table-columns-and-row-nav.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
DataTable: sticky/pinned columns and user column settings

- **Pinned columns** — add `pin: "left" | "right"` to a `Column` to freeze it during horizontal scroll (sticky offsets are measured from the rendered layout; `width` is optional but recommended for stable sizing). The selection column auto-pins left and the row-actions column auto-pins right, with a subtle freeze shadow at the frozen edge once content scrolls under it.
- **Column settings** — pass `columnSettings` to `DataTable.Toolbar` to render a built-in "Columns" control (top-right) that lets users show/hide columns, reorder them (drag), and change pinning by dragging a column between the Fixed left / Scrollable / Fixed right zones.
- **`DataTable.ColumnSettings`** — a "Columns" toolbar popover to show/hide columns, reorder them (drag), and change pinning by dragging a column between the Fixed left / Scrollable / Fixed right zones. Right-aligns itself inside `DataTable.Toolbar`.
- **Persisted column layout** — pass a stable `tableId` to `useDataTable` to persist each user's column visibility, order, and pinning to `localStorage` (per-user preference; not stored in the URL). Omit for in-memory-only layout.
- `Table.Root` now accepts an optional `containerRef` for its scroll container.
7 changes: 4 additions & 3 deletions catalogue/src/fundamental/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ Plus `badgeVariants` CVA for custom-styled siblings.

**Import:** compound namespace + helpers from `'@tailor-platform/app-shell'`, e.g. `DataTable`, `useDataTable`, `useCollectionVariables`, `createColumnHelper`, and types such as `Column`, `UseDataTableReturn`.

**Purpose:** Production list screens over GraphQL **connections**. Owns toolbar filter chips (**`DataTable.Filters`** from column `filter` configs), header sort, **`DataTable.Pagination`** (cursor-first; First/Last when `total` is provided), loading skeleton/error row, **`onClickRow`**, **`rowActions`** (kebab column), **`onSelectionChange`** (checkbox column), **column pinning** (`pin: "left" | "right"`) and built-in **column settings** (`<DataTable.Toolbar columnSettings>` — user show/hide + reorder + pin, persisted per-user via **`tableId`**).
**Purpose:** Production list screens over GraphQL **connections**. Owns toolbar filter chips (**`DataTable.Filters`** from column `filter` configs), header sort, **`DataTable.Pagination`** (cursor-first; First/Last when `total` is provided), loading skeleton/error row, **`onClickRow`**, **`rowActions`** (kebab column), **`onSelectionChange`** (checkbox column), **column pinning** (`pin: "left" | "right"`) and **`DataTable.ColumnSettings`** (user show/hide + reorder + pin, persisted per-user via **`tableId`**).

**Primitives:** Builds on low-level **`Table`**; do not reinvent pagination/filters manually unless the dataset is trivial.

Expand All @@ -410,8 +410,9 @@ const table = useDataTable({
});

<DataTable.Root value={table}>
<DataTable.Toolbar columnSettings>
<DataTable.Toolbar>
<DataTable.Filters />
<DataTable.ColumnSettings />
</DataTable.Toolbar>
<DataTable.Table />
<DataTable.Footer>
Expand All @@ -422,7 +423,7 @@ const table = useDataTable({

**Row navigation:** whole row is clickable via **`onClickRow`** → `navigate(detailHref(row))`; wrap the primary identifier cell in `<Link>` for keyboard/SR access. Never add a per-row "View" / "Open" / "→" button. (A first-class row-interaction API is under design — see the row-interaction tracking issue.)

**Column pinning & settings:** set `pin: "left" | "right"` on a `Column` to freeze it during horizontal scroll — selection auto-pins left, row-actions auto-pins right (a `width` is optional but recommended for stable sizing). Pass **`columnSettings`** to **`DataTable.Toolbar`** to render the built-in "Columns" control (show/hide, reorder, re-pin) at the top-right; pass a stable, **unique** **`tableId`** to persist each user's layout to `localStorage` (a per-user preference — intentionally not in the URL like filters/sort).
**Column pinning & settings:** set `pin: "left" | "right"` on a `Column` to freeze it during horizontal scroll — selection auto-pins left, row-actions auto-pins right (a `width` is optional but recommended for stable sizing). Drop **`DataTable.ColumnSettings`** in the toolbar to let users show/hide, reorder, and re-pin columns (it right-aligns itself); pass a stable, **unique** **`tableId`** to persist each user's layout to `localStorage` (a per-user preference — intentionally not in the URL like filters/sort).

**Column alignment:** each `Column` accepts **`align`** (`"left" | "right"`) applied to both header and body cell. Numeric `type` columns (`"number"`, `"money"`) default to `"right"` automatically so digits align on the decimal place — pass `align="left"` to opt out; everything else defaults to `"left"`.

Expand Down
2 changes: 1 addition & 1 deletion catalogue/src/pattern/list/dense-scan/PATTERN.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Omit `fill` on pages that should flow and scroll naturally (forms, dashboards, a
- Bulk actions toolbar appears only when ≥1 row is selected
- Whole row is clickable via `onClickRow`; wrap the primary identifier cell in `<Link>` for keyboard/SR access. No per-row "View" / "Open" buttons
- Per-row `Menu` (overflow `…`) is reserved for non-navigation actions (Archive, Duplicate, Delete)
- Wide lists (many columns / horizontal scroll): pin the column users scan by to the **left** — the record's identifier (invoice / order **reference**) or its **name** (customer, product) — so it stays anchored while the rest scrolls; optionally pin a single high-signal **status** or **total** to the **right**. Keep the pinned set small (≈1 left, at most 1 right) — over-pinning eats the scroll area. Let users override via `<DataTable.Toolbar columnSettings>` (show/hide + reorder + re-pin) with a stable `tableId` so each user's layout persists
- Wide lists (many columns / horizontal scroll): pin the column users scan by to the **left** — the record's identifier (invoice / order **reference**) or its **name** (customer, product) — so it stays anchored while the rest scrolls; optionally pin a single high-signal **status** or **total** to the **right**. Keep the pinned set small (≈1 left, at most 1 right) — over-pinning eats the scroll area. Let users override via `DataTable.ColumnSettings` (show/hide + reorder + re-pin) with a stable `tableId` so each user's layout persists

## Anti-patterns

Expand Down
34 changes: 18 additions & 16 deletions docs/components/data-table.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,15 @@ function JournalsPage() {

`DataTable` is a namespace object. All sub-components read state from `DataTable.Root` via context.

| Sub-component | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DataTable.Root` | Context provider. Wraps all other sub-components. Required. |
| `DataTable.Table` | Renders the `<table>` with headers and body. Required. |
| `DataTable.Toolbar` | Container for toolbar content (e.g. filters). Optional. Pass `columnSettings` to render the built-in "Columns" control (show/hide + reorder + pin) at the top-right. See props below. |
| `DataTable.Filters` | Add-filter panel + active filter chips, auto-generated from column filter configs. Requires `control` from `useCollectionVariables`. |
| `DataTable.Footer` | Footer container for pagination and other footer content. Optional. |
| `DataTable.Pagination` | Pre-built pagination controls with optional row count and selection info. Requires `control` from `useCollectionVariables`. Place inside `DataTable.Footer`. |
| Sub-component | Description |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DataTable.Root` | Context provider. Wraps all other sub-components. Required. |
| `DataTable.Table` | Renders the `<table>` with headers and body. Required. |
| `DataTable.Toolbar` | Container for toolbar content (filters, column settings). Optional. Lays children out in a row. |
| `DataTable.Filters` | Add-filter panel + active filter chips, auto-generated from column filter configs. Requires `control` from `useCollectionVariables`. |
| `DataTable.ColumnSettings` | "Columns" popover to show/hide columns, reorder them (drag), and pin them by dragging between the **Fixed left**, **Scrollable**, and **Fixed right** zones. Persists per-user when `useDataTable` has a `tableId`. Place inside `DataTable.Toolbar`. |
| `DataTable.Footer` | Footer container for pagination and other footer content. Optional. |
| `DataTable.Pagination` | Pre-built pagination controls with optional row count and selection info. Requires `control` from `useCollectionVariables`. Place inside `DataTable.Footer`. |

### `DataTable.Root` Props

Expand All @@ -161,11 +162,10 @@ function JournalsPage() {

### `DataTable.Toolbar` Props

| Prop | Type | Default | Description |
| ---------------- | ----------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `children` | `ReactNode` | — | Toolbar content (e.g. `DataTable.Filters`), laid out on the left. |
| `columnSettings` | `boolean` | `false` | Render the built-in "Columns" control (show/hide + reorder + pin) anchored to the top-right. Persists per-user when `useDataTable` has a `tableId`. |
| `className` | `string` | — | Additional CSS class for the toolbar container. |
| Prop | Type | Default | Description |
| ----------- | ----------- | ------- | ----------------------------------------------------------------------- |
| `children` | `ReactNode` | — | Toolbar content (e.g. `DataTable.Filters`, `DataTable.ColumnSettings`). |
| `className` | `string` | — | Additional CSS class for the toolbar container. |

### `DataTable.Filters` Props

Expand Down Expand Up @@ -212,7 +212,7 @@ Row selection is enabled by providing `onSelectionChange` to `useDataTable`. The
## Column pinning, visibility & ordering

- **Pin** a column with `pin: "left" | "right"`. Pinned columns stay visible during horizontal scroll; the selection column auto-pins left and the row-actions column auto-pins right. A subtle shadow appears at the frozen edge once the table is scrolled under it. Sticky offsets are measured from the rendered layout, so a `width` isn't required — but setting `width` on pinned columns is recommended so their size stays stable as content changes.
- **Column settings.** Pass `columnSettings` to `DataTable.Toolbar` to render a built-in "Columns" control — a popover to show/hide columns, reorder them (drag), and change pinning by dragging a column between the **Fixed left**, **Scrollable**, and **Fixed right** zones. It's a toolbar prop (not a composed sub-component) because the control always sits in the same top-right position.
- **`DataTable.ColumnSettings`** gives users a "Columns" popover to show/hide columns, reorder them (drag), and change pinning by dragging a column between the **Fixed left**, **Scrollable**, and **Fixed right** zones. Place it inside `DataTable.Toolbar`; it right-aligns itself, so filters sit left and this control sits right on the same row.
- **Persistence.** Pass a stable, **unique** `tableId` to persist each user's column layout (visibility, order, pinning) to `localStorage` (key `as:data-table:v1:<tableId>`). This is a per-user preference — it is deliberately **not** stored in the URL like filters/sort/pagination, so it survives reloads and isn't reset by shared/filtered links. Omit `tableId` for in-memory-only layout (state simply isn't persisted). Two tables mounted with the same `tableId` share one storage key and overwrite each other — use a unique id per table (e.g. `<route>:<entity>`); a dev-mode warning fires on duplicates.

```tsx
Expand All @@ -223,7 +223,9 @@ const table = useDataTable<Order>({
});

<DataTable.Root value={table}>
<DataTable.Toolbar columnSettings />
<DataTable.Toolbar>
<DataTable.ColumnSettings />
</DataTable.Toolbar>
<DataTable.Table />
</DataTable.Root>;
```
Expand Down Expand Up @@ -276,7 +278,7 @@ A column definition passed to `useDataTable`. `Column<TRow>` is a discriminated
| `render` | `(row: TRow) => ReactNode` | Renders the cell content. Optional — overrides the built-in `type` renderer when set. |
| `id` | `string` | Stable identifier for column visibility and React key. Falls back to `label` when omitted. |
| `width` | `number` | Fixed column width in pixels. Optional. |
| `pin` | `"left" \| "right"` | Freezes the column to that edge so it stays visible during horizontal scroll (the default; the user can override it via the toolbar's `columnSettings` control). Sticky offsets are measured from the rendered layout, so `width` isn't required — but setting `width` on pinned columns is recommended for stable sizing. The selection column auto-pins left and the row-actions column auto-pins right. |
| `pin` | `"left" \| "right"` | Freezes the column to that edge so it stays visible during horizontal scroll (the default; the user can override it via `DataTable.ColumnSettings`). Sticky offsets are measured from the rendered layout, so `width` isn't required — but setting `width` on pinned columns is recommended for stable sizing. The selection column auto-pins left and the row-actions column auto-pins right. |
| `align` | `"left" \| "right"` | Horizontal alignment. Defaults to `"right"` for `type: "number"` and `type: "money"`; `"left"` otherwise. Pass `"left"` to opt a numeric column out. |
| `truncate` | `boolean` | Truncate overflowing text with an ellipsis. Wires up an app-shell `<Tooltip>` automatically when the resolved cell value is a string or number — resolved via `accessor` first, then `row[col.id]` as a fallback — so hovering the cell reveals the full value. With `inferColumns`, no explicit `accessor` is needed because `id` is pinned to the field name. Requires another column to anchor the row width (`width` on a neighbor, or a fixed-size column like selection / row actions). |
| `accessor` | _(narrowed per `type`)_ | Extracts the raw value. The return type is narrowed per `type` branch — returning an array is a compile error on all typed columns except `badge`, and returning a plain object is a compile error on all typed columns. Untyped columns (`type` omitted) retain `unknown`. `null` and `undefined` are always allowed. |
Expand Down
3 changes: 2 additions & 1 deletion examples/vite-app/src/pages/data-table-lab/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,9 @@ const DataTableLabPage = () => {
}
>
<DataTable.Root value={settingsTable}>
<DataTable.Toolbar columnSettings>
<DataTable.Toolbar>
<DataTable.Filters />
<DataTable.ColumnSettings />
</DataTable.Toolbar>
<DataTable.Table />
</DataTable.Root>
Expand Down
Loading
Loading