Skip to content

feat(editor): own input via a hidden focusable textarea (ADR-036) - #5

Merged
myrddian merged 1 commit into
mainfrom
feat/editor-input-ownership
Aug 1, 2026
Merged

feat(editor): own input via a hidden focusable textarea (ADR-036)#5
myrddian merged 1 commit into
mainfrom
feat/editor-input-ownership

Conversation

@myrddian

@myrddian myrddian commented Aug 1, 2026

Copy link
Copy Markdown
Owner

First step of ADR-036 (recorded in #4 — merge that first for the ADR text, though this stands alone).

Problem

editor.ts listened on document for keydown. So does input.ts. Nothing coordinated the two — the only thing keeping them apart was the server having sent input.prompt {mode: "none"} for that screen (WizardFormApp.defaultInputPrompt). Any editor-bearing screen that forgot got every keystroke handled twice.

Downstream of the same choice: no IME support anywhere in the frontend, no paste path in the editor, nothing for assistive tech to focus, and no mobile on-screen keyboard — a keyboard needs a focused input, which is exactly why input.ts uses one (#42).

Change

Each editor owns a hidden-but-focusable <textarea> and reads keys from it. Focus becomes the arbiter, so the coordination problem goes away structurally rather than by convention. mode: "none" still gets sent; it's now an optimisation, not a correctness requirement.

Painting stays fully custom — the terminal aesthetic is untouched. Repaints replace a separate paintNode, never mountNode, so the key source survives every keystroke. Re-creating it 60×/minute would drop focus and make IME composition impossible.

This is the Ace / CodeMirror 5 / Monaco split: native input handling, custom painting.

Bug found by the tests

renderEditor runs while the subtree is still detached — the caller attaches it afterwards — so .focus() silently did nothing. Not a test artifact: main.ts has the same ordering, so the editor never took focus in the browser either. Fixed with an explicit focusActiveEditor() after the attaching replaceChildren, rather than a timing hack.

focusKeyInput() takes focus but never steals it: if another field has focus it stays there. Strictly less aggressive than swallowing every document keystroke.

Verification

  • 5 new tests, 68 passing across 11 files
  • tsc --noEmit clean, bundle builds
  • The regression test that matters: a keydown on document.body no longer reaches the editor

Not verified: real mobile keyboard, IME, and paste. This is tests-in-happy-dom only. The textarea makes them possible; composition still needs explicit compositionstart/compositionend handling, which is deliberately not in this change.

Next

Reconciling paint instead of replaceChildren — retires the activeEditor singleton and the findEditorId pre-scan, and makes viewport.resize safe by construction rather than by a guard.

The editor listened on `document` for keydown. So does input.ts, and
nothing coordinated the two — the only thing keeping them apart was the
server having sent `input.prompt {mode: "none"}` for the screen. A
screen that forgot double-handled every keystroke.

Each editor now owns a hidden-but-focusable textarea and reads keys from
it, so focus is the arbiter and the coordination problem disappears
structurally. Painting stays fully custom: repaints replace a separate
paint node, never the mount node, so the key source survives every
keystroke — re-creating it would drop focus and make IME composition
impossible.

Also fixes a latent focus bug the tests surfaced: renderEditor runs while
the subtree is still detached and the caller attaches it afterwards, so
.focus() was a no-op. Hence the explicit focusActiveEditor() step in
main.ts after the attaching replaceChildren.

focusKeyInput takes focus but never steals it — if another field has it,
it stays there. Strictly less aggressive than swallowing every document
keystroke, which is what it replaces.

Unblocks mobile on-screen keyboard and gives IME/paste something to
attach to (composition handling itself is not in this change).

Regression test: a keydown on document.body no longer reaches the editor.

Co-Authored-By: Claude Opus 5 <[email protected]>
@myrddian
myrddian merged commit d23d68e into main Aug 1, 2026
6 of 7 checks passed
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