Skip to content

fix(keyboard): stop the NiiVue tab from capturing keys and double-applying c/v (#223, #224)#242

Merged
korbinian90 merged 2 commits into
mainfrom
claude/vigilant-babbage-b97ef1
Jun 15, 2026
Merged

fix(keyboard): stop the NiiVue tab from capturing keys and double-applying c/v (#223, #224)#242
korbinian90 merged 2 commits into
mainfrom
claude/vigilant-babbage-b97ef1

Conversation

@korbinian90

@korbinian90 korbinian90 commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Fixes #223 and #224. Both are keyboard-handling conflicts between the VS Code/host layer and NiiVue's own canvas hotkeys.

#223 — NiiVue tab captures keys meant for the "top box"

The extension contributes keybindings for 1-5, r, i, b, x, m, z, s, u, shift+u, shift+d, etc., gated only by when: activeCustomEditorId == niiVue.default. Those commands are intentional no-ops (extension.ts) — the webview's own keydown listener does the real work; the VS Code commands exist only for command-palette discovery and keybinding customization.

The problem: activeCustomEditorId stays niiVue.default even when keyboard focus moves to Quick Open or the command palette. So when you open the "top box" over a NiiVue tab and type, the single-key bindings fire their no-op commands and swallow the keystrokes instead of letting them reach the input.

Fix: add && !inputFocus to every contributed keybinding. inputFocus is true when a VS Code input widget (Quick Open, command palette, etc.) is focused and false when the webview itself is focused, so the shortcuts now only act when the viewer holds focus.

#224c/v applied twice on the focused canvas

The app's useKeyboardShortcuts hook handles c/v on keydown and broadcasts the action to every selected canvas. NiiVue's own canvas handler also handles them on keyup (clipPlaneHotKey/viewModeHotKey), but only for the focused/in-bounds canvas. Result: the focused canvas is acted on twice per press — e.g. one c lands on clip-plane index 2 instead of 1.

Fix: construct each ExtendedNiivue with clipPlaneHotKey: '' and viewModeHotKey: ''. NiiVue matches hotkeys with a strict KeyboardEvent.code === hotKey, so an empty string never matches and its built-in c/v handlers are disabled. The app's broadcast handler becomes the single source of truth, so every selected canvas advances exactly once.

Tests

  • apps/vscode/test/keybindings.test.ts (new): asserts every contributed keybinding carries both activeCustomEditorId == niiVue.default and !inputFocus, with an extra guard over the bare printable keys.
  • apps/pwa/tests/keyboard-shortcuts.spec.ts: new e2e case asserting a single c press lands on clip-plane index 1.
  • Unit suites pass (vscode 80/80, niivue-react 85/85); type-check + lint clean. The affected e2e specs pass locally in the WebGL lane (26/26).

Note on arrow keys (investigated, intentionally not changed)

An earlier revision also tried to make the app the single source of truth for the ←/→ (4D frame) keys, on the assumption they double-applied like c/v. CI disproved that: the app's volumeNext/volumePrev pass a volume object to nv.setFrame4D(id, frame), which expects a volume id, so they are silent no-ops — NiiVue's own handler is the only thing that advances the frame on arrow press. Suppressing it broke arrow navigation (and the pre-existing 4d-sync keyboard test). That change has been reverted; NiiVue keeps owning the arrow keys, and there is no double-apply to fix. (The volumeNext/volumePrev no-op is a real but separate latent bug, worth a follow-up.)

Reviewer notes

  • The events.ts change lives in the shared @niivue/react package, so the [bug] clip plane shortcut c is applied twice #224 fix reaches the apps that mount the shared Menu/useKeyboardShortcuts handler: vscode, PWA, Jupyter, Tauri, and Streamlit's styled viewer.
  • Trade-off: Streamlit's unstyled embed (UnstyledCanvasContainer only, no Menu) has no app keyboard handler, so disabling NiiVue's built-in c/v removes them there with no replacement. That mode has no shortcut UI, so this is considered acceptable; called out in the changeset.
  • The !inputFocus change is VS Code-only (apps/vscode/package.json).

Known residual / follow-ups (out of scope here)

  • volumeNext/volumePrev keyboard handlers are no-ops (wrong arg to setFrame4D); NiiVue currently covers arrow navigation, so it is not user-visible, but the app-level broadcast for arrows never actually runs.
  • m triggers two different actions when a canvas is focused (app: toggle crosshair; NiiVue: cycle drag mode) — same conflict root, but not a double-apply.
  • Pre-existing showHeader keybinding mismatch (ctrl+h vs mac cmd+shift+h, while the webview handler expects ctrl+shift+h).

🤖 Generated with Claude Code

@korbinian90 korbinian90 force-pushed the claude/vigilant-babbage-b97ef1 branch from 7f10e75 to b45bd2b Compare June 14, 2026 21:27
@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

🚀 PWA Preview Deployment

Your PWA preview has been deployed!

Preview URL: https://niivue.github.io/niivue-vscode/pr-242/


This preview will be updated automatically when you push new commits to this PR.

github-actions Bot added a commit that referenced this pull request Jun 14, 2026
…223, #224)

VS Code shortcut keybindings (1-5, r, i, b, x, m, z, s, u, ...) were gated
only by `activeCustomEditorId == niiVue.default`, which stays true even when
focus moves to Quick Open or the command palette. Because the bound commands
are no-ops (the webview's own keydown listener does the real work), the
bindings merely swallowed those keystrokes, so they could not be typed into
the "top box". Add `&& !inputFocus` to every binding so they only fire when
the viewer holds focus (#223).

NiiVue's built-in `c` (clip plane) and `v` (view mode) hotkeys ran on the
focused canvas on top of the app's useKeyboardShortcuts handler, which
already broadcasts to every selected canvas. The focused canvas was therefore
acted on twice (once on keydown by the app, once on keyup by NiiVue). Disable
NiiVue's clipPlaneHotKey/viewModeHotKey so the app is the single source of
truth (#224).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@korbinian90 korbinian90 force-pushed the claude/vigilant-babbage-b97ef1 branch from b45bd2b to 5400bb6 Compare June 14, 2026 22:02
github-actions Bot added a commit that referenced this pull request Jun 14, 2026
@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

coverage

Overall line coverage: 38.5% (−2.7) vs main

Package Statements Branches Functions Lines
Shared core (packages/niivue-react) 41.2% (+0.3) 42.7% (+0.2) 45.8% (+0.3) 41.7% (+0.3)
apps/pwa 28.9% 33.3% 52.9% 30.2%
apps/jupyter 14.4% 15.9% 14.9% 14.5%
apps/streamlit 17.8% 5.3% 18.5% 17.7%
apps/vscode 38.7% 39.6% 18.9% 38.1%
apps/desktop-tauri 81.8% 59.1% 78.9% 84.3%

📊 View full report →

github-actions Bot added a commit that referenced this pull request Jun 15, 2026
@korbinian90 korbinian90 merged commit 274fa33 into main Jun 15, 2026
13 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🧹 PWA Preview Cleanup

The preview deployment for this PR has been removed.

github-actions Bot added a commit that referenced this pull request Jun 15, 2026
github-actions Bot added a commit that referenced this pull request Jun 15, 2026
korbinian90 added a commit that referenced this pull request Jun 17, 2026
Resolve the conflict in apps/pwa/tests/keyboard-shortcuts.spec.ts. Keep the e2e->unit migration (the single wiring smoke test) and preserve #242's #224 clip-plane regression as an integration test: that NiiVue's built-in clipPlaneHotKey is disabled so one 'c' press is one step is a claim the jsdom hook unit test cannot make (it has no real NiiVue canvas).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] vscode niivue tab captures keys

1 participant