[C07] Data Table (component)

Summary

C07 is a component spec, not a screen — distinct from every other entry in this catalogue. It is the canonical generic Data Table for the admin portal: the single source of truth for tabular UI, designed so every list page (E02 Event Participants, future Members, Affiliates, Results, Audit log, T02–T05 Number Stock, etc.) mounts <DataTable> rather than re-implementing rows, selection, and bulk chrome.

The bulk-engaged shell — "variant E" from the bulk toolbar exploration — is locked: 1.5px indigo border + 4px outer glow on the wrapper, plus a bonded DTBulkBar welded to the table’s top edge. Operators can never miss the actions; the actions are spatially welded to the rows they act on. Page chrome above the table (page header, stats strip, filter chips, utilities row) is never modified by the table — no layout jitter on enter/exit.

This .adoc is the persistent contract; the design at :design-url: is the visual reference. It is built as ap-data-table in the admin-portal SPA (app/shared/data-table/; eventual home @ems/shared-ui per WS4 of the admin-portal greenfield plan), and every list in the portal renders its table through it: E02 Event Participants, C10 Orders List, T02 Number Stock, T09 Stock Activity and the T05 reports.

Actor & Context

Actor: indirect — operators interact with C07 via every list-page consumer; the component itself has no actor of its own. Frequency: every list-page render. Precondition: the consumer has a row dataset + a column declaration; the backend provides server-side sort/paginate where applicable (C07 is display-only at the substrate level). Entry point: mounted by the consumer screen as <DataTable> (or whichever stack-appropriate name lands in the implementation — the prop contract is the canonical part).

API Surface

The contract is stated in the terms the implementation uses: Angular signal inputs and outputs, with caller-owned cells as templates. The table holds no state of its own. Sort, filters, selection and paging belong to the list, which keeps them in the URL; the table renders them and emits what the operator asked for.

Column shape

A column is the filter substrate’s ColumnConfig<T> (see Filterable List Pattern) plus only what rendering needs. A list hands its config straight through and adds hints where a column wants them. A column that exists only on screen — a row-action column, a state pill the criteria cannot filter on — is declared the same way, with no filter and no sort.

Field Type Purpose

field

string

Unique column id; also the cell value’s path when there is no formatter.

label

string

Header text, sentence case. Empty for a row-action column, which then names itself with ariaLabel.

sort

{ alias? } (optional)

Makes the column sortable; the header shows the sort glyph. From the substrate.

filter

FilterFacet (optional)

Makes the column filterable; the header hosts the popover with the substrate’s widget for the facet. From the substrate.

formatter

(row) ⇒ string (optional)

Default cell text. From the substrate.

filterOptions

{value, label}[] (optional)

Options for an enum filter.

mono

boolean (optional)

Identifier content (numbers, codes) in the mono font at the body size.

num

boolean (optional)

Right-align numeric content.

wrap

boolean (optional)

Wrap to at most two lines instead of truncating.

width

CSS length (optional)

Fixed header width.

blank

string (optional)

Text for an empty default cell; overrides the table’s blank (default —).

title

(row, text) ⇒ string | null (optional)

Tooltip of a default cell; defaults to the cell’s text, so a truncated value can be read in full.

cellClass

string (optional)

Global utility classes for the cell.

stopRowClick

boolean (optional)

Default true for a templated cell, false for a default text cell.

DataTable inputs and outputs

Input / output Type Purpose

columns

DataTableColumn<T>[]

Column declaration (above).

rows

T[]

Row data.

rowId

(row, index) ⇒ id (optional)

Row identity; defaults to row.id, else the index.

sort, filters

the list’s TableState.sort and .filters

What the headers show: glyph, aria-sort, filter-on marker, the widget’s current value.

sortChange

output SortDescriptor[]

The next sort for a header click: asc, desc, off; shift-click stacks or steps one column in a multi-sort.

filterChange

output {field, value}

A filter set or cleared in a header popover.

loading

boolean

One full-width Loading… row.

error, errorTitle

string or null

A load failure: a role="alert" row with the reason and Try again (output retry). Never reads as an empty list.

emptyTitle, emptyHint

string

The empty row’s copy. Content projected as [empty] supplies its action (Clear filters, Show inactive, Open all orders…).

selected

Set<id> or null

Selected ids. null (the default) renders no selection column.

isRowSelectable, selectLabel

(row) ⇒ … (optional)

Rows that may be selected; each checkbox’s accessible name.

selectionChange

output Set<id>

The whole next selection, from a row checkbox, the header’s select-all-on-page, or the bar’s Clear. A list with a cap applies it here.

bulkMode

boolean

When true, the bulk bar and the surface’s border and glow. The list sets it from its own threshold.

bulkActions

{id, label, danger?, disabled?, title?}[]

The bar’s verbs; output bulkAction emits the id. A disabled verb carries its reason as title.

bulkContext

string (optional)

Context after the count, e.g. across page 1.

bulkTotal

number (optional)

Shows the Select all N pill while the selection is smaller; output selectAllMatching.

isRowClickable, rowClick

(row) ⇒ boolean, output row

Rows that may be clicked, with a pointer cursor; the click.

rowKeyboard, rowLabel

boolean, (row) ⇒ string

Also make clickable rows focusable and activatable with Enter and Space, with an accessible name. For a list whose row click is its only entry point (T09).

isRowInactive

(row) ⇒ boolean (optional)

Struck through and faint, every cell.

isRowMuted

(row) ⇒ boolean (optional)

Faded without the strike: readable but not actionable (a cancelled order, a destroyed number).

focusedRowId

id (optional)

Highlights the row whose detail panel is open.

Click-to-act mechanism

A list supplies a column’s cells as a template, <ng-template apCell="<field>" let-row>. A click inside a templated cell does not reach the row click: the table marks the cell ap-dt-stop and the row’s handler checks event.target.closest('.ap-dt-stop') before emitting. A column opts back in with stopRowClick: false. The selection checkbox cell is always a stop.

Visual Treatment

Default mode (no bulk chrome)

Typography and structure follow List Screen Affordances § Table Typography, which supersedes the canvas’s uppercase header:

  • No wrapper border of its own: the table sits in the list’s content surface, which supplies the frame.

  • Header: one treatment for every column, sortable or not — 0.75rem, weight 600, muted, sentence case. A sortable header adds the sort glyph (↕ unsorted, ↑ / ↓, numbered when several columns sort) and a filterable one the filter toggle; neither changes the label.

  • Body 0.8125rem; identifiers in the mono font at the same size; long text truncated with the full value as a tooltip; at most two lines per row.

  • Hover: pointer cursor where the row is clickable; row tint on hover.

  • Selected and focused rows: the accent-soft tint.

  • Inactive row (isRowInactive): strikethrough and faint across every cell. Muted row (isRowMuted): faded only.

Bulk-engaged mode

  • Content surface: accent border read as 1.5px + 4px outer glow at 10% accent. The table sets it on the surface it sits in, drawn as box-shadow rings over the surface’s 1px border so entering bulk mode shifts nothing. A table outside a surface carries the treatment itself.

  • Bulk bar bonded to the table’s top edge, inside the surface: accent background, inverse type, ~10px x 14px padding.

    • Indeterminate checkbox (white-stroked) on the left; it clears the selection.

    • <count> selected label, plus optional context label (· across page 1).

    • Select all N pill (background rgba(255,255,255,0.10)) when count < bulkTotal.

    • Action buttons right-aligned: white text, rgba(255,255,255,0.10) background, 1px translucent-white border. Danger actions outlined in soft red rgba(255,180,180,0.55) instead.

    • Clear button (Clear ✕) at the far right, button-less, separated by a thin vertical divider.

  • Header row + body rows: unchanged from default mode.

  • Selected rows: tint stays accent-soft (lighter than the bar) — the bar provides the selection-mode signal; row tint is gentle.

  • The page chrome above the surface — command row, filter band — does not change. The list’s page actions stay where they are; the verbs are on the bar.

Out of Scope (today)

  • Column resize and hide/show (FU-3, FU-4).

  • Sticky headers (FU-1). Not built with the component: the table scrolls horizontally inside a table-responsive container, which defeats position: sticky against the page, so a sticky header needs the table to own a fixed-height scroll container — a layout decision, not a style.

  • A server-side sort and paginate adapter. The table emits the next sort and the list writes it to the URL; pagination is the list’s pagination bar, outside the table. See Design Decisions.

  • Side panels and modals — summoned by the list from a cell or a row click, never owned by C07.

  • Touch / mobile responsive treatment (FU-6).

  • Row-selection keyboard interaction — Space to select, Shift+click for a range, arrow-key navigation (FU-5). Clickable rows can opt into Enter and Space activation (rowKeyboard); every control in a cell is an ordinary focusable button or link.

  • Sibling dimming around the focused row. The focused row is highlighted; its siblings are not dimmed.

Design Anchors

  • Portal Pattern

  • UI Design Principles

  • design-journal/2026-04/jhipster-filterable-data-table.adoc — table substrate pattern (filter / sort / pagination shared layer); C07 is the visual substrate, the headless adapter is the data substrate. Eventual composition.

  • design-journal/2026-04/admin-portal-screen-design-prompt-iteration.adoc — broader handoff workflow.

  • E02 Event Participants — first canonical consumer; the design prototype was driven by E02’s column set.

  • C04 Reassignment Dialog — modal pattern that calling screens summon via onRowClick.

  • _explore-bulk-toolbar/Bulk Toolbar Variants.html (in the design bundle) — the placement exploration that picked variant E.

Design Decisions

  • Component, not screen (2026-04-30). C07 is the only entry in this catalogue that is a component spec rather than a screen use-case. The C-prefix is allocated because the catalogue’s namespace covers "anything cross-cutting"; component contracts qualify. Distinct from every other Cxx in two ways: no actor flow, no :design-url: of its own when accessed via a consumer screen.

  • Locked bulk treatment — variant E (2026-04-30). 1.5px indigo border + 4px outer glow + bonded DTBulkBar. Five placements were compared (header swap, neutral bar above filters, indigo bar above filters, floating dock, table-attached glow); E was chosen for the strongest spatial link between actions and rows.

  • Mode toggle = bulkMode prop (2026-04-30). Caller-controlled, not derived from row count. Page chrome above the table is never modified by C07 — no layout jitter on enter/exit. Calling screens decide when to engage bulk mode based on their own selection-count rules (e.g. E02 engages at ≥2 selected; another consumer might engage at ≥1).

  • Click-to-act cells are caller-owned (2026-04-30). Cell renderers handle their own clicks; C07 provides the dt-stop mechanism so row-click bubbling is suppressed cleanly. E02's four click levels (row → side panel; Category → category dialog; Primary Number → C04; Order chip → C06 modal/popover) are all expressible without C07 coupling.

  • Sort + pagination are display-only (2026-04-30). C07 renders the sort indicator chevron; the consumer owns sort state + the data fetch. C07 renders no pagination chrome at all; the consumer renders pagination outside the wrapper. Server-side adapters are deferred until a consumer needs them.

  • Select all N pill is caller-driven (2026-04-30). C07 renders the pill when bulkTotal is provided AND count < bulkTotal. The action behind the pill (calling a "select-all-matching" backend endpoint, expanding the selection set, etc.) is the consumer’s job. C07 just exposes the affordance.

  • Selected row tint distinct from bar (2026-04-30). Selected rows tint to #f8f7ff — lighter than the bonded indigo BulkBar #4f46e5. The bar provides the unmistakable mode signal; row tint is gentle so the table stays scannable.

  • Inactive treatment is row-wide (2026-04-30). isRowInactive returning true strikes through and fades the entire row, every cell. Click handlers (row click, cell-render handlers) stay wired — operators may need to interact with cancelled-order chips on inactive EPs, etc.

  • Built on the substrate’s column, not beside it (2026-10-16). The C07 column extends the filter substrate’s ColumnConfig<T>, and the table hosts the per-column filter popovers itself. Each list passes its config through, so a column is declared once for the query, the chips and the table. The alternative — a C07-shaped column and a wrapper bridging the two — kept two declarations of every column in step for no gain.

  • Sort is emitted, not held (2026-10-16). A header click emits the next sort (asc → desc → off, shift-click stacks); the list writes it to the URL and passes it back. The cycle lives once in the table rather than in each list, and the list remains the only owner of sort state.

  • Bulk glow on the surface, command row untouched (2026-10-16). The table sits inside the list’s content surface and has no border of its own, so the bulk border and glow go on the surface, drawn so that nothing moves. The bulk verbs are on the bonded bar only; the command row keeps its page actions rather than swapping in a second copy of the same verbs away from the rows.

  • One failure row, distinct from empty (2026-10-16). A load failure renders as an alert row with Try again, never as the empty state, because an operator who cannot tell an outage from "there is nothing here" concludes there is nothing to act on.

Consumers

Consumer Status

E02 Event Participants

Built on C07. View, order chip, and the click-to-change Category (C08) and Number (C04) cells are caller templates. Bulk mode at ≥2 selected; the verbs are present and disabled until their flows exist.

C10 Orders List

Built on C07. Number link, buyer, status pill and WooCommerce link are caller templates; a cancelled order is a muted row; a load failure is the error row.

T02 Number Stock

Built on C07. Bulk mode at ≥1 selected, with Return, Flag UNFIT and Dispose gated by the selection’s states and the 200-row cap held by the list. Locked rows are muted, unselectable and not clickable.

T09 Stock Activity

Built on C07. Rows open their batch by click, Enter or Space.

T05 Stock Reports

Built on C07 through ap-report-table, a thin variant that maps each report column (shared with the CSV export) onto a C07 column and renders state columns as the stock state pill.

E04 Event Results, M02 Membership Members, T03, T04, future Audit Log and Affiliations lists

Future.

Future Use Cases

Forward-looking. Not in v1. Captured here so the design intent isn’t lost and so v1 stubs in the right places (or doesn’t, where the absence is the intent).

FU-1 — Sticky table header

Long lists (E02 with 10k+ participants, E04 with thousands of results) need a sticky header row so the column titles stay visible during scroll. Out of v1 scope per the design. Trigger: first consumer that hits a "I lost track of what column I’m looking at" UX problem on real production data.

FU-2 — Server-side sort + paginate adapter

Today C07 is display-only on sort + has no pagination chrome at all. Consumers wire their own. A v2 adapter — declarative sort prop with caller-provided (column, direction) ⇒ void callback, plus a pagination slot or built-in chrome — would reduce per-consumer plumbing.

The headless filter substrate at design-journal/2026-04/jhipster-filterable-data-table.adoc already prototypes the JhipsterQueryAdapter (toApiQueryParams / toUrlQueryParams / fromRoute) with 35 unit tests. C07 v2’s server-side adapter should compose with that adapter rather than reinvent it.

FU-3 — Operator-controlled column visibility settings

Mirrors E02's FU-7 (captured there because E02 is the first screen to surface the need). Implementation belongs at the C07 level so every consumer benefits — settings live with the component substrate, not per-screen.

A "Column settings" affordance (gear icon / table header utility) lets the operator hide/reveal columns from the canonical set. Persisted per-user, per-screen.

Distinct from the structural displayListField mechanic (which hides a column when, e.g., no event has team-based registrations) — that’s structure-driven; FU-3 is operator-driven. Also distinct from filter-driven hiding: a column whose filter is equals is hidden by default, because every row holds the same value (Filterable List Pattern → Visibility). The column chooser may show such a column again, and an export always carries it.

FU-4 — Column resize + column reorder

Power-user affordance. Hide behind a settings flag if/when surfaced.

FU-5 — Keyboard interaction

Space/Enter to select a focused row; Shift+click for range select; arrow keys to navigate rows. Accessibility-grade UX — defer until a screen-reader pass is on the roadmap.

FU-6 — Touch / mobile responsive treatment

Today’s design assumes desktop. Mobile / tablet operators (event-day check-in scenarios per the field-ops use cases) need a different layout — likely card-per-row with affordance-on-tap. Trigger: mobile-first screen surfaces in the roadmap.

FU-7 — Bulk-action overflow menu

The current design shows 4 bulk actions inline; what happens at 6, 8, 10? An overflow ⋯ menu pattern is the obvious answer but hasn’t been designed. Trigger: first consumer with > 5 bulk actions.

Notes

Open points from the design pass, answered

The first design pass left these for a second one. Building the component answered most of them; what remains open is in Future Use Cases.

  1. Stale E02 column set in the canonical example (a Secondary Number column). Concerns the example data only; the component is unaffected, and E02 renders the columns its own config declares.

  2. Sort indicator in both directions. Built: ↕ unsorted, ↑ / ↓ for the active direction, numbered by priority when several columns sort, and aria-sort on every sortable header.

  3. Select all N needs a backend path. The contract stands as designed: C07 renders the pill when bulkTotal exceeds the selection and emits selectAllMatching; acting on every match — an ids-only fetch, or a bulk action that accepts a filter expression — is the list’s. No list wires it yet, because no list endpoint offers either path. T02 shows its cap in bulkContext instead.

  4. Sticky header trigger. Deferred; see Out of Scope for why it is not a style change.

  5. Composition with the headless substrate. Settled as option (a): C07’s column extends the substrate’s ColumnConfig<T> with rendering hints, and the table itself hosts the filter popovers with the substrate’s widgets. The lists keep TableState in the URL through the JHipster query adapter and pass sort and filters down; there is no bridge layer and no parallel substrate. The table’s rows run through a TanStack table in manual mode, so a later client-side feature lands on its row model.

  6. Column visibility settings (FU-3). Open.

  7. Bulk-action overflow (FU-7). Open; no list has more than three verbs.

  8. Keyboard and accessibility pass (FU-5). Partly answered: sortable headers and filter toggles are buttons, selection checkboxes are named per row, aria-sort is set, clickable rows can be keyboard-activatable. Selection by keyboard and arrow-key navigation remain open.

  9. Empty-state variants. Callers discriminate upstream: the list supplies the title and hint for its situation (nothing yet, filtered to nothing, scope unresolved) and projects the matching action. A load failure is a separate error row, so it can never read as an empty list.

  10. Touch / mobile treatment (FU-6). Deferred.