# 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<HTMLDivElement | null>(null);
  const actions = useViewerStore((state) => state.actions);

  return (
    <section
      ref={viewerRootRef}
      aria-label="Interactive 3D mechanical system viewer"
      aria-roledescription="3D viewer"
      className="relative min-h-0 flex-1"
      tabIndex={0}
    >
      <ViewerKeyboardController
        scopeRef={viewerRootRef}
        requireFocusWithin
        onCommand={(command) => {
          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. */}
    </section>
  );
}
```

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.
