Skip to content

fix(app): make the question dock keyboard-first and stop it flashing - #58494

Merged
kitlangton merged 3 commits into
betafrom
question-dock
Oct 24, 2026
Merged

kitlangton merged 3 commits into
betafrom
question-dock

Conversation

@kitlangton

@kitlangton kitlangton commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What it does

Makes the question dock work from the keyboard the way you'd expect, and stops it flashing. The arrows only move a highlight, Space marks an answer and Enter commits it and moves on.

  • Pick one: nothing is picked when a question opens. Up/Down move the highlight, Space picks and stays, Enter picks and goes to the next question (or submits on the last), and 1–9 pick without advancing.
  • Pick many: Space or 1–9 toggle on key down (no repeats, no double toggle from the trailing click), and Enter moves on with whatever is checked.
  • Type your own answer: arrowing onto it only highlights it. Typing a character starts the answer with it and picks it, and Enter in the field moves on.
  • Left/Right switch questions when focus isn't in the text field. ⌘[ / ⌘⏎ (Alt+← / Ctrl+⏎) still work.
  • One highlighted card at a time; the pick shows only as the radio dot or check. The highlight, the answers box height and question changes move on a no-bounce spring instead of popping. Reduced motion gets a plain fade.

How it works

Key handling lives in a pure questionKey(event, { mac, zone, count }) in question-keys.ts, which maps one keydown to one intent (move, activate, commit, type, number…); RTL flips the arrows, and nothing fires during IME composition. The highlight is a roving tabindex: store.focus is the highlighted answer and holds the only tab stop, so screen readers announce it separately from aria-checked. Mouse hover moves the highlight too. It is drawn as one layer behind the cards that glides to the highlighted card by its clip-path, landing at once on a new question. The box height follows a ResizeObserver through a spring, and a question change slides 12px plus a fade via WAAPI.

Risk

Low. Contained to the question dock. Roles and aria-checked are unchanged. Right Arrow on the last question does not submit; Enter, Next and ⌘⏎ do.

Proof

Today (left) vs the first cut of this PR (right), keyboard only. This was recorded before the highlight model, so on the right ↓ still picks:

https://problems.kitlangton.com/media/Ub0fG5f_RsdLJyh08vCkxQ.mp4

  • question-keys.test.ts: unit tests for every intent, including Enter, typing on the custom answer and IME composition
  • session-question-dock.spec.ts: component tests for ↓ not changing the answer, Space marking, Enter advancing in both question types, typing into the custom answer, Left/Right and IME composition

…itions

Up/Down move the selection in single-select questions and move a quiet focus ring in multi-select ones. Space acts on key down. Left/Right switch questions outside the text field. Selection, focus, marks and progress fade on one spring instead of swapping properties, the answers box follows each question's height, and a new question slides in from the side it comes from.

@opencode-agent opencode-agent Bot left a comment

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.

Works as described. I reproduced the old behaviour by running the PR's new component tests against the base: 4 fail there (radio Up/Down, Left/Right switching, Space on key down, IME Enter/Escape). The full question dock spec passes with this change, and so do the 8 questionKey unit tests and tsc -b in packages/app. The before/after screenshots show the same view after one ArrowDown: on the base only the focus moves; with this change the selection moves too.

Public API is unchanged; this only touches the web app's question dock and its CSS.

No blocking concerns.

Before

After

@opencode-agent opencode-agent Bot left a comment

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 new commit works as described. I re-ran question-keys.test.ts (9 pass), the question dock and focus-ring component specs (18 pass in headless Chromium) and tsc -b in packages/app (clean). The after screenshot shows ArrowDown highlighting "Wide" without picking it.

Public API is unchanged.

One thing worth fixing, inline: after a mouse move, Space and Enter can act on a different answer from the one that has keyboard focus.

Before

After

if (store.editing || sending()) return
focus(store.focus + step)
// The pointer moves the highlight like the arrows do, but never takes focus from the custom answer's field.
const hover = (i: number, event: PointerEvent) => {

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.

hover() moves store.focus but leaves keyboard focus where it was. activate and commit then act on store.focus, not on the focused button. So if keyboard focus is on answer 1 and the mouse passes over answer 3, Space toggles (or Enter picks) answer 3. A screen reader still reports answer 1, and that button has dropped to tabindex=-1. Could hover also move focus (with preventScroll) when focus is already inside the list? Or could Space/Enter act on the focused element?

@kitlangton
kitlangton merged commit 3347afd into beta Oct 24, 2026
11 of 15 checks passed
@kitlangton
kitlangton deleted the question-dock branch October 24, 2026 03:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant