Skip to content

useKeyCapture skips isButtonActivation, so a Space claim kills native button activation #4748

Description

@atomantic

Problem

client/src/hooks/useKeyCapture.js and client/src/hooks/useKeyboardShortcuts.js both answer the same question — "should this window key event reach an app-level handler?" — but they filter different things:

guard useKeyboardShortcuts useKeyCapture
isEditableTarget
aria-modal dialog open enabledInDialog enabledInDialog (added in #4731)
⌘/Ctrl/Alt chord
e.repeat ignoreRepeat
isButtonActivation

isButtonActivation (client/src/lib/a11yKeyboard.js) is the sharp one. VoiceWidget deliberately stands down for Space on a focused button (client/src/components/voice/VoiceWidget.jsx, added because the POST drills' Start/Match buttons broke), and useKeyboardShortcuts mirrors that. useKeyCapture does not: it preventDefaults Space unconditionally, so every claiming surface re-breaks the native button activation the other two paths specifically protect.

Each new option added at one hook also has to be re-remembered at the other, and only one of the five has been kept in sync so far.

Approach

Extract the shared predicate into client/src/lib/a11yKeyboard.js (which already hosts isButtonActivation) — something like shouldIgnoreGlobalKey(event, { enabledInDialog, ignoreRepeat, allowChords }) — and have both hooks call it. Then a guard lands in one place for both.

Sequence the isButtonActivation part carefully: it changes behavior at all useKeyCapture call sites (POST cognitive drills, Morse keyer, RapidReader, OpenWorld playback transport, exploration-mode jump). The drills' Start-button interaction was tuned recently, so that surface needs its suite re-read rather than a blind flip.

Acceptance criteria

  • One predicate module answers "should this global key handler ignore this event", consumed by both hooks.
  • Space on a focused <button> activates the button rather than being claimed, at every useKeyCapture call site.
  • A test proves that, using the stand-in-listener pattern in client/src/test/voiceHotkeySpy.js.
  • Existing drill / Morse / RapidReader / OpenWorld keyboard behavior is otherwise unchanged.

Also worth folding in (small)

client/src/pages/OpenWorld.transport.test.jsx and client/src/pages/OpenWorld.fastTravel.test.jsx carry ~40 lines of identical mock preamble (scene/HUD/overlay stubs, the useOpenWorldData payload, the 8-endpoint services/api stub). A third OpenWorld page suite makes it three copies, and each new endpoint the page polls has to be added to every copy. Export the fixture payloads from a shared client/src/test/openWorldPageMocks.js and dynamic-import them inside each vi.mock factory (the vi.mock calls themselves must stay per-file for hoisting).

Related to #4731.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcode-qualityeffort:mediumRecommended reasoning effort: mediumplanTracked by /do:replan

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions