# gridFocus One 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. ```ts Signature function 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 ```ehtml app/shared/templates/month-grid/index.ehtml import { gridFocus, type GridFocus } from "@elements/ui/behaviors"; interface MonthGridState { g?: GridFocus; } void, private ui: MonthGridState = {})> ui.g = gridFocus(e.target as HTMLElement, { current: () => (e.target as HTMLElement).querySelector("[aria-selected=true]"), })} onremove={() => ui.g?.release()}>
onPick(day)}>{day.getDate()}
``` 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. ## Used by [DataTable](../templates/data-table).