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/1 is 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 roving tabindex — you get platform behavior.

  • data_grid/1 is an interactive role="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, use table/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:

KeyAction
Arrow keysMove cell focus one cell in that direction
Home / EndFirst / last cell in the row
Ctrl+Home / Ctrl+EndFirst / last cell in the grid
Enter / F2Enter edit mode for the focused cell
EscapeCancel 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))}
end

The 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

data_grid(assigns)

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 :col slot. Defaults to [].
  • caption (:string) - accessible name for the grid (aria-label). Defaults to nil.
  • active_row (:integer) - 0-based row index that holds roving focus. Defaults to 0.
  • active_col (:integer) - 0-based column index that holds roving focus. Defaults to 0.
  • 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.

empty_state(assigns)

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 to nil.
  • 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.

filter_bar(assigns)

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 to nil.
  • 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 to false.
  • 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 (use filter_chip/1).

filter_chip(assigns)

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 to nil.
  • value (:string) - sent as phx-value-key with the remove event. Defaults to nil.
  • Global attributes are accepted.

Slots

  • inner_block - optional custom content in place of label.

table(assigns)

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 :col slot. Defaults to [].

  • caption (:string) - table caption; the accessible name/description of the table. Strongly recommended. Defaults to nil.

  • caption_visible (:boolean) - when false the caption is kept for AT but visually hidden. Defaults to true.

  • selectable (:boolean) - adds a leading checkbox column + select-all. Defaults to false.

  • responsive (:string) - Narrow-viewport strategy. scroll keeps the table and lets it scroll horizontally inside a focusable region. stack collapses each row into a labelled card below the --stack breakpoint (cells expose data-label from the column label).

    Defaults to "scroll". Must be one of "scroll", or "stack".

  • loading (:boolean) - renders shimmer skeleton rows and sets aria-busy. Defaults to false.

  • loading_rows (:integer) - how many skeleton rows to show while loading. Defaults to 5.

  • sort_by (:string) - the key of the currently sorted column. Defaults to nil.

  • sort_dir (:string) - asc or desc for the sorted column. Defaults to nil.

  • sort_event (:string) - phx-click event name emitted by sort buttons. Defaults to "sort".

  • selected (:list) - ids (matched against row_id) of selected rows. Defaults to [].

  • row_id (:any) - 1-arity fun row -> id; falls back to the row index. Defaults to nil.

  • all_selected (:boolean) - checks the select-all box. Defaults to false.

  • 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 when rows is empty and not loading.
  • error - include only in the error state; replaces the table body with an error region.