# Viewer keyboard accessibility layer This milestone now includes a reusable keyboard command layer for the 3D viewer. It is intentionally split into pure shortcut matching utilities, a React hook, and an accessible help dialog so the viewer can bind commands to Zustand actions without coupling low-level keyboard parsing to store shape. ## Files - `src/utils/viewerKeyboardShortcuts.ts` defines the default shortcut catalogue, command IDs, formatter helpers, grouping helpers, and safe event-target filtering. - `src/hooks/useViewerKeyboardShortcuts.ts` registers scoped `keydown` listeners and returns the active shortcut catalogue. - `src/components/viewer/ViewerKeyboardController.tsx` combines the hook, a screen-reader live region, a DOM command event, and the help dialog. - `src/components/viewer/ViewerKeyboardHelpDialog.tsx` renders a focus-managed modal shortcut reference. - `src/utils/viewerKeyboardShortcuts.test.ts` covers matching behavior, modifier safety, repeat handling, target filtering, formatting, and grouping order. ## Default commands | Command | Shortcut | Intent | | --- | --- | --- | | Reset camera | `R` | Return the camera to the active/default preset | | Fit model to view | `F` | Frame the whole machine | | Next camera preset | `C` | Cycle forward through saved viewpoints | | Previous camera preset | `Shift + C` | Cycle backward through saved viewpoints | | Solid view | `1` | Switch to solid PBR rendering | | Wireframe view | `2`, `W` | Switch to wireframe inspection | | Cross-section view | `3`, `X` | Enable the active clipping plane | | Toggle exploded view | `E` | Separate or collapse assemblies | | Decrease / increase explosion | `[`, `]` | Adjust exploded separation; key repeat is allowed | | Toggle annotations | `L` | Show or hide part labels | | Toggle parts panel | `P` | Open or close component controls | | Toggle detail drawer | `D` | Open or close selected-part engineering notes | | Clear selection | `Esc` | Deselect active part / dismiss transient overlays | | Focus part search | `/` | Move focus to the part filter field | | Play / pause animation | `Space`, `K` | Toggle the mechanical cycle | | Show shortcuts | `Shift + ?`, `F1` | Open the shortcut reference | ## Recommended MechanicalViewer integration Mount the controller inside the viewer shell and scope it to a focusable region. The root should describe the interactive canvas without forcing all page navigation into an application role. ```tsx import { useRef } from 'react'; import { ViewerKeyboardController } from '../components/viewer/keyboard'; import { useViewerStore } from '../store/viewerStore'; export function MechanicalViewerShell() { const viewerRootRef = useRef(null); const actions = useViewerStore((state) => state.actions); return (
{ switch (command) { case 'resetCamera': actions.resetCamera(); break; case 'fitCamera': actions.fitCameraToMachine(); break; case 'wireframeMode': actions.setViewMode('wireframe'); break; case 'crossSectionMode': actions.setViewMode('cross-section'); break; case 'solidMode': actions.setViewMode('solid'); break; // Map the remaining commands to the existing viewer actions. } }} /> {/* Existing toolbar, canvas, drawers, labels, and status bar. */}
); } ``` The controller also emits a bubbling `mechanica:viewer-command` `CustomEvent`, which is useful for analytics, command logging, or incremental adoption while store actions are being wired. ## Target safety rules Keyboard shortcuts must not hijack text entry, native controls, or toolbar buttons. By default the matcher ignores: - `input`, `textarea`, and `select` - content-editable regions - editable ARIA roles such as `textbox`, `searchbox`, `combobox`, and `spinbutton` - native controls and interactive ARIA roles such as `button`, `link`, `slider`, `tab`, and `menuitem` - any element inside `data-viewer-shortcuts="ignore"` Use `data-viewer-shortcuts="allow"` only on a deliberate shortcut focus target, such as a canvas wrapper. Avoid placing it on a broad parent that contains form fields. ## Accessibility checklist - The viewer region should be reachable by keyboard with `tabIndex={0}`. - The region needs an accessible name such as `aria-label="Interactive 3D mechanical system viewer"`. - Visible controls must keep native button/input semantics; do not rely on shortcuts as the only way to perform an action. - The shortcut dialog traps focus while open, closes on `Esc`, restores focus, and explains that form fields keep their native keyboard behavior. - Commands announce through a polite live region so screen-reader users receive confirmation when a shortcut changes viewer state.