Skip to main content

AdminBulkActionTable

Implemented

Controlled 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.

Categoryorganisms
Sourcecomponents/organisms/AdminBulkActionTable/AdminBulkActionTable.tsx
AI HintComposes DataTable's selection prop. Use when rows are acted on in BULK — actions receive the selected row OBJECTS (not keys), so no key→row lookup is needed in handlers. Fully controlled: selectedKeys + onSelectionChange are required props and the wrapper holds zero internal state — the parent owns the Set<string>. Keep rowKey stable across re-renders or selections silently detach on data refresh. Put irreversible actions last and mark destructive: true (only switches the button to the danger variant; confirm-before-delete semantics stay in your handler via ConfirmModal). EN-default i18n (toolbarLabel/selectedCountLabel/selectAllLabel/selectRowLabel/emptyMessage/ariaLabel) — FR consumers pass their own per the JSDoc recipes. Exported from the barrel along with AdminBulkActionTableProps, AdminBulkActionTableColumn and AdminBulkAction.

Controlled 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).

Orders awaiting review
BuyerEventTotal
Marie TremblayFestival Jazz MTL$96.00
Jean-Pierre GauthierOSM Symphonie n° 5$288.00
Sophie LavoieLion d'Or Gala$54.00
Alexandre CôtéNuit du Piano$120.00
Émilie BergeronBal 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

Prop
Type
Default
Description
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) => string
—

Stable 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>) => void
—

Toggle callback — receives the next Set of selected keys.

caption
string
—

Visible table caption; falls back to ariaLabel (hidden) when omitted.

emptyMessage
string
"No data to display"

Rendered in DataTable's accessible empty state. FR: "Aucune donnée à afficher".

ariaLabel
string
"Bulk action table"

Accessible name for the section and caption fallback. FR: "Tableau d'actions groupées".

toolbarLabel
string
"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'}`.

selectAllLabel
string
"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}`.

className
string
—

Class on the outer section wrapper.

id
string
—

Stable id (auto-generated abulk-<useId()> fallback).

Column definition

Prop
Type
Default
Description
id*
string
—

Column identifier.

header*
string
—

Header label.

cell*
(row: T) => React.ReactNode
—

Cell renderer — receives the row object.

width
string
—

CSS width for the column.

align
"left" | "right" | "center"
—

Cell + header alignment.

Action definition

Prop
Type
Default
Description
id*
string
—

Action identifier (React key).

label*
string
—

Button label shown in the toolbar.

onSelect*
(selectedRows: T[]) => void
—

Fired on click — receives the currently selected row objects (not keys).

destructive
boolean
—

Renders the danger button variant (instead of secondary) for irreversible actions.

Props

Preview

AdminBulkActionTable

Prop
Type
Required
Default
Description
columns
AdminBulkActionTableColumn<T>[]
Required
—

Column definitions — id / header / cell / width / align. See AdminBulkActionTableColumn below.

rows
T[]
Required
—

Row data — one item per row, typed by the generic.

rowKey
(row: T, i: number) => string
Required
—

Stable React key per row. DOUBLES as the selection key — keep it stable across re-renders or selections silently detach on data refresh.

actions
AdminBulkAction<T>[]
Required
—

Bulk actions surfaced in the toolbar — id / label / onSelect(selectedRows) / destructive?. See AdminBulkAction below.

selectedKeys
ReadonlySet<string>
Required
—

Currently selected row keys — CONTROLLED from the parent. The wrapper holds zero internal selection state.

onSelectionChange
(keys: Set<string>) => void
Required
—

Toggle callback — receives the next Set of selected keys (row toggle OR toggle-all). The parent is the source of truth.

caption
string
Optional
—

Visible table caption; falls back to ariaLabel (hidden) when omitted. EN default flow — pass localized values for FR.

emptyMessage
string
Optional
'No data to display'

Empty-state cell copy (DataTable's role=status / aria-live=polite). EN default — FR: 'Aucune donnée à afficher'.

ariaLabel
string
Optional
'Bulk action table'

Accessible name for the section and the table caption fallback. EN default — FR: "Tableau d'actions groupées".

toolbarLabel
string
Optional
'Bulk actions'

Accessible label for the bulk-actions toolbar. EN default — FR: 'Actions groupées'.

selectedCountLabel
(count: number) => string
Optional
(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'}`.

selectAllLabel
string
Optional
'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
Optional
(key) => `Select row ${key}`

Accessible label factory for per-row checkboxes (forwarded to DataTable). EN default — FR: (key) => `Sélectionner la ligne ${key}`.

className
string
Optional
—

Class on the outer section wrapper.

id
string
Optional
abulk-<useId()>

Stable id for the wrapper section (auto-generated abulk-<useId()> fallback).

Quick use:<AdminBulkActionTable />

Accessibility

ARIA roletable

ARIA

2 rules

WCAG 4.1.2

Select-all + per-row checkboxes come from DataTable's selection prop with unique accessible labels (selectAllLabel / selectRowLabel); toolbar is a role='toolbar' with an aria-live='polite' region announcing the selection count (WCAG 4.1.2)

WCAG 4.1.2

While no row is selected the toolbar sets aria-disabled and dims (opacity) — actions exist in the a11y tree but announce as disabled rather than disappearing (WCAG 4.1.2)

control

1 rule

ADR-035 + Wave-3 L5-M3 controlled contract

Fully controlled component — selectedKeys/onSelectionChange are REQUIRED; the wrapper holds zero internal selection state. parent MUST own the Set<string> and update it from onSelectionChange, or selection silently desyncs on re-render

Usage

Import

tsxSnippet
import { AdminBulkActionTable } from "@epasslive/design-system"

Example 1

tsxSnippet
<AdminBulkActionTable columns={cols} rows={rows} rowKey={(r) => r.id} actions={acts} selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} ariaLabel="Orders awaiting review" />

Example 2

tsxSnippet
<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

Design System v4.1.0 | ePassLive