> ## Documentation Index
> Fetch the complete documentation index at: https://developer.kodexa.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Keyboard Shortcuts

> Bind keys to named scripts in a V2 data form. Shortcuts register on mount, clear on unmount, and surface in the global help dialog.

V2 data forms can declare keyboard shortcuts as a top-level array on the schema. Each entry binds a key combination to a named script and is registered under a per-form scope, so a form effectively **resets all of its shortcuts every time it mounts** -- you never have to manually un-register them.

## Why declarative

Shortcuts live in the schema next to `scripts` and `bridge`. The form author writes a key → script mapping; the platform handles registration, scoping, and cleanup. Because each shortcut points at a named script, the same script can be invoked via a hotkey, a button click, a trigger, or a service-bridge response without duplicating code.

## Schema

Add a `shortcuts` array to the form's top-level definition:

```yaml theme={null}
version: "2"
bridge:
  permissions: ["data:read", "navigation"]
shortcuts:
  - key: "control+1"
    description: "Jump to the invoice page"
    group: "navigation"
    scriptRef: gotoInvoice
  - key: "control+f"
    altKey: "meta+f"
    description: "Focus the total field"
    scriptRef: focusTotal
scripts:
  gotoInvoice: |
    (ctx, bridge) => bridge.navigation.setPage(1)
  focusTotal: |
    (ctx, bridge) => bridge.navigation.focusAttribute(
      ctx.dataObjects[0]?.uuid,
      "invoice/total"
    )
nodes:
  - component: v2:panel
    # ...
```

### FormShortcut Fields

| Field         | Type                 | Required | Description                                                                          |
| ------------- | -------------------- | -------- | ------------------------------------------------------------------------------------ |
| `key`         | `string`             | Yes      | Primary binding (e.g. `"control+1"`, `"meta+shift+s"`)                               |
| `altKey`      | `string \| string[]` | No       | One or more alternative bindings that also fire the script                           |
| `description` | `string`             | Yes      | Label shown in the keyboard-shortcuts help dialog (`⌘/`)                             |
| `group`       | `string`             | No       | Help-dialog group: `"navigation"`, `"zoom"`, `"data-entry"`, `"document"`, or `"ui"` |
| `scriptRef`   | `string`             | Yes      | Name of a script in the form's `scripts` map to invoke                               |

## Lifecycle and scoping

Every shortcut is registered under the scope `form:<viewId>`. The platform:

1. **On mount** -- clears the scope (so a stale registration from a previous mount is gone), then registers each entry under the scope.
2. **On `shortcuts` change** -- runs the same clear-then-register cycle automatically. Hot-reloading a form during development replaces the bindings cleanly.
3. **On unmount** -- clears the scope. Shortcuts from other forms or the global help dialog are untouched.

Because scopes are isolated, two open forms can register the same key in different ways -- the binding effective at any moment is the one belonging to the most recently mounted form for that key.

<Note>
  If your form's `key` collides with a global shortcut already in the help dialog (for example `meta+/`), the existing global binding wins and the form's is silently dropped. Prefer keys not used globally.
</Note>

## Cross-platform notation

You author bindings with the same `key` string on every platform (for example `control+1`), and the platform normalizes them to the keystroke each browser actually delivers, so most bindings work for Windows, Linux, and Mac reviewers without per-platform entries.

On Mac this means:

* **`control+X` is rewritten to `⌘⌥X` (meta+alt) at runtime.** A binding declared as `control+1` therefore fires when a Mac reviewer presses `⌘⌥1`, not the physical Control key. This is expected -- if you are debugging why a `control+1` shortcut appears to respond to `⌘⌥1` on Mac, that is the normalization at work.
* **`alt+<letter>` and `alt+<digit>` chords auto-derive the macOS-substituted character.** macOS turns `⌥`+key into a special character (for example `⌥R`→®, `⌥6`→§, `⌥⇧R`→‰), and the shortcut auto-registers that form so a plain `⌥`+key still triggers. Note: the option **dead-key** letters (`e`, `i`, `n`, `u`) and `⌥⇧K` produce no standalone character and are **not** auto-derived -- declare an explicit `altKey` for those.
* **Registered chords are reclaimed from the browser.** Any registered chord is intercepted in the capture phase (`preventDefault`) so the browser's built-in behaviour -- Chrome tab switching on `Ctrl+PageDown` / `Ctrl+5`–`8` (matched as `⌘⌥5`–`8` on Mac), for example -- doesn't consume it first. This only applies when focus is outside an input, textarea, select, or contentEditable element, so typing in a field is never affected.

The `altKey` field still lets you add explicit alternative bindings when you want one; the Mac rewrites above happen automatically and do not require an `altKey` entry.

## Inside the script

Scripts referenced by `scriptRef` follow the standard V2 signature -- a function of `(ctx, bridge)`:

```javascript theme={null}
(ctx, bridge) => {
  // ctx exposes the form's data context plus a `shortcut` payload
  // describing the FormShortcut entry that fired this invocation.
  bridge.log.debug("Triggered by " + ctx.shortcut.key);

  // bridge.* gives you the same Bridge API documented under
  // "Bridge API & External Services".
  bridge.navigation.setPage(1);
}
```

The `ctx.shortcut` payload (`{ key, description, scriptRef, ... }`) is added by the shortcut dispatcher so a single script can branch on which binding fired it.

## What scripts typically do

The most common actions a shortcut performs are spatial navigation, attribute focus, and data-object manipulation -- all available through the bridge:

```javascript theme={null}
// Jump the spatial viewer to a known page
bridge.navigation.setPage(3);

// Focus a specific field and highlight its tag in the document
bridge.navigation.focusAttribute(parentUuid, "shipment/originZip");

// Read the current page so a shortcut can "go to next page in this group"
const current = bridge.navigation.getCurrentPage();
const total = bridge.navigation.getPageCount();
if (current && current < total) bridge.navigation.setPage(current + 1);

// Add a child data object under a known parent
const obj = await bridge.data.addDataObject(parentUuid, "LineItem");
bridge.data.setAttribute(obj.uuid, "LineItem/Description", "New line");
```

See [Bridge API & External Services](/guides/data-forms/bridge-api) for the full method surface.

## Example: rotate the current page

A common review task is fixing a sideways scan. `bridge.navigation.rotatePage(direction)` rotates the spatial viewer's **current page** by 90° in the given direction, and a pair of shortcuts makes it a one-keystroke flip:

```yaml theme={null}
version: "2"
bridge:
  permissions: ["navigation"]
shortcuts:
  - key: "alt+r"
    description: "Rotate page right"
    group: "document"
    scriptRef: rotateRight
  - key: "alt+shift+r"
    description: "Rotate page left"
    group: "document"
    scriptRef: rotateLeft
scripts:
  rotateRight: |
    (ctx, bridge) => bridge.navigation.rotatePage("right")
  rotateLeft: |
    (ctx, bridge) => bridge.navigation.rotatePage("left")
```

`alt+r` and `alt+shift+r` are example bindings chosen by the form author -- exactly like every other shortcut in this guide, you pick the keys -- not a fixed global hotkey. Swap them for any combination that suits your reviewers.

A few things to know about `rotatePage`:

* **Permission.** It calls the `navigation` capability, so `"navigation"` must be in `bridge.permissions`.
* **Direction is `"left"` or `"right"`.** Those are the only two accepted values. Any other string is ignored (a console warning is logged and the call is a no-op).
* **Rotation is relative.** Each call turns the page ±90° from its *current* angle rather than setting an absolute angle -- so calling `rotatePage("right")` twice lands at 180°, and a following `rotatePage("left")` returns it to 90°.
* **Current page only.** It rotates just the page the viewer is on. Rotating every page of a document remains a separate menu action.
* **Targets one document.** With no argument it rotates the page of the form's default (first) document family. Pass an optional `documentFamilyId` -- `bridge.navigation.rotatePage("right", familyId)` -- to target a specific open document; rotation applied to one document does not bleed across to others.

The method returns nothing. An invalid direction logs a console warning and is a no-op, but a missing `navigation` permission throws a `Permission denied: navigation` error like any other gated Bridge call.

## Permissions

Shortcut scripts call the same Bridge API as any other V2 script, so they obey the same `bridge.permissions` contract. A shortcut that calls `bridge.navigation.setPage(...)` needs `"navigation"` in the form's permissions array; one that calls `bridge.data.addDataObject(...)` needs `"data:write"`. Permission errors surface to the browser console and the shortcut becomes a no-op.

## Help dialog

Registered shortcuts appear in the global keyboard-shortcuts help dialog (open with `⌘/` on Mac or `Ctrl+/` on Windows/Linux), grouped by the optional `group` field. Use a clear `description` -- that's what users will see when they're hunting for a key.

## Example: invoice review form

A reviewer-oriented form that binds the number keys to the document's classification groups and uses `Tab`-like flow to move between common fields:

```yaml theme={null}
version: "2"
bridge:
  permissions: ["data:read", "data:write", "navigation"]
shortcuts:
  - key: "control+1"
    description: "Jump to Invoice"
    group: "navigation"
    scriptRef: gotoInvoice
  - key: "control+2"
    description: "Jump to Bill of Lading"
    group: "navigation"
    scriptRef: gotoBOL
  - key: "control+t"
    description: "Focus invoice total"
    group: "data-entry"
    scriptRef: focusTotal
  - key: "control+n"
    description: "Add a line item"
    group: "data-entry"
    scriptRef: addLineItem
scripts:
  gotoInvoice: |
    (ctx, bridge) => bridge.navigation.setPage(
      ctx.invoicePage ?? 1
    )
  gotoBOL: |
    (ctx, bridge) => bridge.navigation.setPage(
      ctx.bolPage ?? 2
    )
  focusTotal: |
    (ctx, bridge) => bridge.navigation.focusAttribute(
      ctx.dataObjects[0]?.uuid,
      "invoice/total"
    )
  addLineItem: |
    async (ctx, bridge) => {
      const parent = ctx.dataObjects[0];
      const obj = await bridge.data.addDataObject(parent.uuid, "LineItem");
      bridge.navigation.focusAttribute(obj.uuid, "LineItem/Description");
    }
```

When the reviewer opens this form, four shortcuts appear in the help dialog under "Navigation" and "Data Entry". Switching to a different form removes them; reopening this form restores them. No manual cleanup is required.

## Conformance

The shortcut dispatcher and Bridge API are covered by a conformance suite in `kodexa-ui/src/schema/runtime/conformance/`. Each binding behaviour ships as a paired `.js` + `.expected.json` script that runs against a stub-store-backed Bridge -- the suite catches binding regressions before they reach the browser.
