Skip to content

feat(data-table): bulk selection actions in the footer - #496

Open
itsprade wants to merge 8 commits into
mainfrom
feat/data-table-footer-selection-actions
Open

itsprade wants to merge 8 commits into
mainfrom
feat/data-table-footer-selection-actions

Conversation

@itsprade

@itsprade itsprade commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

What this does

Adds bulk actions to DataTable (tailor-inc/platform-planning#1738). Pass selectionActions to useDataTable and, while rows are selected, DataTable.Footer becomes a bulk-action bar: the selection count, your actions, and Clear, with pagination kept alongside. When nothing is selected, the footer looks exactly as it does today.

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ ☑ V2001  Tyrell Parts SA   Raw material   C. Lindqvist   EMEA   Inactive   US$238,429.82    │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│ 5 of 240 selected │ ▷ Activate (2)  ⏸ Deactivate (3)  ⤓ Export  ⋯ │ Clear   Rows per page 25  Page 1 / 10  « ‹ › » │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

This PR started as the prototype and open question from 3 Sep. The direction was settled with Sean in #pf-app-shell and at the 18 Sep board planning: build it into DataTable, place it in the footer, and let the consumer control which actions appear.

API

It is one option, shaped like rowActions. There is nothing new to compose in JSX.

const table = useDataTable({
  columns,
  data,
  control,
  selectionActions: [
    {
      id: "activate",
      label: "Activate",
      icon: <Play />,
      appliesTo: (vendor) => vendor.status === "inactive", // "Activate (2)", disabled at 0
      onClick: (vendors, { clearSelection }) => {
        activate(vendors); // only the eligible rows, incl. ones selected on other pages
        clearSelection();
      },
    },
  ],
});

<DataTable.Root value={table}>
  <DataTable.Table />
  <DataTable.Footer>
    <DataTable.Pagination />
  </DataTable.Footer>
</DataTable.Root>;
SelectionAction
id, label, icon What the button shows.
variant "default" | "destructive". This is styling only; the app still confirms destructive actions itself (interaction/confirm).
appliesTo Optional. Scopes the action to the selected rows it fits: it shows their count, disables the action at zero, and passes only those rows to onClick.
onClick(rows, { clearSelection }) Receives the selected rows the action applies to. The bar never clears the selection by itself.

New on the return value and context: selectedRows (each selected row as last loaded, across pages) and deselectAllRows (page-scoped). The new type is SelectionAction.

Behaviour

  • Opt-in. The bar needs a non-empty selectionActions, a DataTable.Footer, and at least one selected row. selectionActions also turns on the checkbox column by itself, so onSelectionChange becomes optional.
  • Overflow. Three actions stay inline. The rest move into a More actions menu, which opens upward.
  • Pop-out. In <Layout fill> the footer is already pinned. On a page-scrolling table, the bar is position: sticky while rows are selected, so it rides the bottom of the viewport and settles back into place at the end of the table. There is no JS.
  • Built on the generic Toolbar (feat: add generic toolbar #559). The bar is a Toolbar.Row, so it gets role="toolbar" and Arrow/Home/End navigation.
  • Accessibility. A persistent polite live region announces the count from the first selection. When the bar closes while focus is inside it, focus returns to the header checkbox.
  • Tone. The bar uses accent, which stays soft in all three themes and in both light and dark mode. Hover and secondary text are re-pointed so they stay legible on the tinted bar.
  • Pagination. While the bar is up, Pagination hides its own "N of M row(s) selected" text and shares the line with the bar. It wraps to a second line only when there isn't room.

Compatibility

  • API: additive. Tables without selectionActions render exactly as before, and a test asserts this. New context members are optional. selectedRows is required on UseDataTableReturn, like expandedIds was, so only a hand-built return value would need it.
  • Behaviour change: the header checkbox is now page-scoped in both directions. Checking it used to replace the selection with the current page, and unchecking it cleared every page. That silently dropped rows picked on other pages: with 5 rows selected on page 1, "select all" on page 2 gave 25, not 30. It now adds or removes only the current page, and clearSelection still empties everything. Tables whose selection stays on one page see no difference. The fix is its own commit (dcc9e77) so it can be split out if preferred, and the changeset calls it out.
  • @IzumiSy, one change to feat: add generic toolbar #559. The DataTable root goes from overflow-hidden to overflow-clip. Both clip the toolbar to the rounded frame, but hidden also makes the root a scroll container, which pinned the sticky footer inside the table. clip keeps your clipping without that side effect.

Commits

  1. fix(data-table): the header checkbox keeps other pages' rows.
  2. feat(data-table): selectionActions and remembered selected rows. Selection becomes an ordered map of id → row as last seen, and the current page's copy wins.
  3. feat(data-table): the footer bar (selection-bar.tsx), plus the Pagination and Footer changes and en/ja labels.
  4. feat(examples): /showcase/data-table-selection moves onto the real API, and /dashboard/products replaces its "Selected: …" line with bulk actions.
  5. docs(data-table): docs-src sections and the SelectionAction table; interaction/multi-select is rewritten around selectionActions, since the floating bar on a raw Table.Root is gone; list/dense-scan is updated; decisions/data-table-selection-footer-actions.md is now Decided; and a minor changeset.

The earlier commits on this PR are the original prototype and a merge of main.

Worth discussing

  • appliesTo(row) replaces the consumer-supplied count from the first sketch. Selection spans pages, and an app with server pagination can't count rows on other pages without its own cache. The table already sees every row the user selects.
  • Overlap with refactor(core): align state ownership with React Compiler #525. It rewrites selection state and adds controlled rowSelection. With a controlled selection, rows that haven't loaded yet are counted once they load; this is documented. Whichever lands second adapts, and the row memory just goes wherever selection is written.
  • Toasts. Bulk actions usually end in a toast, and the default bottom-right toast covers the footer's pagination for a few seconds.
  • Narrow tables. Below roughly 1,100px of table width, the bar and pagination wrap onto two lines. Hiding "Rows per page" while the bar is up would keep them on one line.

Follow-ups (not in this PR)

  • "Select all N" across pages.
  • A placeable DataTable.SelectionActions for custom placement, following the "option = default placement, sub-component = custom placement" rule from tailor-inc/platform-planning#1699.
  • Pending/loading state on an action.
  • Tooltips explaining a disabled action.
  • Escape to clear.

Try it

pnpm install && pnpm build && pnpm dev
  • /showcase/data-table-selection: 240 vendors. Tick rows, page with a selection open, open ⋯ → Delete, and use the Pinned footer / Page scroll switch.
  • /dashboard/products: Publish / Archive / Delete bulk actions.

Verification

  • Tests: selection-bar.test.tsx (13 new), plus hook tests (cross-page selection, selectedRows, selectionActions) and a header-checkbox test. 1,842 core tests pass.
  • Checks: pnpm fmt:check, pnpm exec turbo run lint test type-check check-dts (28/28), pnpm docs:check, and docs examples compile against core.
  • In the browser, on the example app's theme (bloom) in light and dark mode, at 1440px and a narrower ~1000px window: the bar, counts, overflow menu, the confirm flow, Clear moving focus to the header checkbox, cross-page selection, the header checkbox, and sticky pop-out in page-scroll mode. Not yet eyeballed on the default and cream themes.

🤖 Generated with Claude Code

Adds `/data-table-selection` to the vite example: 240 vendor records with a
checkbox column, and a footer that swaps its row-count text for a bulk-action
bar while a selection is open.

The bar reuses the same `DataTable.Pagination` in both states, so the
right-hand cluster is identical whether or not rows are selected. Inversion is
done by re-pointing the surface tokens on a wrapper inside the footer, so the
pagination buttons, the page-size Select and the action Buttons re-theme
themselves without any class overrides.

Prototype only — nothing under packages/** changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Code Metrics Report

main (ecc3f93) #496 (75cb5da) +/-
Coverage 87.1% 87.1% 0.0%
Test Execution Time 2m7s 2m3s -4s
Details
  |                     | main (ecc3f93) | #496 (75cb5da) | +/-  |
  |---------------------|----------------|----------------|------|
  | Coverage            |          87.1% |          87.1% | 0.0% |
  |   Files             |            178 |            178 |    0 |
  |   Lines             |           5467 |           5467 |    0 |
  |   Covered           |           4764 |           4764 |    0 |
+ | Test Execution Time |           2m7s |           2m3s |  -4s |

Reported by octocov

…t or as a pattern

Records the footer direction for multi-select bulk actions, notes that
`catalogue/src/pattern/interaction/multi-select` currently prescribes a floating
bottom bar (built on raw Table.Root, predating DataTable selection) and so needs
rewriting either way, and frames the open question for a team call: build the bar
into DataTable via `selectionActions`, or keep it a documented pattern.

No decision taken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@itsprade
itsprade force-pushed the feat/data-table-footer-selection-actions branch from a473622 to 9a6439b Compare September 3, 2026 14:56
itsprade and others added 6 commits September 29, 2026 12:28
Brings the prototype up to date with main (58 commits, incl. the generic
Toolbar #559 and the docs-src pipeline #396). main moved every demo page
under pages/showcase/ (#513), so the prototype moves with them, unchanged:
/data-table-selection -> /showcase/data-table-selection, listed in the
sidebar's Showcase group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kbox

Selection persists across pages, but the header checkbox did not respect
that: checking it replaced the selection with the current page's rows, and
unchecking it cleared every page. With 5 rows picked on page 1, select-all on
page 2 gave 25 selected instead of 30.

The header checkbox is now page-scoped in both directions. selectAllRows adds
the page's rows to the selection, and a new deselectAllRows removes only the
page's rows; clearSelection still empties everything. deselectAllRows is
optional on DataTableContextValue, and a hand-built context without it falls
back to clearSelection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oss pages

Adds the data half of footer bulk actions (#1738): a selectionActions option
on useDataTable, shaped like rowActions, and the selected rows it acts on.
The footer bar that renders these actions follows in the next commit.

- SelectionAction: id, label, icon, variant, and an optional appliesTo(row)
  predicate that narrows an action to the selected rows it applies to.
  onClick receives those rows plus a clearSelection helper.
- A non-empty selectionActions array enables row selection on its own, so
  onSelectionChange stays optional.
- selectedRows: selection is now an ordered map of id -> row as last seen, so
  rows picked on other pages can still be counted and handed to an action.
  Rows on the current page always win over the remembered copy.

selectedRows is required on UseDataTableReturn, like expandedIds, and
optional on DataTableContextValue, which is documented as hand-constructible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rows are selected

With selectionActions set and at least one row selected, the footer becomes
the bulk-action bar (#1738): the count, the actions, and Clear, with the
footer's own children (usually Pagination) kept alongside. Tables without
selectionActions render exactly as before.

- Built on the generic Toolbar (#559): the bar is a Toolbar.Row, so it gets
  role=toolbar and Arrow/Home/End navigation between its buttons.
- The first three actions render as buttons; the rest collapse into a
  "More actions" menu that opens upward. An action with appliesTo shows its
  eligible count ("Activate (6)") and disables at zero.
- Surface: bg-accent, which stays soft in every theme and both modes. A
  display:contents wrapper re-points --accent and --muted-foreground so hover
  states and secondary text stay visible on the tinted bar.
- Sticky while open, so on a page-scrolling table the bar rides the bottom of
  the viewport until the table's end scrolls into view. The DataTable root
  moves from overflow-hidden to overflow-clip for this: both clip to the
  rounded frame, but hidden also makes the root a scroll container, which
  trapped the sticky footer inside the table.
- Pagination hides its own "N selected" text while the bar is up and shares
  the line with it, dropping to a second line only when it doesn't fit.
- A persistent polite live region announces the count from the first tick;
  when the bar closes with focus inside it, focus returns to the header
  checkbox. Labels in en and ja.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/showcase/data-table-selection was the #496 prototype: a hand-built footer bar
on useDataTableContext with a primary/neutral tone toggle. It now uses the
real API, so the page shows exactly what the core component does:

- selectionActions with appliesTo counts, Activate / Deactivate / Export
  inline and Delete in the More actions menu behind a confirm dialog
  (interaction/confirm)
- actions mutate local state, so counts and badges update after an action
- the generic Toolbar scaffold on top, matching the other DataTable demos
- a Pinned footer / Page scroll switch to compare <Layout fill> with a page-
  scrolling table, where the bar rides the bottom of the window

dashboard/products drops its ad-hoc "Selected: …" line for Publish / Archive
/ Delete bulk actions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lect pattern

- DataTable docs: a Selection actions section (with accessibility notes), a
  SelectionAction reference table, the selectionActions option, and the
  footer/pagination and useDataTableContext notes that go with it.
- interaction/multi-select: rewritten around selectionActions. The floating
  bar on a hand-built Table.Root with native checkboxes is gone, along with
  its dangling source marker; the example is a DataTable with an appliesTo
  action, an overflowed destructive action, and a confirm dialog.
- list/dense-scan: bulk actions point at the footer bar.
- decisions: #496's open question is now a decided record — built into
  DataTable, what changed from the first sketch (appliesTo instead of count,
  rows instead of ids, accent instead of primary/neutral), and follow-ups.
- changeset: minor, calling out the page-scoped header checkbox.

Generated docs/ and the manifest come from pnpm docs:sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@itsprade itsprade changed the title docs(data-table): bulk actions in the footer — direction + prototype feat(data-table): bulk selection actions in the footer Sep 29, 2026
@itsprade
itsprade marked this pull request as ready for review September 29, 2026 12:02
@itsprade
itsprade requested a review from a team as a code owner September 29, 2026 12:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant