# Keyboard Interaction Contract Mechanica's 3D viewer must be operable without a pointer, but it must not steal native keyboard behavior from forms, buttons, links, menus, or assistive-technology-driven controls. This contract defines the shortcut scope model, default bindings, and implementation rules used by the new `keyboardShortcuts` utility and `useKeyboardShortcuts` hook. ## Scope model Keyboard shortcuts are layered by scope: 1. **Global** — available across the app, such as help and dismiss. 2. **Route scope** — active only on a major page (`catalogue`, `viewer`). 3. **Panel/modal scope** — active only while a drawer, modal, or focused composite widget owns interaction. Conflict detection treats `global` bindings as overlapping every scope. Route-local bindings may reuse the same key if they cannot be active at the same time; for example, a catalogue-only shortcut and viewer-only shortcut can both use `E`, but a global `E` would conflict with both. ## Safety rules - Never hijack `Tab`; browser focus order remains the primary keyboard navigation mechanism. - Do not intercept `Space` or `Enter` while focus is on a native/semantic button, link, menu item, tab, slider, tree item, grid, or similar interactive control. - Do not intercept printable shortcuts while focus is in text inputs, textareas, selects, contenteditable regions, or ARIA text entry roles. - Do not match shortcuts during `AltGraph` composition, because many international keyboard layouts emit `Ctrl+Alt` while typing characters. - Repeating keydown events are ignored by default. Continuous controls such as stepping animation or adjusting exploded separation must opt in with `allowRepeat: true`. - Escape is the exception for modal/drawer dismissal and may opt into `allowInEditable` and `allowInInteractive` when the active overlay owns focus. For edge cases, focused elements or ancestors may set: ```html
...
... ``` The nearest policy wins. Use `allow` sparingly; it is intended for the focused viewer canvas or a composite widget that has implemented its own keyboard semantics. ## Default binding catalogue | Scope | Shortcut | Action | | --- | --- | --- | | Global | `?` (`Shift + /`) | Open the keyboard shortcut reference | | Global | `Escape` | Dismiss the active popover, drawer, or modal | | Catalogue | `/` | Move focus to catalogue search | | Catalogue | `Shift + F` | Toggle advanced filters | | Viewer | `Space` | Play/pause the working animation | | Viewer | `ArrowLeft` / `ArrowRight` | Step the animation backward/forward | | Viewer | `-` / `=` | Decrease/increase animation speed | | Viewer | `E` | Toggle exploded view | | Viewer | `[` / `]` | Decrease/increase exploded separation | | Viewer | `S` / `W` / `X` | Switch solid, wireframe, or cross-section mode | | Viewer | `R` | Reset camera | | Viewer | `F` | Frame selected component or full machine | | Viewer | `1` / `2` / `3` / `4` | Isometric, front, side, and top camera presets | | Viewer | `C` | Toggle component visibility panel | ## Implementation pattern Use the pure utility for validation, display, and event matching: ```ts import { DEFAULT_VIEWER_SHORTCUTS, findShortcutConflicts, matchesShortcut, shortcutToLabel, } from '../src/utils/keyboardShortcuts'; const conflicts = findShortcutConflicts(DEFAULT_VIEWER_SHORTCUTS); const label = shortcutToLabel('Shift+/'); ``` Use `useKeyboardShortcuts` when a React surface owns a set of actions: ```ts useKeyboardShortcuts( DEFAULT_VIEWER_SHORTCUTS.map((definition) => ({ ...definition, handler: () => dispatchViewerAction(definition.id), })), { target: () => canvasRef.current, onConflict: reportShortcutConflicts, }, ); ``` The hook registers a single `keydown` listener on the supplied target, skips disabled definitions, reports conflicts, respects editable/interactive target rules, and stops after the first matching enabled shortcut. ## Quality gates - New shortcut definitions must pass `findShortcutConflicts`. - Help UI must render labels through `shortcutToLabel` so Mac, Windows, and Linux users see platform-appropriate modifiers. - Viewer shortcuts must be tested with focus on both the canvas and adjacent controls to verify that shortcuts do not break native button/link/form interaction. - Shortcuts that mutate view state should update the URL state model when the equivalent pointer-driven control would do so.