AuroraUI. Components. DataNavigation
(Aurora UI v0.1.1)
View Source
Data navigation family — semantic table, an interactive data_grid, a
filter_bar (+ filter_chip), and an empty_state.
This family draws a deliberate line between two very different jobs that other kits blur together:
table/1is a semantic<table>. It is the right tool for the vast majority of tabular data: read-mostly rows, optional column sorting, row selection with bulk actions, and full loading/empty/error states. It leans entirely on native table semantics (<caption>,<th scope="col">,aria-sort) so screen readers announce structure for free. No custom focus model, no rovingtabindex— you get platform behavior.data_grid/1is an interactiverole="grid"widget for the narrow case where each cell is individually focusable and editable (a spreadsheet surface). It is heavier: it opts you out of the default reading experience and into an application keyboard model (see below), so reach for it only when cells are edited in place. When in doubt, usetable/1.
data_grid/1 keyboard model
A grid manages a single roving focus point (one cell is tabindex="0", the
rest are tabindex="-1"). The consumer's AuroraDataGrid hook (not shipped
in core — the family exposes the data-aui-grid DOM contract for you to wire)
implements:
| Key | Action |
|---|---|
| Arrow keys | Move cell focus one cell in that direction |
| Home / End | First / last cell in the row |
| Ctrl+Home / Ctrl+End | First / last cell in the grid |
| Enter / F2 | Enter edit mode for the focused cell |
| Escape | Cancel edit, restore the previous value, keep focus |
The server renders the active cell (active_row/active_col) as
tabindex="0"; the hook updates roving focus client-side without a round trip.
Sorting event contract (table/1)
Sortable headers render a real <button> inside the <th> that emits
phx-click={@sort_event} with phx-value-key set to the column's key. Your
LiveView handles it and re-assigns sort_by/sort_dir:
def handle_event("sort", %{"key" => key}, socket) do
{:noreply, assign(socket, sort_by: key, sort_dir: toggle(socket, key))}
endThe current column reflects aria-sort="ascending" | "descending"; other
sortable columns advertise aria-sort="none".
Selection event contract (table/1)
With selectable, a checkbox column is added. Row checkboxes emit
phx-click={@select_event} with phx-value-id (from row_id); the header
select-all checkbox emits phx-click={@select_all_event}. The set of selected
ids is passed back in via selected.
Summary
Functions
An interactive role="grid" for cell-by-cell focus and in-place editing.
A centered empty-state block: illustration, title, description, and a primary
action. Use inside a container that already communicates what is empty (a
card, a panel, a table state) — the block itself is role="status" so a
freshly-loaded empty result is announced.
A filter shell: controls, removable applied-filter chips, a clear-all button, and a politely-announced result count.
A single applied-filter chip. Renders as a list item so it belongs inside the
filter_bar :chips slot. When remove_event is set, a remove button with a
descriptive accessible name (Remove filter: <label>) is shown.
A semantic data table with sorting, selection, and full state coverage.
Functions
An interactive role="grid" for cell-by-cell focus and in-place editing.
This is intentionally heavier than table/1: it replaces native table reading
semantics with an application keyboard model (Arrow keys move a single roving
focus point, Enter/F2 edit, Escape cancels). Only reach for it when cells are
edited in place — otherwise table/1 is more accessible and less code.
The grid emits the data-aui-grid DOM contract; wire the roving-focus/edit
keyboard handling with your own AuroraDataGrid hook (the server keeps
active_row/active_col authoritative so focus survives a patch).
Examples
<.data_grid rows={@cells} active_row={@r} active_col={@c} caption="Budget">
<:col :let={row} label="Item">{row.item}</:col>
<:col :let={row} label="Amount" editable>{row.amount}</:col>
</.data_grid>Attributes
rows(:list) - row data; each is passed to every:colslot. Defaults to[].caption(:string) - accessible name for the grid (aria-label). Defaults tonil.active_row(:integer) - 0-based row index that holds roving focus. Defaults to0.active_col(:integer) - 0-based column index that holds roving focus. Defaults to0.- Global attributes are accepted.
Slots
col(required) - one grid column; inner block receives the row via:let. Accepts attributes:label(:string) (required)editable(:boolean) - advertises the cell as editable to AT.
A centered empty-state block: illustration, title, description, and a primary
action. Use inside a container that already communicates what is empty (a
card, a panel, a table state) — the block itself is role="status" so a
freshly-loaded empty result is announced.
Examples
<.empty_state title="No invoices yet" description="Invoices appear here once you bill a customer.">
<:icon><.receipt_icon /></:icon>
<:actions><.button phx-click="new">Create invoice</.button></:actions>
</.empty_state>Attributes
title(:string) (required) - the empty-state headline.description(:string) - supporting sentence under the title. Defaults tonil.size(:string) - overall scale of the block. Defaults to"md". Must be one of"sm","md", or"lg".- Global attributes are accepted.
Slots
icon- decorative icon or illustration (marked aria-hidden).actions- primary action(s) that resolve the empty state.
A filter shell: controls, removable applied-filter chips, a clear-all button, and a politely-announced result count.
URL state + LiveView streams
Keep filters in the URL so they are shareable and back/forward-safe, and render results into a stream so only changed rows are patched:
# router-driven: filters live in params, results in a stream
def handle_params(params, _uri, socket) do
filters = Filters.parse(params)
rows = Catalog.list(filters)
{:noreply,
socket
|> assign(:filters, filters)
|> assign(:count, length(rows))
|> stream(:rows, rows, reset: true)}
end
# a chip's remove button just patches the URL without that key
def handle_event("drop", %{"key" => key}, socket) do
{:noreply, push_patch(socket, to: ~p"/catalog?#{Filters.drop(socket.assigns.filters, key)}")}
end
<.filter_bar count={@count} active?={@filters != %{}} clear_event="clear">
<.field ... />
<:chips>
<.filter_chip :for={{k, v} <- @filters} label={label(k, v)}
remove_event="drop" value={k} />
</:chips>
</.filter_bar>
<.table rows={@streams.rows} ...>...</.table>Attributes
label(:string) - accessible name for the filter region. Defaults to"Filters".count(:integer) - result count; announced politely when it changes. Defaults tonil.count_unit(:string) - noun for the count, e.g. "results", "orders". Defaults to"results".active?(:boolean) - whether any filter is applied; toggles the clear-all button. Defaults tofalse.clear_event(:string) - phx-click for the clear-all button. Defaults to"clear_filters".- Global attributes are accepted.
Slots
inner_block- the filter controls (selects, search field, …).chips- the applied-filter chips (usefilter_chip/1).
A single applied-filter chip. Renders as a list item so it belongs inside the
filter_bar :chips slot. When remove_event is set, a remove button with a
descriptive accessible name (Remove filter: <label>) is shown.
Examples
<.filter_chip label="Status: Active" remove_event="drop" value="status" />Attributes
label(:string) (required) - the chip's visible text.remove_event(:string) - phx-click emitted by the remove button. Defaults tonil.value(:string) - sent as phx-value-key with the remove event. Defaults tonil.- Global attributes are accepted.
Slots
inner_block- optional custom content in place oflabel.
A semantic data table with sorting, selection, and full state coverage.
When not to use
If each cell is individually focusable and editable in place, use
data_grid/1 instead — a <table> is not an application grid.
Examples
<.table caption="Team members" rows={@members} selectable
selected={@selected} sort_by={@sort_by} sort_dir={@sort_dir}>
<:col :let={m} label="Name" key="name" sortable>{m.name}</:col>
<:col :let={m} label="Role">{m.role}</:col>
<:col :let={m} label="Seats" key="seats" sortable numeric>{m.seats}</:col>
<:bulk_actions>
<.button variant="danger" phx-click="archive">Archive</.button>
</:bulk_actions>
<:empty>No members yet.</:empty>
</.table>Attributes
rows(:list) - the row data; each is passed to every:colslot. Defaults to[].caption(:string) - table caption; the accessible name/description of the table. Strongly recommended. Defaults tonil.caption_visible(:boolean) - when false the caption is kept for AT but visually hidden. Defaults totrue.selectable(:boolean) - adds a leading checkbox column + select-all. Defaults tofalse.responsive(:string) - Narrow-viewport strategy.scrollkeeps the table and lets it scroll horizontally inside a focusable region.stackcollapses each row into a labelled card below the--stackbreakpoint (cells exposedata-labelfrom the columnlabel).Defaults to
"scroll". Must be one of"scroll", or"stack".loading(:boolean) - renders shimmer skeleton rows and sets aria-busy. Defaults tofalse.loading_rows(:integer) - how many skeleton rows to show while loading. Defaults to5.sort_by(:string) - thekeyof the currently sorted column. Defaults tonil.sort_dir(:string) -ascordescfor the sorted column. Defaults tonil.sort_event(:string) - phx-click event name emitted by sort buttons. Defaults to"sort".selected(:list) - ids (matched againstrow_id) of selected rows. Defaults to[].row_id(:any) - 1-arity funrow -> id; falls back to the row index. Defaults tonil.all_selected(:boolean) - checks the select-all box. Defaults tofalse.select_event(:string) - Defaults to"select_row".select_all_event(:string) - Defaults to"select_all".Global attributes are accepted.
Slots
col- one column. Its inner block receives the row via:let. Accepts attributes:label(:string) - header text (also used as the stacked cell label).key(:string) - sort key sent as phx-value-key; required for sortable columns.sortable(:boolean)align(:string) - cell text alignment. Must be one of"start","center", or"end".numeric(:boolean) - right-aligns and uses tabular figures.
bulk_actions- actions shown above the table while any row is selected.empty- shown in place of rows whenrowsis empty and not loading.error- include only in the error state; replaces the table body with an error region.