AdminDataTable
ImplementedPlain admin data table with optional row navigation — Wave-3 L5-M3 thin composition over the canonical DataTable organism. The historical Admin column API is preserved (id / cell / width / align — no sorting, no selection); DataTable owns the table chrome (header rhythm, row hover, responsive overflow, accessible empty state). Its distinctive feature is row navigation: rowHref(row) makes the FIRST cell a real <a> whose ::after stretches over the whole row, so the entire row is clickable while the accessible name, keyboard focus, middle-click and open-in-new-tab all come from a genuine link rather than a click handler on <tr>; onRowActivate(row, event) is the SPA hook for unmodified primary clicks. Exported from the barrel along with AdminDataTableProps and AdminDataTableColumn.
components/organisms/AdminDataTable/AdminDataTable.tsxPlain admin data table with optional row navigation — a thin composition over the canonical DataTable organism that keeps the historical Admin column API (id/cell) while DataTable owns the table chrome and the accessible empty state; rowHref turns each row into a real whole-row anchor.
AdminDataTable
Organism — Admin family
Wave-3 L5-M3 composition over the canonical DataTable organism. The historical Admin column API is preserved (id / cell / width / align — no sorting, no selection). rowHref(row) makes the FIRST cell a real anchor whose ::after stretches over the whole row, so the entire row is clickable while the accessible name, keyboard focus, middle-click and open-in-new-tab all come from a genuine link; onRowActivate(row, event) is the SPA hook for unmodified primary clicks.
Every row is a real link — click anywhere on one, or Tab into it and press Enter. Middle-click and open-in-new-tab work natively.
| Private link | URL | Clicks |
|---|---|---|
| Board promo | epass.live/p/board26 | 412 |
| Newsletter | epass.live/p/news | 1204 |
| Partner portal | epass.live/p/partner | 87 |
No activation yet — click a row (unmodified primary click).
This component supports light and dark mode.
Usage
AdminDataTable is generic over its row type — define columns with a cell renderer per column, pass rows and a stable rowKey, and add rowHref when each row navigates to a detail page. The design system is framework-agnostic: rowHref governs the link semantics while onRowActivate lets your router take over unmodified clicks — the preview above wires both to keep them observable.
import { AdminDataTable } from '@epasslive/design-system'
const columns: AdminDataTableColumn<LinkRow>[] = [
{ id: 'label', header: 'Private link', cell: (r) => r.label },
{ id: 'url', header: 'URL', cell: (r) => <code>{r.url}</code> },
{ id: 'clicks', header: 'Clicks', align: 'right', cell: (r) => r.clicks },
]
<AdminDataTable
columns={columns}
rows={rows}
rowKey={(r) => r.id}
ariaLabel={t('privateLinks')}
emptyMessage={t('noLinksYet')}
rowHref={(r) => `/admin/links/${r.id}`}
onRowActivate={(r) => router.push(`/admin/links/${r.id}`)}
/>Row-navigation contract
rowHref renders the first cell as a genuine <a> whose ::after stretches across the row — clicking anywhere navigates, while focus, the accessible name, middle-click and open-in-new-tab come from the anchor itself. onRowActivate fires only for unmodified primary clicks, so ⌘/Ctrl-click and middle-click keep the native anchor behaviour. Return undefined from rowHref for rows that must stay non-navigable.
Accessibility
Table chrome comes from the canonical DataTable organism: th scope="col" on every column (WCAG 1.3.1), the table named by ariaLabel (WCAG 4.1.2), and the empty state rendered in a role="status" / aria-live="polite" cell. Navigable rows are real anchors — keyboard users Tab into the link and modified clicks work natively (WCAG 2.1.1).
Props
columns*AdminDataTableColumn<T>[]Column definitions — see the column table below.
rows*T[]Row data — one item per row, typed by the generic.
rowKey*(row: T, index: number) => stringStable React key per row.
captionstringTable caption — renders visibly when set; ariaLabel doubles as the sr-only caption otherwise.
emptyMessagestring"No data to display"Rendered in the empty state cell (role="status", aria-live="polite").
ariaLabelstring"Data table"Accessible name for the table — also feeds the caption.
classNamestringClass on the rendered table wrapper.
idstringStable id for the scroll wrapper (auto-generated adt-<useId()> fallback).
rowHref(row: T) => string | undefinedDestination per row — the first cell becomes a whole-row anchor. Return undefined to leave a row non-navigable.
onRowActivate(row: T, event: React.MouseEvent) => voidSPA hook for unmodified primary clicks; rowHref still governs semantics and modified clicks.
Column definition
AdminDataTableColumn<T> — the historical Admin column API: plain cell renderers, no sort config.
id*stringColumn identifier.
header*stringHeader label.
cell*(row: T) => React.ReactNodeCell renderer — receives the original row object (format here, not in the data).
widthstringCSS width for the column.
align"left" | "right" | "center"Cell + header alignment.
Props
AdminDataTable
columnsAdminDataTableColumn<T>[]—Column definitions — id / header / cell / width / align. See AdminDataTableColumn below.
rowsT[]—Row data — one item per row, typed by the generic.
rowKey(row: T, index: number) => string—Stable React key per row. Doubles as the selection key when this row is composed into a selection-bearing wrapper.
captionstring—Visible table caption. When set it renders via DataTable's captionVisible path; otherwise ariaLabel doubles as the sr-only caption.
emptyMessagestring'No data to display'Empty-state cell copy (role=status / aria-live=polite via DataTable). EN default — FR consumers pass 'Aucune donnée à afficher'.
ariaLabelstring'Data table'Accessible name for the table — also feeds the sr-only caption fallback. FR: 'Table de données'.
classNamestring—Additional CSS classes on the rendered DataTable wrapper.
idstringadt-<useId()>Stable id for the scroll wrapper (auto-generated adt-<useId()> fallback). Useful for aria-controls wiring from a parent search or filter.
rowHref(row: T) => string | undefined—Destination per row. When supplied, the FIRST cell renders a real anchor whose ::after stretches over the whole row, so the entire row is clickable while the accessible name, keyboard focus, middle-click and open-in-new-tab all come from the anchor itself (WCAG 2.1.1). Return undefined to leave an individual row non-navigable.
onRowActivate(row: T, event: React.MouseEvent) => void—Optional SPA hook — fires only for an unmodified primary click so a router can take over; rowHref still governs semantics and modified clicks (⌘/Ctrl/Shift/Alt, middle-click). DS is framework-agnostic (no next dep) — the consumer owns the actual navigation.
<AdminDataTable />Accessibility
ARIA
2 rulesUsage
Import
import { AdminDataTable } from "@epasslive/design-system"Example 1
<AdminDataTable columns={cols} rows={rows} rowKey={(r) => r.id} ariaLabel="Private links" emptyMessage="No private links yet — create one above." />Example 2
<AdminDataTable columns={cols} rows={rows} rowKey={(r) => r.id} ariaLabel="Tickets" rowHref={(r) => `/admin/tickets/${r.id}`} onRowActivate={(r) => router.push(`/admin/tickets/${r.id}`)} />Anti-patterns
— Do not fetch data directly in organisms
— Avoid tight coupling to specific data sources
— Do not attach onClick to <tr> or <td> for row navigation — that discards the anchor semantics rowHref exists to provide (keyboard focus, middle-click, open-in-new-tab)
— Do not reach for this organism when you need per-column sorting (use AdminSortableTable), bulk selection (use AdminBulkActionTable), search filtering (use AdminFilterableTable) or pagination (use AdminPaginatedTable) — those are sibling compositions over the same DataTable