feat(editor): own input via a hidden focusable textarea (ADR-036) - #5
Merged
Conversation
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]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First step of ADR-036 (recorded in #4 — merge that first for the ADR text, though this stands alone).
Problem
editor.tslistened ondocumentfor keydown. So doesinput.ts. Nothing coordinated the two — the only thing keeping them apart was the server having sentinput.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.tsuses 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, nevermountNode, 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
renderEditorruns while the subtree is still detached — the caller attaches it afterwards — so.focus()silently did nothing. Not a test artifact:main.tshas the same ordering, so the editor never took focus in the browser either. Fixed with an explicitfocusActiveEditor()after the attachingreplaceChildren, 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
68 passing across 11 filestsc --noEmitclean, bundle buildsdocument.bodyno longer reaches the editorNot verified: real mobile keyboard, IME, and paste. This is tests-in-happy-dom only. The textarea makes them possible; composition still needs explicit
compositionstart/compositionendhandling, which is deliberately not in this change.Next
Reconciling paint instead of
replaceChildren— retires theactiveEditorsingleton and thefindEditorIdpre-scan, and makesviewport.resizesafe by construction rather than by a guard.