diff --git a/components/question-dock.tsx b/components/question-dock.tsx index a0adbb0..feb1bf4 100644 --- a/components/question-dock.tsx +++ b/components/question-dock.tsx @@ -323,8 +323,19 @@ function QuestionDockForm({ else if (kind === "quiz") setSel((prev) => ({ ...prev, selected: new Set(), otherSelected: false, dontKnow: !prev.dontKnow })); else { - selectOther(); - otherInputRef.current?.focus(); + // Space on the ask's Other row (owner refinement round, + // 2026-09-24): SELECTING it focuses the input for typing, but + // the Space that DESELECTS an already-selected Other must never + // drop the user back into the typing field — the core's + // toggleOther has no single-select off-path (it displaces), so + // the off-path lives here: clear the pick, keep the typed text, + // focus stays where it is. Re-selecting is the way back into + // the field. + if (sel.otherSelected) setSel((prev) => ({ ...prev, otherSelected: false, dontKnow: false })); + else { + otherInputRef.current?.focus(); + selectOther(); + } } }; @@ -439,7 +450,7 @@ function QuestionDockForm({ )} > {isSel && ( - + {qd.mode === "multi-select" ? "✓" : ""} )} @@ -509,7 +520,7 @@ function QuestionDockForm({ )} > {sel.otherSelected && ( - + {qd.mode === "multi-select" ? "✓" : ""} )} @@ -523,6 +534,20 @@ function QuestionDockForm({ // single-select, alongside the siblings in multi. setSel((prev) => (prev.otherSelected ? prev : toggleOther(selectMode, prev))); }} + onKeyDown={(e) => { + // Esc escapes the typing (owner refinement round, + // 2026-09-24): focus leaves the input, the row STAYS + // selected — deselection is the next Space's job, acting + // on the row — and the arrow walk resumes from this + // row (kbdIdx pins it). stopPropagation keeps the + // window chain from reading this Esc as abort-turn. + if (e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); + e.currentTarget.blur(); + setKbdIdx(optionCount); + } + }} placeholder="Type your own answer…" className="lh-input w-full min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground" onClick={(e) => e.stopPropagation()} diff --git a/tests/render-cards.test.tsx b/tests/render-cards.test.tsx index 0614a9c..19acb27 100644 --- a/tests/render-cards.test.tsx +++ b/tests/render-cards.test.tsx @@ -170,6 +170,10 @@ describe("render smoke tests", () => { assert.ok(html.includes('role="group"'), "checkbox group"); assert.ok((html.match(/role="checkbox"/g) ?? []).length >= 3, "options AND the Other row are checkboxes"); assert.ok(html.includes('data-testid="other-option"'), "Other is still an option row in multi-select"); + // The bigger \u2713 glyph (text-base, 2026-09-24 owner round) only + // mounts on a CHECKED row — unreachable from the unselected static + // render, so it's live-verified (Playwright: 14px computed in the + // 16px box) instead of pinned here. }); it("a malformed question input renders the quiet fallback, never a crash", () => {