gridlist
Three files. Arrow down to a row, then press Enter to reach its buttons.
import { gridlist } from '@markless/ui';
const list = 'display: grid; gap: 0.4em; inline-size: min(30rem, 100%)';
const cell = 'display: flex; gap: 0.6em; align-items: center; padding: 0.45em 0.7em; border: 2px solid var(--code-edge); border-radius: 6px; background: var(--paper)';
const grow = 'flex: 1; font-weight: 700';
const quiet = 'font-size: var(--step--2); opacity: 0.7';
const button = 'padding: 0.15em 0.7em; border: 1px solid var(--code-edge); border-radius: 999px; background: var(--raised); color: var(--ink); font: inherit; font-size: var(--step--2); cursor: pointer';
export default function Files() @{
<gridlist.root selectable multiple value={['moodboard']} style={list}>
<gridlist.label style={quiet}>Shared with the studio</gridlist.label>
<gridlist.item value="moodboard">
<gridlist.itemcontent style={cell}>
<gridlist.itemlabel style={grow}>moodboard.png</gridlist.itemlabel>
<span style={quiet}>2.4 MB</span>
<gridlist.itemtrigger style={button}>Rename</gridlist.itemtrigger>
</gridlist.itemcontent>
</gridlist.item>
<gridlist.item value="invoice">
<gridlist.itemcontent style={cell}>
<gridlist.itemlabel style={grow}>invoice-april.pdf</gridlist.itemlabel>
<span style={quiet}>88 KB</span>
<gridlist.itemtrigger style={button}>Rename</gridlist.itemtrigger>
</gridlist.itemcontent>
</gridlist.item>
<gridlist.item value="sardines">
<gridlist.itemcontent style={cell}>
<gridlist.itemlabel style={grow}>sardines-on-toast.jpg</gridlist.itemlabel>
<span style={quiet}>1.1 MB</span>
<gridlist.itemtrigger style={button}>Rename</gridlist.itemtrigger>
</gridlist.itemcontent>
</gridlist.item>
</gridlist.root>
}
A row is named by its value, and the root's value is the list of rows that are picked. Focus
rests on a row, never on anything inside it, so the list is one tab stop. That is why this family
exists instead of a plain list: a row may hold buttons, Enter moves focus onto them, Escape
brings it back out.
Anatomy
gridlist.rootholds the configuration and the rows that are picked.gridlist.labelnames the list, and the grid pointsaria-labelledbyhere.gridlist.itemis one row, named by its ownvalue, and where focus rests.gridlist.itemcontentis the cell a row's content sits in.gridlist.itemlabelis the row's own words, and what typeahead matches against.gridlist.itemtriggeris a button the row holds, out of the tab order.gridlist.itemindicatoris the mark showing that this row is picked.
It writes ui-selectable, ui-multiple, ui-disabled, ui-inside, ui-value and ui-selected
as its state changes.
Pick more than one
Write multiple and several rows are picked at once. It turns selection on by itself, so
selectable is only for a list that picks one at a time.
<gridlist.root multiple value={['moodboard', 'invoice']} onChange={keep}>
A gallery that wraps
Rows are walked by where their boxes sit, not by the order they were written, so wrapped cards
move the way they look. Add wrap and a walk off the last row comes round to the first.
<gridlist.root wrap style="display: flex; flex-wrap: wrap; gap: 0.5em">
Buttons a keyboard can reach
Enter on a row moves focus onto the first gridlist.itemtrigger it holds. The walk knows only
that part: a bare <button> in a row answers Tab, but Enter will not find it.
<gridlist.itemcontent>
<gridlist.itemlabel>moodboard.png</gridlist.itemlabel>
<gridlist.itemtrigger>Rename</gridlist.itemtrigger>
</gridlist.itemcontent>
Keyboard
Focus rests on a row, and the walk steps over disabled rows.
| Key | What it does |
|---|---|
| Down arrow or Up arrow | Focus the row whose box sits below or above this one. |
| Left arrow or Right arrow | Focus the row to the left or the right. Once focus is inside a row, they step between the controls it holds. |
| Home or End | Focus the first or the last row. |
| Enter or F2 | Move focus onto the first control this row holds. |
| Escape | Inside a row, focus goes back to the row. On a row, everything picked is let go. |
| Space | Pick the focused row, or let it go again. |
| Shift and an arrow | Replace what is picked with the run from the anchor row to the one arrived at. Needs multiple. |
| Ctrl or Cmd and A | Pick every row. Needs multiple. |
| Any letter | Focus the next row whose label starts with what you typed. |
API
Every prop is optional unless its row says so, and the types are the family's own.
gridlist.root
| Prop | Type | What it does |
|---|---|---|
value |
readonly string[] |
The rows that are picked, named by the value each row was written with. Omit it and nothing starts picked. |
selectable |
boolean |
Rows can be picked, one at a time. Picking a row that is already picked lets it go. |
multiple |
boolean |
Several rows can be picked at once. Writing it also makes the list selectable. |
disabled |
boolean |
No row can be reached or picked, and the list drops out of the tab order. |
wrap |
boolean |
A walk off the last row comes back round to the first. Omit it and the walk stops at the ends. |
onChange |
(value: readonly string[]) => void |
Called with the rows that are picked whenever that changes. |
| Attribute | Present when | What to key styling off |
|---|---|---|
role |
Always | grid. |
tabindex |
Always | 0, or -1 while the list is disabled. The list is its own single tab stop until focus lands on a row. |
aria-labelledby |
Always | The id of the gridlist.label naming this list. |
aria-multiselectable |
multiple is set |
true, for a screen reader rather than your stylesheet. |
aria-disabled |
Always | true or false. |
ui-selectable |
Rows can be picked at all | The list is doing selection. |
ui-multiple |
multiple is set |
The many-picked look. |
ui-disabled |
disabled is set |
Nothing in here responds. |
ui-inside |
Focus is on a control a row holds | The arrows belong to that control for as long as this lasts. |
| Selector | Default | Why |
|---|---|---|
[ui-selectable] |
user-select: none |
A Shift walk and a Shift click both paint the page's own text selection across the rows they cross, over the top of the selection the list is reporting. |
Everything a <div> accepts reaches the element too, apart from onChange, which is the family's.
gridlist.label
No props of its own. The grid's aria-labelledby points here, so this names the list. It renders a
<span>, and everything a <span> accepts reaches the element.
gridlist.item
| Prop | Type | What it does |
|---|---|---|
value |
string |
Required. This row's identity in the picked set. Two rows must not share one. |
disabled |
boolean |
This row cannot be reached by the walk or picked. |
| Attribute | Present when | What to key styling off |
|---|---|---|
role |
Always | row. |
tabindex |
Always | -1 as rendered. Focus arriving on a row moves the 0 here, so the list remembers where it was. |
aria-selected |
The list can be picked from | true or false. Absent entirely while nothing can be picked. |
aria-disabled |
Always | true or false. |
ui-value |
Always | Holds this row's name, so one selector can reach one row. |
ui-selected |
This row is picked | The picked look. |
ui-disabled |
This row or the list is disabled | The unreachable look, and what the walk steps over. |
Everything a <div> accepts reaches the element too.
gridlist.itemcontent
No props of its own. A row means nothing to a reader without a cell, and a list of rich items has
one column, so a row holds exactly one. It renders a <div role="gridcell">, and everything a
<div> accepts reaches the element.
gridlist.itemlabel
No props of its own. Typeahead matches this part when a row has one, and the whole row when it does
not. It renders a <span>, and everything a <span> accepts reaches the element.
gridlist.itemtrigger
No props of its own. It renders a <button>, and everything a <button> accepts reaches the
element.
| Attribute | Present when | What to key styling off |
|---|---|---|
type |
Always | button, so a control inside a form does not submit it. |
tabindex |
Always | -1: the row is the tab stop, and Enter on the row is what reaches this. |
disabled |
This row or the list is disabled | The native disabled state. |
gridlist.itemindicator
No props of its own. It renders a <span>, and everything a <span> accepts reaches the element.
| Attribute | Present when | What to key styling off |
|---|---|---|
aria-hidden |
Always | true. The row already carries aria-selected, and a second announcement of the same fact is noise. |
ui-selected |
Its row is picked | Show the mark. |
