Skip to content
Open
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
13 changes: 13 additions & 0 deletions .changeset/clear-kites-select.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@tailor-platform/app-shell": minor
---

Add controlled and uncontrolled row selection through `useDataTable`'s `rowSelection` option. Pass `selectedIds` with `onChange` to control selection, or `defaultSelectedIds` to initialize table-owned selection. The existing `onSelectionChange` callback is deprecated in favor of `rowSelection.onChange`, but remains supported for compatibility.

```tsx
const table = useDataTable({
columns,
data,
rowSelection: { selectedIds, onChange: setSelectedIds },
});
```
9 changes: 5 additions & 4 deletions docs-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@
"PageInfo",
"PaginationVariables",
"RowAction",
"RowSelectionOptions",
"SelectOption",
"SelectionAction",
"SelectionActionHelpers",
Expand All @@ -446,10 +447,10 @@
"withURLCollectionState"
],
"hashes": {
"typeSurface": "fe43108015be4a00",
"outline": "dc0b82256114191b",
"typeSurface": "6420153668673350",
"outline": "8a885df3785f3a03",
"snapshot": null,
"outputMd": "3e8474e4d1437c91",
"outputMd": "8848cda0fe63b441",
"examples": null
}
},
Expand Down Expand Up @@ -1693,7 +1694,7 @@
"packages/core/skills/app-shell-patterns/references/components/combobox.md": "dfb775c7c4307409",
"packages/core/skills/app-shell-patterns/references/components/command-palette.md": "7e5bc675a593791a",
"packages/core/skills/app-shell-patterns/references/components/csv-importer.md": "53d3c795ca21b9dc",
"packages/core/skills/app-shell-patterns/references/components/data-table.md": "3e8474e4d1437c91",
"packages/core/skills/app-shell-patterns/references/components/data-table.md": "8848cda0fe63b441",
"packages/core/skills/app-shell-patterns/references/components/date-picker.md": "f39002ad79625dc4",
"packages/core/skills/app-shell-patterns/references/components/default-header.md": "375e55f4b12fa2be",
"packages/core/skills/app-shell-patterns/references/components/default-sidebar.md": "3422388a0cc78571",
Expand Down
46 changes: 42 additions & 4 deletions docs-src/components/data-table.docs.outline.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
type DataTablePaginationProps,
type DataTableAction,
type RowAction,
type RowSelectionOptions,
type SelectionAction,
type UseDataTableOptions,
type UseDataTableReturn,
Expand Down Expand Up @@ -228,7 +229,7 @@ By default `DataTable.Filters` renders the active filter chips plus the **Add fi
| Rows selected and `total` is not provided | `Y row(s) selected` |
| No selection enabled and no `total` | _(nothing displayed)_ |

Row selection is enabled by providing `onSelectionChange` or `selectionActions` to `useDataTable`. The `total` value comes from `DataTableData.total`. While the footer shows the bulk-action bar (see [Selection actions](#selection-actions)), the bar owns the selection count and this text is omitted.
Row selection is enabled by providing `rowSelection`, `onSelectionChange`, or a non-empty `selectionActions` array to `useDataTable`. The `total` value comes from `DataTableData.total`. While the footer shows the bulk-action bar (see [Selection actions](#selection-actions)), the bar owns the selection count and this text is omitted.

When pagination changes page or page size, `DataTable.Table` resets its own scroll container to the top automatically. That applies whether navigation comes from the built-in `DataTable.Pagination` or from custom controls using the same table context.

Expand Down Expand Up @@ -376,9 +377,45 @@ The trigger is a native `<button>`, so Enter/Space activation and the focus ring

**Known limitation — row count.** Detail rows are real `<tr>` elements, so a screen reader counts them: ten records with two expanded announces as twelve rows. Fixing this needs `aria-rowcount` plus explicit `aria-rowindex` on every row (with detail rows sharing their parent's index) and correct interaction with pagination; `role="presentation"` on the detail row would fix the count but remove the panel from screen-reader table navigation. Neither is implemented.

## Row selection

Pass `rowSelection` to `useDataTable` to enable the checkbox column. Rows must have a string or number `id`; selected ids are their string-normalized values, and rows without an id are excluded. An empty configuration (`rowSelection: {}`) enables uncontrolled selection without a notification callback.

### Controlled selection

Provide `selectedIds` and `onChange` together to own the selection. The `RowSelectionOptions` type requires `onChange` in controlled mode and rejects combining `selectedIds` with `defaultSelectedIds`.

```tsx
const [selectedIds, setSelectedIds] = useState<string[]>([]);

const table = useDataTable<Order>({
columns,
data,
rowSelection: { selectedIds, onChange: setSelectedIds },
});
```

Commands such as `toggleRowSelection(row)` request a new array through `onChange`; the rendered selection changes only when the owner updates `selectedIds`. Each command derives from the current committed value, so multiple commands before the owner commits share the same base. Compute batched changes yourself and set `selectedIds` directly instead of issuing repeated commands.

### Uncontrolled selection

Omit `selectedIds` to let the table own selection state. `defaultSelectedIds` initializes it only when the hook mounts; later changes to that default do not reset selection. `onChange` is optional.

```tsx
const table = useDataTable<Order>({
columns,
data,
rowSelection: { defaultSelectedIds: ["INV-1001"] },
});
```

Keep the controlled/uncontrolled mode stable while mounted. Callbacks notify user-driven selection changes, not mounting or external `selectedIds` updates. `onSelectionChange` is deprecated but remains supported for compatibility. Replace `onSelectionChange: handleSelectionChange` with `rowSelection: { onChange: handleSelectionChange }` to keep uncontrolled notification behavior. `rowSelection.onChange` takes precedence when both callbacks are supplied, and `onSelectionChange` is used when the new configuration has no callback.

Selection spans pages. The header checkbox adds or removes only the current page's rows, preserving ids selected on other pages; `clearSelection()` clears every page. `selectedRows` contains current-page rows in their latest version and off-page rows as last loaded. Selected ids whose rows have never been loaded are omitted from `selectedRows` until seen, so the selection count can exceed the number of available row objects. Bulk actions operate only on those available rows.

## Selection actions

Pass `selectionActions` to `useDataTable` and, while at least one row is selected, `DataTable.Footer` becomes a bulk-action bar: the selection count, your actions, and a **Clear** button, with the footer's own children (usually `DataTable.Pagination`) kept alongside. Providing the option also enables row selection, so `onSelectionChange` is optional. There is nothing new to compose in JSX — just keep `DataTable.Footer` in the tree.
Pass `selectionActions` to `useDataTable` and, while at least one row is selected, `DataTable.Footer` becomes a bulk-action bar: the selection count, your actions, and a **Clear** button, with the footer's own children (usually `DataTable.Pagination`) kept alongside. A non-empty array also enables row selection, so `rowSelection` and `onSelectionChange` are optional. There is nothing new to compose in JSX — just keep `DataTable.Footer` in the tree.

```tsx
const table = useDataTable<Vendor>({
Expand Down Expand Up @@ -450,7 +487,8 @@ const table = useDataTable({
| `onClickRow` | `(row: TRow) => void` | Called when the user clicks a row. Adds a pointer cursor to rows. |
| `tableId` | `string` | Stable id used to persist per-user column layout (visibility, order, pinning) to `localStorage`. When omitted, column layout is in-memory only and resets on reload. |
| `rowActions` | `RowAction<TRow>[]` | Per-row action items rendered in a kebab-menu column. The column is omitted when empty or not provided. |
| `onSelectionChange` | `(ids: string[]) => void` | Called with selected row IDs on change. Providing this (or `selectionActions`) enables the checkbox column. Rows must have a string or number `id`. |
| `rowSelection` | `RowSelectionOptions` | Controlled `selectedIds` + `onChange`, or uncontrolled `defaultSelectedIds` + optional `onChange`. Providing even `{}` enables checkboxes. See [Row selection](#row-selection). |
| `onSelectionChange` | `(ids: string[]) => void` | **Deprecated:** use `rowSelection.onChange`. Still supported for compatibility; enables checkboxes and is used when the new callback is absent. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to say let's make a ticket to remove on v2.0.0 but I guess we just sweep all "deprecated" properties at that time

| `selectionActions` | `SelectionAction<TRow>[]` | Bulk actions shown in `DataTable.Footer` while rows are selected. A non-empty array also enables selection. See [Selection actions](#selection-actions). |
| `rowExpansion` | `RowExpansionOptions<TRow>` | Expandable detail rows: `render`, plus optional `canExpand` / `getLabel`, and `expandedIds` + `onChange` together for controlled mode. See [Expandable rows](#expandable-rows). |
| `sort` | `false \| { multiple?: boolean }` | Sort behaviour. `false` disables sorting entirely. `{ multiple: true }` enables multi-column sorting. Omit or pass `{}` for single-column sort (default). |
Expand Down Expand Up @@ -1054,7 +1092,7 @@ function MyCustomPagination() {
}
```

The selection is here too — `selectedIds`, `selectedRows` (each selected row as last loaded, across pages), `selectAllRows` / `deselectAllRows` for the current page, and `clearSelection` — for the rare case the built-in [selection actions](#selection-actions) bar doesn't fit.
The selection is here too — `selectedIds`, `selectedRows` (each loaded selected row as last seen, across pages; unseen ids are omitted), `selectAllRows` / `deselectAllRows` for the current page, and `clearSelection` — for the rare case the built-in [selection actions](#selection-actions) bar doesn't fit.

## SDK Plugin (`@tailor-platform/sdk-plugin-app-shell`)

Expand Down
46 changes: 42 additions & 4 deletions docs/components/data-table.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/core/src/components/data-table/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type {
NumberCellOptions,
DataTableAction,
RowAction,
RowSelectionOptions,
SelectionAction,
SelectionActionHelpers,
UseDataTableOptions,
Expand Down
Loading
Loading