Skip to content

focus: let a focused widget claim Tab - #199

Merged
kolkov merged 1 commit into
gogpu:mainfrom
samyfodil:fix/focus-tab-grab
Aug 2, 2026
Merged

focus: let a focused widget claim Tab#199
kolkov merged 1 commit into
gogpu:mainfrom
samyfodil:fix/focus-tab-grab

Conversation

@samyfodil

Copy link
Copy Markdown
Contributor

The focus manager consumes Tab and Shift+Tab for traversal before the widget tree is reached, and handleTab returns true unconditionally — whether or not there is anywhere to traverse to. A widget that needs Tab itself can therefore never see it.

That makes it impossible to embed a terminal emulator: shell completion is bound to Tab, and the key never arrives. Same for a code editor with indent-on-Tab, or any grid that wants Tab to move a cell.

What this adds

widget.KeyGrabber, a single optional method:

type KeyGrabber interface {
    GrabsKey(k event.Key) bool
}

Before consuming a key it reserved, the manager asks the focused widget whether it wants it, and steps aside if the answer is yes.

  • Only the focused widget is consulted, so nothing else in the tree changes behaviour.
  • Only for keys the manager was already going to consume.
  • Widgets that do not implement it are unaffected — traversal works exactly as before.

Prior art

Qt exposes the same escape hatch through focusNextPrevChild; the web does it with preventDefault on Tab. Both put the decision on the focused widget rather than on the traversal code.

Tests

TestTabReachesAKeyGrabber covers both directions: a focused widget that claims Tab receives it, and the same widget with the claim withdrawn leaves traversal exactly as it was.

The focus manager consumes Tab and Shift+Tab for traversal before the widget
tree is reached, and handleTab returns true unconditionally — whether or not
there is anywhere to traverse to. A terminal emulator can therefore never see
Tab, so shell completion is dead in any app that embeds one.

Add widget.KeyGrabber: the manager asks the FOCUSED widget whether it wants a
key the manager was about to consume, and steps aside if so. Only that widget
is consulted, and only for keys already reserved, so traversal elsewhere is
untouched. Qt exposes the same escape hatch via focusNextPrevChild, the web
via preventDefault on Tab.
@samyfodil
samyfodil requested a review from kolkov as a code owner August 1, 2026 22:05
@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@kolkov kolkov 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.

Clean, minimal addition that solves a real problem. Verified against enterprise references:

  • Qt focusNextPrevChild(bool) — same pattern, Tab-specific, widget returns false to claim
  • Flutter FocusNode.onKeyEvent — more general (any key, bubbles), but requires architecture we don't have
  • Web preventDefault() on keydown — same semantics

The GrabsKey(Key) signature is better than a Tab-specific GrabsTab() — forward-compatible for when the manager starts consuming Escape (dialog dismissal) or arrows (focus direction).

Shift+Tab is covered implicitly since both produce KeyTab with modifier — the widget decides via GrabsKey.

Two minor suggestions (non-blocking):

  1. Shift+Tab test case — add one block to TestTabReachesAKeyGrabber verifying Shift+Tab also reaches the grabber. Currently only plain Tab is tested.

  2. Accessibility note in godoc — widgets that grab Tab remove the standard keyboard navigation path. One sentence would help: "Widgets that grab Tab should provide an alternative way to leave focus (e.g., Escape) for accessibility."

Separate interface is correct for Go — merging into Focusable would force all focusable widgets to implement GrabsKey. Type assertion on the focused widget is the idiomatic pattern (same as PointerCapturer, KeyGrabber, Lifecycle).

@kolkov
kolkov merged commit cd1f292 into gogpu:main Aug 2, 2026
9 checks passed
@kolkov kolkov mentioned this pull request Aug 2, 2026
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.

2 participants