AdminFilterableTable
ImplementedSearch-filtered admin data table — Wave-3 L5-M3 composition of the canonical DataTable organism with the TableToolbar molecule (search input + clear affordance). The wrapper owns the filtering semantics: filterPredicate(row, query) narrows rows client-side, a live region in the toolbar announces 'N / total matching', aria-controls wires the search input to the table wrapper, and emptyMessage (no data) vs noMatchMessage (filter miss) distinguish the two empty states. Filter chips (filters prop, forwarded to TableToolbar) are independent from the search predicate — chips handle categorical facets, the predicate handles the free-text query. Exported from the barrel along with AdminFilterableTableProps and AdminFilterableTableColumn.
components/organisms/AdminFilterableTable/AdminFilterableTable.tsxSearch-filtered admin data table — a Wave-3 L5-M3 composition of the canonical DataTable organism with the TableToolbar molecule. The wrapper owns filterPredicate, the live 'N / total matching' counter and the aria-controls wiring; distinct emptyMessage (no data) vs noMatchMessage (filter miss).
AdminFilterableTable
Organism — Admin family
Composes DataTable (table chrome, accessible empty state) with TableToolbar (search input + clear affordance). filterPredicate(row, query) narrows rows client-side, the match counter is an aria-live region, the search input carries aria-controls pointing at the table wrapper, and emptyMessage vs noMatchMessage distinguish no-data from filter-miss.
| Event | Venue | City |
|---|---|---|
| Festival Jazz MTL | Théâtre Maisonneuve | Montréal |
| OSM Symphonie n° 5 | Maison Symphonique | Montréal |
| Lion d'Or Gala | Lion d'Or | Montréal |
| Yelle en spectacle | Impériel Bell | Québec |
| Nuit du Piano | Palais Montcalm | Québec |
This component supports light and dark mode.
Usage
Pass filterPredicate matching every searchable field (name, venue, city) — the wrapper trims the query and skips filtering entirely when it is empty. Localize both empty states: emptyMessage fires when there is no data, noMatchMessage when the filter excludes everything.
import { AdminFilterableTable } from '@epasslive/design-system'
<AdminFilterableTable
columns={columns}
rows={rows}
rowKey={(r) => r.id}
filterPredicate={(row, query) =>
`${row.name} ${row.venue} ${row.city}`.toLowerCase().includes(query.toLowerCase())
}
ariaLabel="Events"
noMatchMessage="Aucun événement ne correspond à votre recherche"
/>Conventions
Filtering is client-side only — for server-driven search, compose TableToolbar + DataTable (or ListPanel) directly with your own debounced fetch. The aria-controls wiring (input → table wrapper) is derived from id; pass a stable id when you need to target the table from elsewhere.
Accessibility
The search input carries aria-controls pointing at the table wrapper and an accessible label (WCAG 4.1.3 — Status Messages): the match counter lives in an aria-live="polite" region in the toolbar, so results-count changes are announced without moving focus. The results table is named via its caption; the empty state renders in DataTable's role="status" cell.
Props
columns*AdminFilterableTableColumn<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 (index is the pre-filter position).
filterPredicate*(row: T, query: string) => booleanKeep-row test — receives the trimmed query string (skip-filter on empty is handled by the wrapper).
searchPlaceholderstring"Search…"Placeholder for the toolbar search input.
searchLabelstring"Filter rows"Accessible label for the search input.
emptyMessagestring"No data to display"Empty state when rows is empty.
noMatchMessagestring"No rows match your search"Empty state when the filter excludes every row.
ariaLabelstring"Filterable table"Accessible name — also feeds the results table caption.
classNamestringClass on the outer section wrapper.
idstringStable id (auto-generated aflt-<useId()> fallback).
matchCountFormat(filtered: number, total: number) => string(f, t) => `${f} / ${t} matching`Format the trailing live-region count when a filter is active. EN default — FR: (f, t) => `${f} / ${t} correspondance(s)`.
rowCountFormat(total: number) => string(t) => `${t} rows`Format the trailing live-region count when no filter is active. EN default — FR: (t) => `${t} ligne(s)`.
clearSearchLabelstring"Clear search"Accessible label for the clear-search button (forwarded to TableToolbar). EN default — FR: 'Effacer la recherche'.
filtersTableToolbarFilter[]Optional filter chip group for column-level faceting (forwarded to TableToolbar). Independent from filterPredicate — chips handle categorical facets, the predicate handles the free-text query.
filtersLabelstring"Filters"Accessible label for the filter chip group (forwarded to TableToolbar). EN default — FR: 'Filtres'.
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.
Props
AdminFilterableTable
columnsAdminFilterableTableColumn<T>[]—Column definitions — id / header / cell / width / align. See AdminFilterableTableColumn below.
rowsT[]—Row data — one item per row, typed by the generic.
rowKey(row: T, index: number) => string—Stable React key per row (index is the PRE-FILTER position).
filterPredicate(row: T, query: string) => boolean—Keep-row test — receives the TRIMMED query string (whitespace-only queries are skipped before the predicate is called; lowercase both sides yourself).
searchPlaceholderstring'Search…'Placeholder for the toolbar search input. EN default — FR: 'Rechercher…'.
searchLabelstring'Filter rows'Accessible label for the search input. EN default — FR: 'Filtrer les lignes'.
emptyMessagestring'No data to display'Empty state when rows is empty (no data at all). EN default — FR: 'Aucune donnée à afficher'.
noMatchMessagestring'No rows match your search'Empty state when the filter predicate excludes every row. EN default — FR: 'Aucune ligne ne correspond à votre recherche'.
ariaLabelstring'Filterable table'Accessible name for the wrapper section and the results table caption. EN default — FR: 'Tableau filtrable'.
classNamestring—Class on the outer section wrapper.
idstringaflt-<useId()>Stable id for the wrapper section (auto-generated aflt-<useId()> fallback). Pass a stable id when you need to target the table from elsewhere; aria-controls derives from it.
matchCountFormat(filtered: number, total: number) => string(f, t) => `${f} / ${t} matching`Format the live-region count WHEN a filter is active. EN default — FR: (f, t) => `${f} / ${t} correspondance(s)`.
rowCountFormat(total: number) => string(t) => `${t} rows`Format the live-region count when no filter is active. EN default — FR: (t) => `${t} ligne(s)`.
clearSearchLabelstring'Clear search'Accessible label for the clear-search button (forwarded to TableToolbar). EN default — FR: 'Effacer la recherche'.
filtersTableToolbarFilter[]—Optional filter chip group for column-level faceting (forwarded to TableToolbar). Independent from filterPredicate — chips handle categorical facets, the predicate handles the free-text query.
filtersLabelstring'Filters'Accessible label for the filter chip group (forwarded to TableToolbar). EN default — FR: 'Filtres'.
<AdminFilterableTable />Accessibility
ARIA
2 rulesUsage
Import
import { AdminFilterableTable } from "@epasslive/design-system"Example 1
<AdminFilterableTable columns={cols} rows={rows} rowKey={(r) => r.id} filterPredicate={(row, q) => `${row.name} ${row.venue} ${row.city}`.toLowerCase().includes(q.toLowerCase())} ariaLabel="Events" />Example 2
<AdminFilterableTable columns={cols} rows={orders} rowKey={(r) => r.id} filterPredicate={orderSearch} ariaLabel="Commandes" emptyMessage="Aucune donnée à afficher" noMatchMessage="Aucune commande ne correspond à votre recherche" searchPlaceholder="Rechercher…" searchLabel="Filtrer les commandes" clearSearchLabel="Effacer la recherche" matchCountFormat={(f, t) => `${f} / ${t} correspondance(s)`} rowCountFormat={(t) => `${t} commande(s)`} filters={statusChips} filtersLabel="Filtres" />Anti-patterns
— Do not fetch data directly in organisms
— Avoid tight coupling to specific data sources
— Do not filter inside cell renderers — use filterPredicate against the rows array (cells render the post-filter view)
— Do not use this organism for server-driven search — compose TableToolbar + DataTable (or ListPanel) directly with your own debounced fetch
— Do not conflate emptyMessage and noMatchMessage — localizing both is mandatory; they diagnose two different states for screen-reader users
— Do not hide the clear-search affordance (it is part of the TableToolbar contract — the clearSearchLabel prop exists so FR consumers can localize the aria-label)