Skip to content

fix(ui): ring a row only while it holds the focus itself - #84

Merged
winetree94 merged 1 commit into
mainfrom
settings-double-focus
Aug 6, 2026
Merged

fix(ui): ring a row only while it holds the focus itself#84
winetree94 merged 1 commit into
mainfrom
settings-double-focus

Conversation

@winetree94

Copy link
Copy Markdown
Contributor

What

Settings → Appearance painted two focus rings when tabbing to the Theme select: one around the TRSelect trigger and one around the whole row. The same doubling appeared on Language and on every settings row carrying a focusable control.

Root cause

CoderListRow drove its ring from Focus.onFocusChange, which reports FocusNode.hasFocus — true when the node or any descendant holds primary focus. canRequestFocus: false stopped the row from being a tab stop but not the callback, so a focused trailing control flipped the row too.

Previous reports were fixed at call sites, never at the row primitive, which is why it kept coming back. Upstream tinyrack_ui fixed the identical bug for TRTreeNav in 0.29.1 by following the primary focus; this row never got that treatment (it predates it, d974d83).

Changes

  • coder_list_row.dart — track hasPrimaryFocus instead of hasFocus.
  • coder_list_row.dart — the ring moves to an always-present foregroundDecoration that only changes colour. Adding/removing an inset border shrank the content box and re-laid-out the trailing control, destroying its focus node mid-traversal — the keyboard trap upstream documented in CHANGELOG 0.30.1 / 0.31.1.
  • controlOwnsFocus (new, plumbed through SettingsRow) — a row whose onTap only repeats what its control already does hands the control its single tab stop and drops its own button semantics. Set on CoderSwitchRow / CoderCheckboxRow; pointer taps on the row still toggle.
  • chat_tool_card.dart — a tab stop that painted no focus indicator at all, the same invariant from the other side. Now rings the same way.

Session/terminal tabs (app.dart:1459,1498) and the skill list row keep two tab stops: selecting and closing/toggling are different actions.

Audit

All 44 CoderListRow / SettingsRow call sites were enumerated. The spurious ring affected every non-interactive row with a focusable control (general_settings_page.dart:121,177, agent_settings_page.dart:431,454, advanced_settings_page.dart:65, settings_page.dart:369,474,598, app_settings_page.dart:325, app.dart:1785); all are fixed by the primitive change with no call-site edits.

Tests

New apps/coder_app/test/focus_ring_test.dart (7 cases): four failed for the intended reason before the fix. Covers the double ring, a nav row still ringing, no layout shift on focus, one tab stop per switch row, two stops kept for a distinct trailing action, pointer toggle preserved, and tool-card focus visibility.

Verification

  • dart run melos verify — all 12 gates pass, goldens unchanged (unfocused pixels are identical).
  • dart run melos verify:debug — passes against the real Debug Flutter runner and embedded daemon (run under xvfb-run on this headless machine; CI's Linux Debug E2E shard covers it natively).

A settings row read plain focus, which a Focus node reports for its
descendants too, so tabbing to the Theme select painted a ring around the
select and another around the row it sits in. The row now follows the
primary focus, as TRTreeNav upstream already does.

The ring moves to a foreground decoration that is always present and only
changes colour. Adding and removing a border inset the content box, which
re-laid-out the trailing control and destroyed its focus node as traversal
stepped onto it.

A row whose tap only repeats what its trailing control already does now
hands the control its single tab stop, so a switch setting costs one Tab
press and announces itself once. A row whose control does something else,
such as a session tab and its close button, keeps both stops.

The tool card in the transcript is a tab stop that drew no focus at all,
which is the same invariant seen from the other side, so it draws the ring
the same way.
@winetree94
winetree94 added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit 9e5f018 Aug 6, 2026
34 checks passed
@winetree94
winetree94 deleted the settings-double-focus branch August 6, 2026 15:04
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