Skip to content

Stop camera keys from moving the view while typing in a field - #1110

Open
Devin T. Currie (DTCurrie) wants to merge 2 commits into
mainfrom
fix/camera-keys-typing
Open

Devin T. Currie (DTCurrie) wants to merge 2 commits into
mainfrom
fix/camera-keys-typing

Conversation

@DTCurrie

Copy link
Copy Markdown
Member

Stops the camera's WASD and arrow keys from moving the view while a text field has focus. Before this, typing wasd into any input flew the camera, because the camera loop reads held keys off window with no field check.

Keybindings

  • isEditableTarget(target) is new. It reports whether a focused element takes typed text: an input, textarea or select, anything inside one, or a contenteditable element.

Components

  • InputBindings skips its per-frame camera movement while document.activeElement is editable.
  • KeyboardBindings replaces its private isEditable with isEditableTarget, so the shortcut dispatcher and the camera agree on what counts as typing.

Why?

Why check document.activeElement and not the key event's target?

The camera moves in a useTask loop that polls which keys are held. No key event reaches it, so the focused element is the only signal it has.

Testing

Added isEditableTarget.spec.ts, covering inputs, textareas, selects, contenteditable elements, buttons, the body and null. Changing the check's || to && fails cases in that spec and in the existing KeyboardBindings.svelte.spec.ts. The camera loop has no spec, because useTask needs a live canvas.

Ran the binaries behind pnpm check and pnpm test directly, since this was a worktree: svelte-check (0 errors) and the full vitest --run (134 files, 1975 tests). Ran eslint and prettier --check on the changed files. No Go changed, so go vet and golangci-lint did not run.

@changeset-bot

changeset-bot Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: de85a83

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@viamrobotics/visualization Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://viamrobotics.github.io/visualization/pr-preview/pr-1110/

Built to branch gh-pages at 2026-10-07 16:00 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@DTCurrie

Copy link
Copy Markdown
Member Author

Claude (@claude) review

@claude

claude Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished Devin T. Currie (@DTCurrie)'s task in 2m 7s —— View job


Review

  • Read CLAUDE.md
  • Read changed files
  • Run git diff
  • Post review

The fix is correct. isEditableTarget is extracted cleanly, the two consumers wire it in correctly (event target for KeyboardBindings, document.activeElement for the polling loop), and the changeset is a patch. Two observations:

Nit — doc comment overstates the contract (inline on isEditableTarget.ts)
The current wording "which no shortcut should take" describes the camera loop's policy, not what the predicate itself means. KeyboardBindings still fires modified shortcuts (⌘S, etc.) when this returns true. Suggestion: /** Whether target is an element that accepts typed text. */

Missing test: child of a contenteditable container (inline on isEditableTarget.spec.ts)
HTMLElement.isContentEditable returns true for elements that inherit editability from a parent, not just those that carry the attribute. A <span> inside a rich-text editor should block the camera, and the implementation handles it correctly, but there's no spec case for that shape. Suggestion included.

Observation (not a bug): task still runs while typing
running: () => anyKeysPressed in InputBindings.svelte doesn't know about the editable check, so the useTask loop schedules every frame while WASD keys are held, even when a field is focused. The early return keeps behavior correct. Moving the check into running isn't straightforward (the predicate feeds a Threlte scheduler, and isEditableTarget(document.activeElement) is not a reactive value), so the current placement is the right call.

editor.contentEditable = 'true'
document.body.append(editor)

expect(isEditableTarget(editor)).toBe(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing case: a child of a contenteditable container. HTMLElement.isContentEditable returns true for descendants that inherit editability from a parent, not just elements that carry the attribute themselves. A focused <span> inside a rich-text editor would satisfy isContentEditable and should block the camera — but there's no test for that shape.

Suggested change
expect(isEditableTarget(editor)).toBe(true)
expect(isEditableTarget(editor)).toBe(true)
const child = document.createElement('span')
editor.append(child)
expect(isEditableTarget(child)).toBe(true)

Comment thread src/lib/keybindings/isEditableTarget.ts Outdated
@@ -0,0 +1,4 @@
/** Whether keys pressed with `target` focused are typing into a field, which no shortcut should take. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second clause of the doc comment — "which no shortcut should take" — slightly overstates the contract. KeyboardBindings still fires modified shortcuts (⌘S, etc.) even when this returns true; the caller adds the !mod guard. The function itself is correct; the doc just describes the camera loop's stricter policy rather than what this predicate means on its own.

Suggested change
/** Whether keys pressed with `target` focused are typing into a field, which no shortcut should take. */
/** Whether `target` is an element that accepts typed text. */

@DTCurrie

Copy link
Copy Markdown
Member Author

Nit — doc comment overstates the contract (inline on isEditableTarget.ts)
The current wording "which no shortcut should take" describes the camera loop's policy, not what the predicate itself means. KeyboardBindings still fires modified shortcuts (⌘S, etc.) when this returns true. Suggestion: /** Whether target is an element that accepts typed text. */

Missing test: child of a contenteditable container (inline on isEditableTarget.spec.ts)
HTMLElement.isContentEditable returns true for elements that inherit editability from a parent, not just those that carry the attribute. A inside a rich-text editor should block the camera, and the implementation handles it correctly, but there's no spec case for that shape. Suggestion included.

Claude (@claude) resolve these

@claude

claude Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished Devin T. Currie (@DTCurrie)'s task in 2m 44s —— View job


Resolved

  • Fix doc comment in isEditableTarget.ts — simplified to /** Whether \target` is an element that accepts typed text. */`
  • Add child-of-contenteditable test case in isEditableTarget.spec.ts — appends a <span> child to the editor fixture and asserts isEditableTarget(child) is true
  • Prettier and ESLint passed on changed files
  • Committed and pushed (de85a83)

Note: pnpm test requires Playwright installed in the browser cache, which is unavailable in this CI runner. The PR author confirmed the same limitation. The new test case is structurally identical to the existing ones and exercises HTMLElement.isContentEditable on a descendant, which is exactly what the implementation already handles.

Co-authored-by: Devin T. Currie <DTCurrie@users.noreply.github.com>

This branch has not been deployed

No deployments
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.

1 participant