# Keyboard & hotkeys

> The grid's full keyboard model, and hotkeys of your own that act on the focused row, such as B to buy and Delete to cancel.

The grid is one tab stop: <kbd>Tab</kbd> moves focus into it, and keys move within it, as in a spreadsheet. On top of the grid's own keys, `hotkeys` binds keys for your app (B to buy, S to sell, Delete to cancel) and hands each binding the row the user is on.

Click a row, then press <kbd>B</kbd>, <kbd>S</kbd> or <kbd>Delete</kbd>; <kbd>Shift</kbd>+<kbd>R</kbd> brings the removed rows back.

Example: Hotkeys on the focused row (keyboard.svelte)

```svelte
<script lang="ts">
  import { Grid, type ColumnDef, type Hotkeys } from 'bo-grid';
  import { quotes } from './data';

  const columns: ColumnDef[] = [
    { type: 'text', key: 'symbol', header: 'Symbol', sub: 'name', width: 170 },
    { type: 'price', key: 'price', header: 'Price' },
    { type: 'percent', key: 'change', header: 'Chg %' },
    { type: 'volume', key: 'volume', header: 'Volume', flex: 1 },
  ];
  let rows = $state(quotes());
  let log = $state('Click a row, then press B, S, Delete or Shift+R.');

  // App keys, run before the grid's own, with the focused row.
  const hotkeys: Hotkeys = {
    b: ({ row }) => {
      if (row) log = `Buy ticket for ${row.symbol}`;
    },
    s: ({ row }) => {
      if (row) log = `Sell ticket for ${row.symbol}`;
    },
    Delete: ({ row }) => {
      if (!row) return;
      rows = rows.filter((r) => r.id !== row.id);
      log = `Removed ${row.symbol}`;
    },
    'Shift+R': () => {
      rows = quotes();
      log = 'All rows back';
    },
  };
</script>

<p style="margin: 0 0 10px; opacity: 0.8" aria-live="polite">{log}</p>
<Grid {rows} {columns} height={280} {hotkeys} ariaLabel="Watchlist" />
```

data.ts:

```ts
// Small, fixed data for the docs' examples: every example reads the same
// symbols, so a reader compares features, not datasets.
// A type, not an interface: it then fits GridRow's index signature.
export type Quote = {
  id: number;
  symbol: string;
  name: string;
  sector: string;
  price: number;
  change: number;
  volume: number;
};

const BASE: Omit<Quote, 'id'>[] = [
  { symbol: 'VNM', name: 'Vinamilk', sector: 'Consumer', price: 68.5, change: 1.2, volume: 1_204_000 },
  { symbol: 'FPT', name: 'FPT Corp', sector: 'Technology', price: 132, change: -0.4, volume: 860_500 },
  { symbol: 'HPG', name: 'Hoa Phat', sector: 'Materials', price: 27.35, change: 0.74, volume: 3_412_900 },
  { symbol: 'VCB', name: 'Vietcombank', sector: 'Financials', price: 91.5, change: 0.55, volume: 1_986_200 },
  { symbol: 'MWG', name: 'Mobile World', sector: 'Consumer', price: 51.4, change: -1.1, volume: 2_250_400 },
  { symbol: 'SSI', name: 'SSI Securities', sector: 'Financials', price: 33.3, change: -0.45, volume: 4_104_700 },
  { symbol: 'VIC', name: 'Vingroup', sector: 'Real estate', price: 44.15, change: -0.11, volume: 1_530_100 },
  { symbol: 'GAS', name: 'PV Gas', sector: 'Energy', price: 71.8, change: 0.3, volume: 645_800 },
  { symbol: 'MSN', name: 'Masan', sector: 'Consumer', price: 76.2, change: 2.05, volume: 1_120_300 },
  { symbol: 'TCB', name: 'Techcombank', sector: 'Financials', price: 23.9, change: 0.84, volume: 5_870_000 },
  { symbol: 'VHM', name: 'Vinhomes', sector: 'Real estate', price: 41.6, change: -0.72, volume: 2_904_600 },
  { symbol: 'POW', name: 'PV Power', sector: 'Energy', price: 12.25, change: 1.66, volume: 6_315_200 },
];

/** A fresh copy of the quotes, so one example's edits never reach another. */
export function quotes(): Quote[] {
  return BASE.map((q, i) => ({ id: i + 1, ...q }));
}
```

## The grid's keys

| Keys | Action |
| --- | --- |
| <kbd>↑</kbd> <kbd>↓</kbd> <kbd>←</kbd> <kbd>→</kbd> | Move the focused cell |
| <kbd>Shift</kbd> + arrows | Extend the selection |
| <kbd>Home</kbd> / <kbd>End</kbd> | First / last column; with <kbd>Ctrl/⌘</kbd>, the first / last cell |
| <kbd>PageUp</kbd> / <kbd>PageDown</kbd> | Move by a page of rows |
| <kbd>Enter</kbd> | Edit the focused cell, or run `onRowClick` on a read-only one |
| <kbd>F2</kbd> | Edit the focused cell |
| A printable key | Start editing the focused cell with that character |
| <kbd>Space</kbd> | Tick the focused row (with `rowSelection`) |
| <kbd>Ctrl/⌘</kbd>+<kbd>A</kbd> / <kbd>C</kbd> / <kbd>V</kbd> | Select all / copy / paste |
| <kbd>Ctrl/⌘</kbd>+<kbd>Z</kbd> / <kbd>Y</kbd> | Undo / redo an edit, paste or fill |
| <kbd>Ctrl/⌘</kbd>+<kbd>F</kbd> | Open the find bar (with `findBar`) |
| <kbd>Alt</kbd>+<kbd>↓</kbd> | Open the column menu (with `columnMenu`) |
| <kbd>ContextMenu</kbd> / <kbd>Shift</kbd>+<kbd>F10</kbd> | Open the row menu (with `rowMenu` or `rowPinning`) |
| <kbd>→</kbd> / <kbd>←</kbd> on a tree row | Expand / collapse it |
| <kbd>Enter</kbd> / <kbd>Space</kbd> / <kbd>→</kbd> / <kbd>←</kbd> on a group header | Toggle / expand / collapse the group |
| <kbd>Escape</kbd> | Clear the selection, or close a menu |

While a cell is being edited, keys belong to the editor: <kbd>Enter</kbd> saves, <kbd>Tab</kbd> saves and moves to the next cell (<kbd>Shift</kbd>+<kbd>Tab</kbd>: the previous one), and <kbd>Escape</kbd> cancels. Each of them gives the keyboard back to the grid. Headers are focusable too: <kbd>Enter</kbd> or <kbd>Space</kbd> sorts, with <kbd>Shift</kbd> to add a sort key. Menus follow the usual menu pattern (arrows move, <kbd>Enter</kbd> chooses, <kbd>Escape</kbd> closes and returns focus). Screen-reader semantics are in [Accessibility](https://bonguynvan.github.io/bo-grid/docs/accessibility/).

## Hotkeys

`hotkeys` maps a binding to a handler. Bindings run before the grid's own keys, and each handler gets the focused `row` and `column` (`null` when there is none) and the `event`:

```svelte
<script lang="ts">
  import { Grid, type Hotkeys } from 'bo-grid';

  const hotkeys: Hotkeys = {
    b: ({ row }) => { if (row) openTicket(row, 'buy'); },
    s: ({ row }) => { if (row) openTicket(row, 'sell'); },
    Delete: ({ row }) => { if (row) cancel(row); },
    'Shift+C': () => cancelAll(),
    'Mod+K': () => openSearch(), // Ctrl+K, or ⌘K on a Mac
  };
</script>

<Grid {rows} {columns} height={480} {hotkeys} />
```

### Writing a binding

- A binding is a key (`'b'`, `'Delete'`, `'F2'`, `'Space'`, `'Enter'`) after any of `Shift+`, `Alt+`, `Ctrl+`, `Meta+` and `Mod+` (Ctrl or ⌘, whichever is pressed). Case does not matter.
- A symbol is bound by the character it types: `'?'`, not `'Shift+/'` (which throws).
- A letter also fires from its place on the keyboard when the layout types another alphabet there, so B still works on a Cyrillic or Greek layout.
- A binding that cannot be read, or a key bound twice, throws when the grid gets the map.

### When a binding runs

- Return `false` from a handler to leave the key to the grid, for example to keep the grid's own <kbd>Enter</kbd> on some rows: `Enter: ({ row }) => (row?.locked ? undefined : false)`.
- Keys go to the grid, not to a binding, while a cell is being edited, or while an input, button or menu inside the grid has them.
- A held-down key runs its binding once, so holding <kbd>B</kbd> opens one ticket.

## The focus stays on its row

The selection and an open editor belong to rows, not to positions. When rows are added, removed or reordered (a new `rows` array, a row arriving above the focused one), the focus, the selected range and the editor move with their rows. A hotkey acts on the row the user is looking at, and an edit is written to the row it was opened on. If the focused row goes away, the selection clears; if the edited row goes away, the edit closes unsaved. (With a server `source`, rows are where the source puts them.)

`api.getFocusedCell()` returns the same `{ row, column }` from code, and `api.focusCell(rowId, columnKey)` moves the focus.

## Reference

- `hotkeys`: `Hotkeys` (default `undefined`) — Keys for the app, run before the grid's own. A binding is a key (`'b'`, `'Delete'`, `'Space'`; a symbol by the character it types: `'?'`, not `'Shift+/'`), after any of `Shift+`, `Alt+`, `Ctrl+`, `Meta+` and `Mod+` (Ctrl or ⌘, either). Case does not matter. The handler is given the focused row and column (null when there is none) and the event; return false to leave the key to the grid. Keys go to the grid, not to a binding, while a cell is edited or an input, button or menu inside the grid has them, and a key a binding took runs it once when held down. A binding that cannot be read, or binds a key twice, throws (BG1001–BG1004).

  ```svelte
  <Grid {rows} {columns} height={480}
    hotkeys={{ b: ({ row }) => { if (row) buy(row); }, 'Shift+C': cancelAll, Delete: ({ row }) => { if (row) cancel(row); } }} />
  ```

- `onRowClick`: `(row: GridRow, event: MouseEvent | KeyboardEvent) => void` — Called when a data row is activated by click or Enter (open a detail view, navigate, …). Edit-input and checkbox clicks are excluded.

  ```svelte
  <Grid {rows} {columns} height={480} {selectedRowId} onRowClick={(row) => (selectedRowId = row.id)} />
  ```

- `rowMenu`: `(row: GridRow) => Array<{ label: string; onSelect: () => void }>` — Right-click row menu. Return the items for a row; an empty array shows no menu. Each item runs `onSelect` and closes the menu.

  ```svelte
  <Grid {rows} {columns} height={480}
    rowMenu={(row) => [{ label: 'Cancel order', onSelect: () => cancel(row) }, { label: 'Copy id', onSelect: () => copy(String(row.id)) }]} />
  ```

- `ariaLabel`: `string` (default `undefined`) — Accessible name for the grid (`aria-label` on the `role="grid"` root).

- `getFocusedCell(): { row: GridRow; column: ColumnDef } | null` — The focused cell's row and column, or null when no data cell has focus (nothing focused yet, or the focus is on a group row). For hotkeys that act on the row the user is on.

  ```ts
  const cell = api.getFocusedCell();
  if (cell) openTicket(cell.row);
  ```

- `focusCell(rowKey: string | number, columnKey: string): boolean` — Focus (and select) one cell by row id and column key. Returns false when the row or column is not currently visible.

  ```ts
  if (!api.focusCell(row.id, 'qty')) api.scrollToRow(row.id);
  ```

---
Page: https://bonguynvan.github.io/bo-grid/docs/keyboard/ · All pages: https://bonguynvan.github.io/bo-grid/llms.txt
