import { useEffect } from "react"; import { Keyboard, X } from "lucide-react"; import { Dialog, DialogContent } from "@/components/ui/dialog"; type KeyboardCheatsheetProps = { open: boolean; onClose: () => void; }; /** * Shortcut rows. Each row may have multiple key chips (e.g. "j" and * "↓" both navigate to the next claim) so the `keys` array renders one * `` per entry. The order here is the order rendered top→bottom * and is the project's canonical "drawer shortcuts" reference. */ const SHORTCUTS = [ { keys: ["j", "↓"], description: "Next claim" }, { keys: ["k", "↑"], description: "Previous claim" }, { keys: ["Esc"], description: "Close drawer" }, { keys: ["?"], description: "Toggle this help" }, ] as const; /** * Stable id used to wire the dialog's `aria-labelledby` and the close * button's `aria-describedby` to the visible `

` heading. Exposed * as a constant so tests can assert against the same string the * component renders. */ const CHEATSHEET_TITLE_ID = "keyboard-cheatsheet-title"; /** * Keyboard help overlay (SP4). * * Centered modal-style overlay (per spec: "centered modal-style * overlay") built on the project's `Dialog` primitive — the only * modal primitive in the codebase. It reuses Radix's focus * management, ESC-to-close, and overlay-click-to-dismiss for free; * the dialog's `onOpenChange(false)` is wired straight to the * consumer's `onClose`. * * Dismissal rules (per spec §3.7): * * 1. Overlay click → Radix fires `onOpenChange(false)`. * 2. Escape key → Radix fires `onOpenChange(false)`. * 3. Any other key → The window-level `keydown` listener below * fires `onClose` directly. * * The `?` key is intentionally NOT a dismiss trigger: it's the * toggle key the *parent* owns. The parent flips `open` to false * (and back to true, if it's a toggle) without the cheatsheet * pre-empting it — otherwise the "press ? to toggle" gesture would * silently no-op on the close half of the cycle. * * `aria-describedby={undefined}` suppresses the Radix warning about * a missing description; the cheatsheet's content (key list + helper * text) is its own description and there's no separate description * element worth pointing at. */ export function KeyboardCheatsheet({ open, onClose }: KeyboardCheatsheetProps) { // Dismiss on any key while open (except `?`, which the parent owns). // The listener is only attached when `open` is true — a closed // cheatsheet shouldn't be intercepting keystrokes for the rest of // the app. useEffect(() => { if (!open) return; const handler = (e: KeyboardEvent) => { if (e.key === "?") return; onClose(); }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [open, onClose]); return ( { // Radix fires `false` on overlay click, Escape, and the // dialog's own close button. Forward all three as a single // `onClose` to the parent. if (!o) onClose(); }} > below so // screen readers announce "Keyboard, dialog" instead of an // unlabeled region (Radix logs a warning otherwise). // - `aria-describedby={undefined}` suppresses Radix's // missing-description warning; the cheatsheet's content // (key list + helper text) is its own description. className="max-w-md bg-[color:var(--m-surface)] text-[color:var(--m-ink-primary)] border border-[color:var(--m-border-heavy)]/20" aria-labelledby={CHEATSHEET_TITLE_ID} aria-describedby={undefined} data-testid="keyboard-cheatsheet" >

Keyboard

{/* Explicit close button with a cheatsheet-specific label. The shared DialogContent primitive renders its own X in the top-right corner with a generic "Close dialog" label (out of scope here), so this one carries the specific aria-label and an aria-describedby pointer back to the heading for screen-reader context. `pr-10` on the header keeps the visible title text clear of the primitive's absolute-positioned X in the corner. */}
    {SHORTCUTS.map((s) => (
  • {s.keys.map((k) => ( {k} ))}
    {s.description}
  • ))}

Press any other key to dismiss.

); }