Skip to main content

AdminSortableTable

Implemented

Sortable admin data table — Wave-3 L5-M3 thin composition over the canonical DataTable organism that keeps the historical Admin column API (accessor / cell, sortable-by-default columns, numeric data anchors, hideBelow responsive pruning). DataTable owns the three-state sort cycle (none → asc → desc → none), aria-sort semantics, keyboard-accessible header buttons (Tab / Enter / Space), row hover and the accessible empty state (role=status / aria-live=polite). Columns are sortable BY DEFAULT (sortable: false opts out — the inverse of DataTable's opt-in convention). Re-exports SortDirection for legacy barrel consumers. Exported from the barrel along with AdminSortableTableProps and AdminSortableTableColumn.

Categoryorganisms
Sourcecomponents/organisms/AdminSortableTable/AdminSortableTable.tsx
AI HintComposes the canonical DataTable organism. Use for admin tables with per-column sorting — columns are sortable BY DEFAULT (sortable: false opts out — the inverse of DataTable's opt-in convention). Reserve the opt-out for filter-dimension columns (channel, status) where sorting would mislead. Date accessors normalize to epoch millis in the adapter, so Date columns sort chronologically without a custom comparator. Mark figure columns numeric: true — prices, quantities and dates rendered as figures get the PRIMARY font slot (Inter) + tabular-nums (ADR-030 A2) instead of inheriting SECONDARY (Space Grotesk). EN-default i18n is the OPPOSITE convention from this one — DataTable and AdminSortableTable default FR (house alignment with the underlying DataTable's emptyMessage / selection labels); the three other Admin* wrappers default EN. EN consumers pass their own per the JSDoc recipes. Re-exports SortDirection so legacy barrel consumers keep their import path. Exported from the barrel along with AdminSortableTableProps and AdminSortableTableColumn.

Sortable admin data table — a thin composition over the canonical DataTable organism that keeps the historical Admin column API (accessor/cell, sortable-by-default columns, numeric data anchors, hideBelow responsive pruning) while DataTable owns the sort cycle, aria-sort semantics, keyboard-accessible header buttons and the accessible empty state.

AdminSortableTable

Organism — Admin family

Wave-3 L5-M3 composition over the canonical DataTable organism. The historical Admin column API is preserved: accessor(row) feeds the sort comparator (Dates compare as epoch millis), cell(row) renders the display value, columns are sortable BY DEFAULT (sortable: false opts out — the inverse of DataTable’s opt-in convention), numeric marks data-anchor columns (PRIMARY slot + tabular-nums, ADR-030 A2), and onRowClick makes rows interactive (role="button", Enter/Space).

Click a header to re-sort (Enter / Space works too — each sortable header is a button). Rows are interactive: activate one to select it.

Ventes de billets par événement, triées par brut
Yelle en spectacle640$35,200.002026-09-02
Soirée Jazz312$12,012.002026-09-01
Souper de Noël96$9,120.002026-08-30
Nuit du Piano203$6,090.002026-09-02
Bal des Boys148$4,440.002026-08-28

Aucune ligne sélectionnée — activez-en une avec Entrée ou Espace.

This component supports light and dark mode.

Usage

AdminSortableTable is generic over its row type — define columns with accessor (feeds the sort comparator; Dates compare as epoch millis) and optional cell (display formatting), pass rows and a stable rowKey, and the component owns the sort cycle from there. Columns are sortable by default; opt filter-dimension columns out with sortable={false}. Wire onRowClick to make rows interactive — the preview above keeps the selection in a polite live region.

import { AdminSortableTable } from '@epasslive/design-system'

const columns: AdminSortableTableColumn<SalesRow>[] = [
  { id: 'event', header: 'Event', accessor: (r) => r.event },
  { id: 'channel', header: 'Channel', accessor: (r) => r.channel, sortable: false, hideBelow: 'sm' },
  { id: 'qty', header: 'Tickets', accessor: (r) => r.qty, numeric: true },
  { id: 'gross', header: 'Gross', accessor: (r) => r.gross, numeric: true, cell: (r) => fmtCad(r.gross) },
  { id: 'soldAt', header: 'Last sale', accessor: (r) => r.soldAt, numeric: true,
    cell: (r) => r.soldAt.toLocaleDateString('en-CA') },
]

<AdminSortableTable
  columns={columns}
  rows={rows}
  rowKey={(r) => r.id}
  defaultSort={{ columnId: 'gross', direction: 'desc' }}
  ariaLabel={t('salesByEvent')}
  onRowClick={setSelected}
/>

Column conventions

Keep accessor primitive (string / number / Date) — it feeds the comparator; format in cell, which receives the original row object. Mark figure columns numeric so they render in Inter with tabular-nums and align down the column (ADR-030 A2). Use hideBelow for secondary columns that should prune on narrow viewports. When you need selection checkboxes, row grouping or rowHref navigation, compose the canonical DataTable directly — the Admin wrapper intentionally does not expose those props.

Accessibility

Every sortable header is a real <button> — Tab reaches it and Enter / Space cycles none → ascending → descending → none, with aria-sort declared on the sorting header and omitted on non-sortable ones (WCAG 1.3.1). With onRowClick, rows expose role="button" and activate on Enter / Space. Pass ariaLabel so the table is named (WCAG 4.1.2) — it also feeds the table caption. The empty state renders in a role="status" / aria-live="polite" cell and is announced without stealing focus.

Props

Prop
Type
Default
Description
columns*
AdminSortableTableColumn<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 (i is the pre-sort index).

defaultSort
{ columnId: string; direction: "asc" | "desc" }
—

Initial sort — uncontrolled afterwards (sorting is component-owned state).

emptyMessage
string
"Aucune donnée."

Rendered in the empty state cell (role="status", aria-live="polite"). EN consumers: pass "No data to display".

ariaLabel
string
"Tableau de données triable"

Accessible name — also feeds the table caption. EN consumers: pass "Sortable data table".

sortLabel
(header: string, direction: 'asc' | 'desc' | null) => string
(h) => `Trier par ${h}`

Accessible-name template for sort toggle buttons. Receives the column header and the current sort direction. EN consumers: pass `(h) => \`Sort by ${h}\``.

className
string
—

Class on the rendered table wrapper.

id
string
—

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

onRowClick
(row: T) => void
—

Makes rows interactive (role="button", Enter / Space activation) — receives the original row object.

Column definition

AdminSortableTableColumn<T> — the historical Admin column API, preserved verbatim by the Wave-3 composition.

Prop
Type
Default
Description
id*
string
—

Column identifier — feeds defaultSort.columnId and the sort state.

header*
string
—

Header label.

accessor*
(row: T) => string | number | Date | null
—

Value fed to the sort comparator (Dates are normalized to epoch millis) and the display fallback.

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

Custom cell renderer — receives the ORIGINAL row object (e.g. currency/date formatting).

width
string
—

CSS width for the column.

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

Cell + header alignment.

sortable
boolean
true

Sortable BY DEFAULT — pass false to opt out (filter-dimension columns). Inverse of DataTable's opt-in convention.

hideBelow
"sm" | "md" | "lg"
—

Prunes the column below the given viewport width.

numeric
boolean
—

Data-anchor columns (prices, quantities, dates as figures): PRIMARY slot (Inter) + tabular-nums (ADR-030 A2) instead of SECONDARY (Space Grotesk).

Props

Preview

AdminSortableTable

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

Column definitions — id / header / accessor (required) / cell / width / align / sortable / hideBelow / numeric. See AdminSortableTableColumn 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 — i is the PRE-SORT index.

defaultSort
{ columnId: string; direction: 'asc' | 'desc' }
Optional
—

Initial sort — uncontrolled afterwards (sorting is component-owned state). After mount the cycle is component-owned; a controlled onSort callback is not yet exposed (future DataTable enhancement).

emptyMessage
string
Optional
'Aucune donnée.'

Empty-state cell copy (DataTable's role=status / aria-live=polite). FR default — EN consumers pass 'No data to display'.

ariaLabel
string
Optional
'Tableau de données triable'

Accessible name for the table region + caption. FR default — EN consumers pass 'Sortable data table'.

sortLabel
(header: string, direction: 'asc' | 'desc' | null) => string
Optional
(h) => `Trier par ${h}`

Accessible-name template for sort toggle buttons — receives the column header and the current direction. FR default — EN consumers pass `(h) => `Sort by ${h}``.

className
string
Optional
—

Class on the rendered DataTable wrapper.

id
string
Optional
asort-<useId()>

Stable id for the scroll wrapper (auto-generated asort-<useId()> fallback).

onRowClick
(row: T) => void
Optional
—

Makes rows interactive — focusable <tr> (tabIndex=0, Enter/Space activation). The <tr> KEEPS its row role (DataTable never swaps it for role='button') so cell semantics survive.

caption
string
Optional
ariaLabel

Table caption. Defaults to ariaLabel when omitted — pass explicitly when the caption should read differently from the region's accessible name.

density
'comfortable' | 'compact'
Optional
'comfortable'

Row density — compact switches the per-cell padding to a tighter rhythm via DataTable's density prop.

striped
boolean
Optional
false

Zebra-striping — tint every odd body row with the secondary surface token (theme-swapping, AA both themes).

Quick use:<AdminSortableTable />

Accessibility

ARIA roletable

ARIA

3 rules

WCAG 1.3.1

Sortable <th> declares aria-sort ('ascending' | 'descending' | 'none'); non-sortable <th> omits aria-sort (WCAG 1.3.1)

WCAG 4.1.2

Sort toggle is a real <button type='button'> wrapping the label — Tab / Enter / Space cycle the sort state; sortLabel (FR default 'Trier par ${h}') names the button (WCAG 4.1.2)

WCAG 2.1.1

When onRowClick is set, the <tr> is focusable (tabIndex=0, Enter/Space) and KEEPS its row role — DataTable never swaps it for role='button' so cell semantics survive (WCAG 2.1.1); scroll wrapper is role='region' with focusable overflow

Usage

Import

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

Example 1

tsxSnippet
<AdminSortableTable columns={cols} rows={rows} rowKey={(r) => r.id} />

Example 2

tsxSnippet
<AdminSortableTable columns={cols} rows={sales} rowKey={(r) => r.id} defaultSort={{ columnId: "gross", direction: "desc" }} ariaLabel="Ticket sales by event" onRowClick={setSelected} />

Example 3

tsxSnippet
<AdminSortableTable columns={cols} rows={ventes} rowKey={(r) => r.id} ariaLabel="Ventes de billets par événement" emptyMessage="Aucune donnée." sortLabel={(h) => `Trier par ${h}`} onRowClick={setSelected} />

Anti-patterns

— Do not fetch data directly in organisms

— Avoid tight coupling to specific data sources

— Do not format values inside accessor — return the primitive (string / number / Date); format in cell which receives the original row object (the accessor value is the display fallback)

— Do not mark filter-dimension columns (channel, status) sortable — they mislead users (use sortable: false to opt out, the inverse of DataTable)

— Do not try to control the sort cycle from outside — sorting is component-owned state after defaultSort; a controlled onSort is not yet exposed (future DataTable enhancement)

— Do not reach for this organism when you need bulk selection (use AdminBulkActionTable), search filtering (use AdminFilterableTable) or pagination (use AdminPaginatedTable) — those are sibling compositions over the same DataTable

Design System v4.1.0 | ePassLive