DataTable
A table with the furniture a list page needs, a heading, a search control, a count and paging.
A data table is the table a list page is built from: the rows and columns, and the four things
a consumer would otherwise write around them, a heading, a search control, a count of what is
showing, and a way to the next page. It is a composition rather than a second table: Table
draws the rows, SearchInput names and reports the query, Pagination moves between pages, and
EmptyState and LoadingState say why the table has no rows, or none yet.
What it does not do is decide anything about the data. data is shown in the order it is given
and columns come in the order they are listed, so sorting, filtering on the server and every
other decision stay with the consumer. It does not select rows either, but it does mark one:
selectedRowKey passes straight through to Table, so a chosen row is announced and filled
whenever the page can name it.
Usage
import { DataTable, type ColumnDef } from "@asheeui/web";
const columns: ColumnDef<Invoice>[] = [
{ id: "number", header: "Invoice", cell: (invoice) => invoice.number },
{ id: "customer", header: "Customer", cell: (invoice) => invoice.customer },
{ id: "total", header: "Total", cell: (invoice) => formatMoney(invoice.total) },
];
<DataTable
title="Invoices"
description="Everything issued this quarter."
data={invoices}
columns={columns}
rowKeyAccessor={(invoice) => invoice.id}
searchAccessor={(invoice) => `${invoice.number} ${invoice.customer}`}
pageSize={20}
/>
A column is data rather than markup: a header, which is a node, and a cell renderer that
receives the row and returns what that column shows. id is the column's key and falls back to
the column's position, so a table whose columns can be reordered should state it. rowKeyAccessor
gives every row a stable identity, so a row keeps its element, and its focus, as the data around
it changes; without it a row is keyed by its position. onRowClick is what makes the rows
interactive: with it, a row answers to a press and to Enter, and reports the row it
was given.
title and description are drawn above the toolbar as a heading and a sentence under it.
toolbar is the slot beside the search control, for the filters a page needs and the table
cannot guess, and as changes the element the wrapper renders as.
variant, size, color and radius are the table's own treatment and are handed straight to
Table, so they resolve through the table's cascade rather than a second one, the Props
section below says which tier supplies each.
Searching
The search control is drawn only when there is something to search: searchAccessor, which
reads the text a row is matched by, or onSearchChange, which reports the query and leaves the
filtering to the consumer. A field that could not search would be a control that does nothing,
so searchable is not enough on its own, the registered true is permission, and an accessor
is the reason.
With searchAccessor the table filters the rows it was given, comparing the accessor's text
case-insensitively. With onSearchChange the table reports each query and draws the rows it is
handed next, which is what a consumer that searches on the server wants; give it both and the
consumer's rows are filtered again on the way in, so a server search states onSearchChange
alone.
A new query is a new result set, so the table goes back to the first page: an uncontrolled
table resets its own page, and a controlled one reports onPageChange(1) alongside the query.
searchLabel, registered as Search, names the field and is its placeholder, which is why a
product that searches something other than everything states it once in configuration.
Nothing matching is a state rather than a failure: the table draws EmptyState with
emptyTitle, registered as No results, and emptyDescription, registered as
Try a different search, or clear it to see everything.
Paging and what the table says about itself
paginated is registered true and pageSize 10, so a table of forty rows is four pages
without a word of configuration. page makes the page the consumer's; defaultPage, whose own
default is 1, is where an uncontrolled table starts, and onPageChange reports where a reader
asked to go. The paging controls are drawn only when there is more than one page to draw.
Under the table the count states what is showing, Showing 1 to 10 of 40. It is a sentence a
reader is told rather than a control they operate, so it is a template: rowCountLabel under
components.datatable, or on the instance, with {first}, {last} and {total} filled in. The
default wording is the sentence above, so nothing changes unless a product wants its own.
showRowCount (registered true) hides it, and a table that shows neither the count nor the
paging has no footer at all.
isLoading is the state a table is in while its rows are on their way: the rows are replaced by
a loading region rather than shown as an empty table, because an empty table says the wrong
thing about a request that has not finished. That region names what is loading, loadingLabel,
registered as Loading results, so a waiting table announces more than the framework's generic
Loading. The footer goes with the rows, so a count of zero is never announced while the count is
still unknown.
The chosen row
selectedRowKey passes straight through to Table, so a data table marks the row the page has
chosen, filled for the eye, aria-current for a reader, without managing the selection itself.
The page decides what a press means; the table only says which row is the answer.
Props
The registered values are the behaviour: searchable true, searchLabel Search,
searchDebounce 0, paginated true, pageSize 10, showRowCount true, rowCountLabel
the sentence above, loadingLabel Loading results, and the two empty-state strings. All ten live
under components.datatable, so a product that pages by fifty, searches invoices instead of rows or
calls its wait something else states it once.
variant, size, color and radius are not registered by this component. They are
passed to Table, which resolves them through its own cascade: size and variant from
components.table (registered as md and grid), color from the application's
defaultColor, radius from its defaultRadius, and the table's own fallbacks of primary
and md behind those. That is why restyling components.table restyles every data table, and
why a data table and a bare table on the same page are dressed alike.
data, columns, title, description, caption, selectedRowKey, searchAccessor,
onSearchChange, rowKeyAccessor, onRowClick, page, defaultPage, onPageChange,
rowCount, isLoading, toolbar and as are instance-only. rowCount is what a remote list
states its total with, so the pager and the count sentence know how many rows exist while the
table holds one page. The rows, the columns, the size of the result set, the chosen row and what a
press on one means are statements about one screen; none of them is a statement about the design.
Accessibility
- The rows are a real
<table>with<th scope="col">header cells, so a reader can move between cells and hear which column a value belongs to. A row that can be activated is in the tab order and answers to Enter and Space; a row that cannot is not. - The count is a
role="status"region, so a query or a page change is announced asShowing 11 to 20 of 40without moving focus. An empty result is announced twice on purpose: the count becomesShowing 0 to 0 of 0, and the empty presentation is also a status while it answers a query, so the sentence that explains it is heard too. A table that is empty on first paint stays quiet, because nothing changed. - The table names itself: the heading is wired to it with
aria-labelledby, orcaptionnames it when a page would rather state the name inside the table. - The search field is named by
searchLabel, the same string it shows as a placeholder, so it is announced as a labelled field rather than as a placeholder alone; the control that empties it returns focus to the field. - The paging controls are named,
Previous page,Next page,Page 3, and the current one carriesaria-current="page". WhileisLoading, arole="status"region stands in for the rows, labelled withloadingLabel,Loading resultsunless the product states otherwise, so a waiting table is announced as waiting and says what it is waiting for.
Native
@asheeui/native renders this contract as RowList, because a phone cannot lay out columns side
by side and read them as a row: each column becomes a labelled line inside a Card, onRowPress
replaces onRowClick, and selectedRowKey marks the row a reader has chosen, on both
platforms. components.datatable configures the two alike, so the search wording, the page size,
the count sentence and the empty state are stated once. See React Native.