AdminBulkActionTable
ImplementedControlled bulk-selection admin data table — Wave-3 L5-M3 composition over the canonical DataTable organism. The leading checkbox column, per-row aria-selected + highlight and the accessible empty state come from DataTable's selection prop; this wrapper is FULLY CONTROLLED — the parent owns the selection Set<string> and receives toggle callbacks via onSelectionChange, the wrapper holds zero internal state. The bulk-action toolbar renders an 'N selected' live-region count plus one button per action (destructive variant for irreversible actions), all disabled until at least one row is picked. Exported from the barrel along with AdminBulkActionTableProps, AdminBulkActionTableColumn and AdminBulkAction.
components/organisms/AdminBulkActionTable/AdminBulkActionTable.tsxControlled bulk-selection admin data table — a Wave-3 L5-M3 composition where DataTable owns the checkbox column, per-row aria-selected state and the accessible empty state, while this wrapper renders the 'N selected' bulk-action toolbar (disabled until a row is picked, destructive variant supported). The wrapper holds zero internal state — the parent owns the `Set<string>` of selected row keys via `selectedKeys` and receives toggle callbacks via `onSelectionChange`. All user-visible strings have EN defaults and accept FR overrides.
AdminBulkActionTable
Organism — Admin family
Composes the canonical DataTable organism via its selection prop: select-all + per-row checkboxes, aria-selected highlighting and the empty state come from DataTable; the wrapper renders the 'N selected' toolbar with one button per action (danger variant for destructive). Selection is controlled — the parent owns a `Set<string>` of row keys and receives toggle callbacks. onSelect receives the selected row objects.
Pick rows with the checkboxes — the toolbar enables, announces the count, and fires the action on the selected row objects. Selection is controlled from the preview (the wrapper holds zero internal state).
| Buyer | Event | Total | |
|---|---|---|---|
| Marie Tremblay | Festival Jazz MTL | $96.00 | |
| Jean-Pierre Gauthier | OSM Symphonie n° 5 | $288.00 | |
| Sophie Lavoie | Lion d'Or Gala | $54.00 | |
| Alexandre Côté | Nuit du Piano | $120.00 | |
| Émilie Bergeron | Bal des Boys | $75.00 |
No bulk action fired yet.
This component supports light and dark mode.
Usage
AdminBulkActionTable is generic over its row type and is fully controlled — the wrapper holds zero internal selection state. Define columns with a cell renderer, pass rows, a stable rowKey (it doubles as the selection key), an actions array, and own the selectedKeys Set<string> with an onSelectionChange handler. The toolbar stays disabled until a row is picked and onSelect receives the selected row objects.
import { AdminBulkActionTable } from '@epasslive/design-system'
const columns: AdminBulkActionTableColumn<OrderRow>[] = [
{ id: 'buyer', header: 'Buyer', cell: (r) => r.buyer },
{ id: 'event', header: 'Event', cell: (r) => r.event },
{ id: 'total', header: 'Total', cell: (r) => fmtCad(r.total), align: 'right' },
]
const actions: AdminBulkAction<OrderRow>[] = [
{ id: 'publish', label: 'Publish', onSelect: (rows) => publishOrders(rows) },
{ id: 'delete', label: 'Delete', onSelect: (rows) => deleteOrders(rows), destructive: true },
]
function OrdersTable({ orders }: { orders: OrderRow[] }) {
const [selectedKeys, setSelectedKeys] = React.useState<Set<string>>(new Set())
return (
<AdminBulkActionTable
columns={columns}
rows={orders}
rowKey={(r) => r.id}
actions={actions}
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
ariaLabel="Orders awaiting review"
/>
)
}Conventions
Keep rowKey stable across data refreshes or selections silently detach. Put irreversible actions last and mark them destructive — it only switches the button to the danger variant; confirm-before-delete semantics stay in your handler (compose ConfirmModal). Pass caption for a visible caption; on its own ariaLabel names the table accessibly without visible text.
Accessibility
The select-all and per-row checkboxes come from DataTable's selection plumbing with unique accessible labels, and the toolbar announces the selection count in a role="toolbar" with an aria-live="polite" region (WCAG 4.1.2). While no row is selected the toolbar sets aria-disabled and dims — actions announce as disabled rather than disappearing.
Props
columns*AdminBulkActionTableColumn<T>[]Column definitions — see the column table below.
rows*T[]Row data — one item per row, typed by the generic.
rowKey*(row: T, i: number) => stringStable React key per row — also the selection key.
actions*AdminBulkAction<T>[]Bulk actions surfaced in the toolbar — see the action table below.
selectedKeys*ReadonlySet<string>Currently selected row keys — controlled from the parent.
onSelectionChange*(keys: Set<string>) => voidToggle callback — receives the next Set of selected keys.
captionstringVisible table caption; falls back to ariaLabel (hidden) when omitted.
emptyMessagestring"No data to display"Rendered in DataTable's accessible empty state. FR: "Aucune donnée à afficher".
ariaLabelstring"Bulk action table"Accessible name for the section and caption fallback. FR: "Tableau d'actions groupées".
toolbarLabelstring"Bulk actions"Accessible label for the bulk-actions toolbar. FR: "Actions groupées".
selectedCountLabel(count: number) => string(n) => `${n} selected`Factory for the selection-count announcement (aria-live polite region). FR: (n) => `${n} sélectionné${n === 1 ? '' : 's'}`.
selectAllLabelstring"Select all rows"Accessible label for the select-all checkbox (forwarded to DataTable). FR: "Tout sélectionner".
selectRowLabel(key: string) => string(key) => `Select row ${key}`Accessible label factory for per-row checkboxes (forwarded to DataTable). FR: (key) => `Sélectionner la ligne ${key}`.
classNamestringClass on the outer section wrapper.
idstringStable id (auto-generated abulk-<useId()> fallback).
Column definition
id*stringColumn identifier.
header*stringHeader label.
cell*(row: T) => React.ReactNodeCell renderer — receives the row object.
widthstringCSS width for the column.
align"left" | "right" | "center"Cell + header alignment.
Action definition
id*stringAction identifier (React key).
label*stringButton label shown in the toolbar.
onSelect*(selectedRows: T[]) => voidFired on click — receives the currently selected row objects (not keys).
destructivebooleanRenders the danger button variant (instead of secondary) for irreversible actions.
Props
AdminBulkActionTable
columnsAdminBulkActionTableColumn<T>[]—Column definitions — id / header / cell / width / align. See AdminBulkActionTableColumn below.
rowsT[]—Row data — one item per row, typed by the generic.
rowKey(row: T, i: number) => string—Stable React key per row. DOUBLES as the selection key — keep it stable across re-renders or selections silently detach on data refresh.
actionsAdminBulkAction<T>[]—Bulk actions surfaced in the toolbar — id / label / onSelect(selectedRows) / destructive?. See AdminBulkAction below.
selectedKeysReadonlySet<string>—Currently selected row keys — CONTROLLED from the parent. The wrapper holds zero internal selection state.
onSelectionChange(keys: Set<string>) => void—Toggle callback — receives the next Set of selected keys (row toggle OR toggle-all). The parent is the source of truth.
captionstring—Visible table caption; falls back to ariaLabel (hidden) when omitted. EN default flow — pass localized values for FR.
emptyMessagestring'No data to display'Empty-state cell copy (DataTable's role=status / aria-live=polite). EN default — FR: 'Aucune donnée à afficher'.
ariaLabelstring'Bulk action table'Accessible name for the section and the table caption fallback. EN default — FR: "Tableau d'actions groupées".
toolbarLabelstring'Bulk actions'Accessible label for the bulk-actions toolbar. EN default — FR: 'Actions groupées'.
selectedCountLabel(count: number) => string(n) => `${n} selected`Factory for the selection-count announcement in the aria-live polite region. EN default — FR: (n) => `${n} sélectionné${n === 1 ? '' : 's'}`.
selectAllLabelstring'Select all rows'Accessible label for the select-all checkbox (forwarded to DataTable's selection prop). EN default — FR: 'Tout sélectionner'.
selectRowLabel(key: string) => string(key) => `Select row ${key}`Accessible label factory for per-row checkboxes (forwarded to DataTable). EN default — FR: (key) => `Sélectionner la ligne ${key}`.
classNamestring—Class on the outer section wrapper.
idstringabulk-<useId()>Stable id for the wrapper section (auto-generated abulk-<useId()> fallback).
<AdminBulkActionTable />Accessibility
ARIA
2 rulescontrol
1 ruleUsage
Import
import { AdminBulkActionTable } from "@epasslive/design-system"Example 1
<AdminBulkActionTable columns={cols} rows={rows} rowKey={(r) => r.id} actions={acts} selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} ariaLabel="Orders awaiting review" />Example 2
<AdminBulkActionTable columns={cols} rows={orders} rowKey={(r) => r.id} actions={[{ id: "publish", label: "Publish", onSelect: publishOrders }, { id: "delete", label: "Delete", onSelect: deleteOrders, destructive: true }]} selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} ariaLabel="Commandes en attente" emptyMessage="Aucune donnée à afficher" toolbarLabel="Actions groupées" selectedCountLabel={(n) => `${n} sélectionné${n === 1 ? "" : "s"}`} selectAllLabel="Tout sélectionner" selectRowLabel={(k) => `Sélectionner la ligne ${k}`} />Anti-patterns
— Do not fetch data directly in organisms
— Avoid tight coupling to specific data sources
— Do not let the wrapper hold internal selection state — selectedKeys/onSelectionChange are CONTROLLED props; failing to wire them desyncs the visual selection from the parent's source of truth on the next re-render
— Do not pass selection keys as array — DataTable's selection.selectedKeys is a ReadonlySet<string>; convert with new Set(arr)
— Do not rely on the wrapper for confirm-before-delete semantics — destructive: true only switches the button variant; compose ConfirmModal in your onSelect
— Do not use this organism when you only need per-row actions or single-row selection — AdminDataTable + an action column (or DataTable directly with its selection prop) is simpler