fix(forms): never lose a typed option — commit on blur, keep focus through + #80

Open
laoc wants to merge 5 commits from feat/option-commit-on-blur into feat/issue-77-form-preview
Owner

A typed option was silently dropped unless committed with Enter or the + button. Now it commits on blur too, and clicking + no longer steals focus from the input (preventDefault on mousedown — mouse-specific; the keyboard path was already safe because a keyboard click targets the focused element).

TestOER-verified at 849d9069 including the E(0 chips)/F(8 chips) differentials; f4225866 adopts their option-editor scope probe.

Stacked on #78 (base: feat/issue-77-form-preview). Buzz thread: #edufeed-app.

🤖 Generated with Claude Code

A typed option was silently dropped unless committed with Enter or the + button. Now it commits on blur too, and clicking + no longer steals focus from the input (preventDefault on mousedown — mouse-specific; the keyboard path was already safe because a keyboard click targets the focused element). TestOER-verified at 849d9069 including the E(0 chips)/F(8 chips) differentials; f4225866 adopts their option-editor scope probe. Stacked on #78 (base: feat/issue-77-form-preview). Buzz thread: #edufeed-app. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Typed text in the options editor's pending input was silently dropped
unless the author pressed Enter or clicked + — clicking anywhere else,
including straight onto Preview, lost it (reported by laoc with a
checkbox field, but select and radio share this editor).

One commitPendingOption() helper replaces the two copies of the push
logic; it now also fires on blur. Whitespace-only input is discarded
(new — previously a spaces-only value could be added via Enter). The +
click cannot double-add: the preceding blur commits and clears, so the
click sees an empty input.

Component test drives the real row: type, blur, assert the option
landed. Written first and red at 226302bc for exactly that assertion;
mutation check (onblur line removed) turns exactly that test red.
TestOER's real-browser differential at d1adde48: mousedown on + blurred
the input, the new blur-commit re-rendered the chip row, the moved
button never received the click, and focus died on the button — the
type-then-+ authoring loop was broken (no data loss; blur had already
committed the option).

preventDefault on the button's mousedown keeps focus in the input, so
no blur fires, nothing re-renders mid-click, and the click handler is
the single committer.

The old jsdom 'no double-add' test was green about the right outcome
for the wrong reason (its synthetic blur+click order never happens in a
real browser now). Rewritten to pin what jsdom can pin: the mousedown
is defaultPrevented and the click commits exactly once. The browser
truth lives in e2e/option-blur-commit.test.js (TestOER's probe, adopted
verbatim): 2 failed at d1adde48, 5 passed with this fix, same specs.

Escape still commits on blur rather than discarding — deliberate
product question, not decided here.
E (zero chips) and F (eight chips, wrapped row) are proven able to fail
by the d1adde48/849d9069 differential — the focus regression reproduced
at both ends, so chip count was never the variable. G (keyboard Tab+
Enter, works on both sides because a keyboard click targets the focused
element and has no hit-test to lose) and H (type then straight to
Preview at zero chips) are the always-green controls that make E/F
attributable.

Written by TestOER, adopted verbatim; svelte-check run WITH the file in
this time (0 errors).
Enter, blur and + all COMMIT, so the pending-option input had no
explicit cancel at all — and any Escape-closing ancestor would blur the
input on its way out, committing the very text being abandoned. Escape
with text now discards it and keeps focus; Escape on an empty input
bubbles unchanged so a second press still reaches the ancestor.

Attached with onkeydowncapture, not onkeydown: Svelte 5 delegates plain
keydown to the render root, so a stopPropagation there runs only AFTER
the event has already passed every native ancestor listener — measured
by the new consumed-test failing against the delegated handler. The
blur-after-Escape control pins that the text is discarded, not left
pending for the blur-commit to resurrect.

UX call delegated by laoc in Buzz #edufeed-app (EKKW thread, 2026-08-03).
option-escape-discard.test.js (A-D: discard survives a real re-render on
the same stamped DOM node with focus held; document-level bubble listener
blindness with empty-Escape/Enter positive controls; capture-phase
ancestors still see the key; preventDefault stops a native <dialog>
cancel while the no-preventDefault arm closes) and
option-escape-levels.test.js (six-level listener table that separates
capture from delegated attachment — the discriminating probe my
consumed-test was not). Adopted verbatim from their 4/4 green run at
2e5237f9; both svelte-checked here (0 errors).

Comment refined per their level table: a delegated stopPropagation DOES
still shield body/document/window (above the delegation root, where the
app's svelte:window/document Escape handlers live) — what it cannot
shield is DOM ancestors below the root. My earlier "plain onkeydown
cannot consume a key in this app" was the too-strong version of that.
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/option-commit-on-blur:feat/option-commit-on-blur
git switch feat/option-commit-on-blur

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch feat/issue-77-form-preview
git merge --no-ff feat/option-commit-on-blur
git switch feat/option-commit-on-blur
git rebase feat/issue-77-form-preview
git switch feat/issue-77-form-preview
git merge --ff-only feat/option-commit-on-blur
git switch feat/option-commit-on-blur
git rebase feat/issue-77-form-preview
git switch feat/issue-77-form-preview
git merge --no-ff feat/option-commit-on-blur
git switch feat/issue-77-form-preview
git merge --squash feat/option-commit-on-blur
git switch feat/issue-77-form-preview
git merge --ff-only feat/option-commit-on-blur
git switch feat/issue-77-form-preview
git merge feat/option-commit-on-blur
git push origin feat/issue-77-form-preview
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
edufeed/edufeed-app!80
No description provided.