gridFocus
elements man ui/behaviors/grid-focusOne Tab stop for a two-dimensional grid of cells, arrows between them by row and column: the grid pattern for a calendar or a data table. The map is built from the DOM and kept until the grid changes, so a key in a thousand-row table costs one cell's work, not the table's.
Signaturefunction gridFocus(grid: HTMLElement, opts?: GridFocusOptions): GridFocus; interface GridPosition { row: number; col: number; }
Options
| Option | Type | Default | What it does |
|---|---|---|---|
rows |
string |
"[role=row], tr" |
Selector for rows. |
cells |
string |
"[role=gridcell], [role=columnheader], [role=rowheader], td, th" |
Selector for cells inside a row. |
wrap |
boolean |
false |
A row's end wraps to the next row's start, as in a layout grid. |
wrapColumns |
boolean |
false |
Down from the last row goes to the top of the next column, Up from the first row to the bottom of the previous one. |
pageRows |
number |
0 (off) |
PageUp and PageDown move this many rows when there is no onPage. |
focusDisabled |
boolean |
false |
Disabled cells stay focusable. |
editKeys |
boolean |
true |
Enter and F2 move into a cell's widgets, Escape and F2 come back. |
dir |
"ltr" | "rtl" |
the grid's computed direction | Reading direction. Left and Right flip under rtl. |
current |
() => HTMLElement | null |
the first cell | The cell that holds the tab stop when focus is outside: the selected day, today. |
onPage |
(dir: 1 | -1, e: KeyboardEvent, cell: HTMLElement) => Next |
none | PageUp (-1) or PageDown (1): a calendar changes month. |
onEdge |
(dx: number, dy: number, e: KeyboardEvent, cell: HTMLElement) => Next |
none | An arrow pressed with no cell that way; dx and dy are -1, 0 or 1 in visual direction. A calendar shows the next month. |
onFocusCell |
(cell: HTMLElement, pos: GridPosition) => void |
none | A cell received focus by keyboard. |
onPage and onEdge may return the element to focus, or a function that
returns one; the function runs on the next frame, after a reactive
re-render has drawn the new month.
Keys
| Keys | Action |
|---|---|
| Arrows | One cell. At an edge, onEdge; otherwise focus stays. |
Home, End |
First and last cell of the row; of the grid in a one-column grid. Never call onEdge. |
Ctrl+Home, Ctrl+End |
First cell of the first row, last cell of the last row. Cmd works too. |
PageUp, PageDown |
pageRows rows, or onPage. |
Enter, F2 |
Enter a cell that holds widgets: focus moves to its first widget and arrows belong to the widget. |
Escape, F2 |
Back to the cell from an entered one. |
| Arrows in an entered cell | Right and Down to the next widget, Left and Up to the previous, unless the focused widget uses arrows itself. |
Tab in an entered cell |
The next widget in the grid, entering the cell that holds it; out of the grid after the last one. |
| A letter or digit | On a cell whose first widget is a text field: enters it and types there. |
Cells spanning several slots (colspan, rowspan, aria-colspan,
aria-rowspan) are one stop, and the column you came from is remembered,
so moving down past a wide cell comes back to it. Empty slots and disabled
cells are skipped in the direction of travel unless focusDisabled. Rows
and cells placed in the grid with aria-owns are part of the map. A grid
started while hidden takes its stop when it is shown. When the focused cell
or its row is removed, focus moves to the nearest usable cell in the same
place. Shift+Space, Ctrl+Space and Ctrl+A are left to the template;
Shift+Arrow still moves focus, so a template extends a range from its own
keydown or from onFocusCell.
What takes focus in a cell: the cell itself, or its single widget when it holds exactly one that needs no arrow keys (a link, a button, a checkbox, a radio, a switch), so Space acts on it at once. A widget that uses arrows itself (a text field, a select, a slider, a spinbutton, a radio group, a combobox) is reached with Enter or F2, even alone. Clicking into a widget of a multi-widget cell enters that cell, so Escape and F2 return to it.
The handle
| Member | What it does |
|---|---|
matrix() |
The cell slots: matrix()[row][col] is the cell covering it, or null. |
current() |
The cell that holds the tab stop. |
focus(cell) |
Moves the tab stop to cell (or the cell containing it) and focuses it. |
position(cell) |
The row and column of a cell's first slot, or null. |
sync() |
Re-applies tabindex after the cells changed. Runs on its own on DOM changes. |
release() |
Stops and puts back the tabindex of every cell and widget it changed. |
In a template of your own
app/shared/templates/month-grid/index.ehtmlimport { gridFocus, type GridFocus } from "@elements/ui/behaviors"; interface MonthGridState { g?: GridFocus; } <MonthGrid (weeks: Date[][], selected: Date, onPick: (day: Date) => void, private ui: MonthGridState = {})> <table role="grid" oninsert={(e) => ui.g = gridFocus(e.target as HTMLElement, { current: () => (e.target as HTMLElement).querySelector("[aria-selected=true]"), })} onremove={() => ui.g?.release()}> <tbody> <tr e:for={week of weeks}> <td e:for={day of week} role="gridcell" aria-selected={day.getTime() === selected.getTime() ? "true" : "false"} onclick={() => onPick(day)}>{day.getDate()}</td> </tr> </tbody> </table> </MonthGrid>
Tab lands on the selected day, arrows move through the month, and the template keeps its own click handler for picking.
Writes
tabindex on cells and on the widgets inside them, put back on release;
data-editing on an entered cell.