Table
Rows and columns of data, which the user can move through with the keys, select and sort.
| Name | Kind | Size (kB) | |
|---|---|---|---|
| Beach.jpg | Image | 2400 | |
| Budget.xlsx | Sheet | 48 | |
| Notes.txt | Text | 3 | |
| Song.mp3 | Audio | 5100 |
Selected: none.
<script>
import { $bind, $memo, $signal } from 'brumate/signals';
import Cell from '@brumate/ui/Cell.brm';
import Checkbox from '@brumate/ui/Checkbox.brm';
import Column from '@brumate/ui/Column.brm';
import Row from '@brumate/ui/Row.brm';
import Table from '@brumate/ui/Table.brm';
import TableBody from '@brumate/ui/TableBody.brm';
import TableHeader from '@brumate/ui/TableHeader.brm';
import type { Key } from '@brumate/ui/collection.ts';
import type { SortDescriptor } from '@brumate/ui/table.ts';
import Icon from '../../src/components/Icon.brm';
const files = [
{ id: 'notes', name: 'Notes.txt', kind: 'Text', size: 3 },
{ id: 'photo', name: 'Beach.jpg', kind: 'Image', size: 2400 },
{ id: 'budget', name: 'Budget.xlsx', kind: 'Sheet', size: 48 },
{ id: 'song', name: 'Song.mp3', kind: 'Audio', size: 5100 },
];
let picked = $signal<Key[]>([]);
let sort = $signal<SortDescriptor | null>({ column: 'name', direction: 'ascending' });
const rows = $memo(
sort
? [...files].sort((a, b) => {
const order =
sort!.column === 'size' ? a.size - b.size : a.name.localeCompare(b.name);
return sort!.direction === 'ascending' ? order : -order;
})
: files,
);
</script>
<Table class="ui-table" aria-label="Files" selectionMode="multiple" selectedKeys=$bind(picked) sortDescriptor=$bind(sort)>
<TableHeader>
<Column class="ui-column ui-select-column">
<Checkbox slot="selection" class="ui-box" aria-label="Select all">
<span class="ui-mark" aria-hidden="true"><Icon name="check" size=(12) /><Icon name="minus" size=(12) /></span>
</Checkbox>
</Column>
<Column id="name" class="ui-column" isRowHeader allowsSorting>Name</Column>
<Column class="ui-column">Kind</Column>
<Column id="size" class="ui-column" allowsSorting>Size (kB)</Column>
</TableHeader>
<TableBody>
#for (file of rows by file.id) {
<Row id=(file.id) class="ui-tablerow">
<Cell class="ui-cell">
<Checkbox slot="selection" class="ui-box" aria-label=(`Select ${file.name}`)>
<span class="ui-mark" aria-hidden="true"><Icon name="check" size=(12) /><Icon name="minus" size=(12) /></span>
</Checkbox>
</Cell>
<Cell class="ui-cell">{file.name}</Cell>
<Cell class="ui-cell">{file.kind}</Cell>
<Cell class="ui-cell ui-number">{file.size}</Cell>
</Row>
}
</TableBody>
</Table>
<p>Selected: {picked.join(', ') || 'none'}.</p>
<Table aria-label="Files" selectionMode="multiple" selectedKeys=$bind(picked) sortDescriptor=$bind(sort)>
<TableHeader>
<Column><Checkbox slot="selection" aria-label="Select all" /></Column>
<Column id="name" isRowHeader allowsSorting>Name</Column>
<Column id="size" allowsSorting>Size</Column>
</TableHeader>
<TableBody>
#for (file of rows by file.id) {
<Row id=(file.id)>
<Cell><Checkbox slot="selection" aria-label=(`Select ${file.name}`) /></Cell>
<Cell>{file.name}</Cell>
<Cell>{file.size}</Cell>
</Row>
}
</TableBody>
</Table>
- A
Tableholds aTableHeaderofColumnelements and aTableBodyofRowelements. Each Row has aCellfor each Column, in the same order. It needsaria-labeloraria-labelledby. - A Column with
isRowHeadermakes its cells name their rows. A Table needs one. - A Table can take
items, each with anid, and itsTableBodyanitemtemplate that renders aRowfor each, as a Tree does. WithrowHeight, or in a Virtualizer, it renders only the rows in view, and its keys read the data of all the items.getTextValuereturns the text of an item for the letters that move to a row, for the rows out of view. - A
Rowhas anid, its key. Selection is as in a GridList, on the rows:selectionMode,selectionBehavior,selectedKeys,disabledKeysandonSelectionChange. - A
Checkboxwithslot="selection"in a Cell shows whether its row is selected, and toggles it. One in a Column selects every row, or none, and shows a mixed state while some are selected. The browser counts the rows that can be selected when the page loads, so the box is right from then on, and as rows come and go. onRowActiongets the key of a row that Enter acts on, or a press when nothing is selectable.
Sorting
A Column with an id and allowsSorting sorts the table on a press, or Enter. sortDescriptor holds the column and the direction, { column, direction }, and onSortChange hears of changes. A press sorts ascending, then descending. The Table does not reorder the rows: sort them by sortDescriptor, as the demo does.
A sortable Column shows aria-sort, data-allows-sorting and data-sort-direction, which can draw an arrow:
[data-sort-direction='ascending']::after { content: ' ▲'; }
[data-sort-direction='descending']::after { content: ' ▼'; }
Elements
The Table renders a table with role="grid", a thead and a tbody with role="rowgroup", and tr rows. A Column is a th with role="columnheader". A Cell is a td with role="gridcell", or a th with role="rowheader" in the row header column. A Row shows the states of a GridListItem, and a Column shows data-pressed, data-hovered, data-focused and data-focus-visible.
Keyboard
- Tab stops at the table once, at the row or the cell that had focus last, or else the first selected row, or the first row. Tab from inside the table leaves it.
- Up and Down move between rows, and from a cell to the cell of the same column in the row above or below. Up from the first row goes to the column headers, and Down from a header to its column in the first row.
- Right from a row goes to its first cell, then along the cells, then back to the row. Left goes the other way. Right to left, they turn around. On the column headers, they move between the columns, round the ends.
- A cell with a control, such as a Checkbox or a Button, gives the focus to the control.
- Page Up and Page Down move up and down a page of rows, keeping the column of a cell.
- Home and End go to the first and the last row, or to the first and the last cell of the row. With Ctrl or Cmd on a cell, they go to the first cell of the first row, or the last cell of the last row.
- With
itemsin a windowed table, Home, End, the page keys, Ctrl+A, the box in the header, ranges with Shift and letters reach rows that are out of view. A move to one scrolls it into view, then focuses it. - Space selects the row, Shift extends a range, also with the arrow keys from the row that has focus, Ctrl+A or Cmd+A selects all the rows that are not disabled, and Escape clears the selection. A table with
disallowEmptySelectionkeeps its selection against Escape and the box in the header. Letters move to the row whose text starts with them.
Column resizing
| Name | Kind | Modified |
|---|---|---|
| Notes.txt | Text | Today |
| Beach.jpg | Image | Yesterday |
| Budget.xlsx | Sheet | Last week |
<script>
import Cell from '@brumate/ui/Cell.brm';
import Column from '@brumate/ui/Column.brm';
import ColumnResizer from '@brumate/ui/ColumnResizer.brm';
import ResizableTableContainer from '@brumate/ui/ResizableTableContainer.brm';
import Row from '@brumate/ui/Row.brm';
import Table from '@brumate/ui/Table.brm';
import TableBody from '@brumate/ui/TableBody.brm';
import TableHeader from '@brumate/ui/TableHeader.brm';
const files = [
{ id: 'notes', name: 'Notes.txt', kind: 'Text', modified: 'Today' },
{ id: 'photo', name: 'Beach.jpg', kind: 'Image', modified: 'Yesterday' },
{ id: 'budget', name: 'Budget.xlsx', kind: 'Sheet', modified: 'Last week' },
];
</script>
<ResizableTableContainer class="ui-resizable">
<Table class="ui-table" aria-label="Files">
<TableHeader>
<Column id="name" class="ui-column ui-resizable-column" isRowHeader defaultWidth=(160) minWidth=(100)>
<span tabindex="-1">Name</span>
<ColumnResizer class="ui-resizer" />
</Column>
<Column id="kind" class="ui-column ui-resizable-column" defaultWidth=(120) minWidth=(80)>
<span tabindex="-1">Kind</span>
<ColumnResizer class="ui-resizer" />
</Column>
<Column id="modified" class="ui-column">Modified</Column>
</TableHeader>
<TableBody>
#for (file of files by file.id) {
<Row id=(file.id) class="ui-tablerow">
<Cell class="ui-cell">{file.name}</Cell>
<Cell class="ui-cell">{file.kind}</Cell>
<Cell class="ui-cell">{file.modified}</Cell>
</Row>
}
</TableBody>
</Table>
</ResizableTableContainer>
<ResizableTableContainer onResize=saveWidths>
<Table aria-label="Files">
<TableHeader>
<Column id="name" isRowHeader defaultWidth=(160) minWidth=(100)>
<span tabindex="-1">Name</span>
<ColumnResizer />
</Column>
<Column id="kind">Kind</Column>
</TableHeader>
…
</Table>
</ResizableTableContainer>
- A
ResizableTableContaineraround a Table lets its columns resize. It scrolls the Table when the Table is wider than it. - In it, a Column takes
widthordefaultWidth, in pixels, a percentage, or'1fr', which shares the width that is left with the other columns.minWidthandmaxWidthbound it, in pixels. - A
ColumnResizerin a Column resizes it with a drag. When a resize starts, every column keeps its width in pixels, as in React Aria, and the Table is as wide as its columns. onResizehears of the widths while a resizer moves, andonResizeEndwhen it stops, as aMapfrom the key of a column to its width.- The resizer holds a hidden range input, which a screen reader reads as the width in pixels. Its value is the width of the column, and a change of it from assistive technology sets the width.
- A Column that sorts or has a resizer takes the focus itself first, and Enter or Space sorts it. Left and Right then move through its controls, such as the resizer, before they move to the next column.
- On the input, Enter starts a resize, the arrow keys change the width by 10 pixels, and Escape, Enter, Space, Tab or a blur end it.
- A resizer shows
data-resizing,data-resizable-direction, which isleftat the least width andrightat the most,data-hovered,data-focusedanddata-focus-visible. A Column showsdata-resizingwhile it resizes.