focus: let a focused widget claim Tab - #199
Conversation
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.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
kolkov
left a comment
There was a problem hiding this comment.
Clean, minimal addition that solves a real problem. Verified against enterprise references:
- Qt
focusNextPrevChild(bool)— same pattern, Tab-specific, widget returnsfalseto 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):
-
Shift+Tab test case — add one block to
TestTabReachesAKeyGrabberverifying Shift+Tab also reaches the grabber. Currently only plain Tab is tested. -
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).
The focus manager consumes Tab and Shift+Tab for traversal before the widget tree is reached, and
handleTabreturnstrueunconditionally — 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:Before consuming a key it reserved, the manager asks the focused widget whether it wants it, and steps aside if the answer is yes.
Prior art
Qt exposes the same escape hatch through
focusNextPrevChild; the web does it withpreventDefaulton Tab. Both put the decision on the focused widget rather than on the traversal code.Tests
TestTabReachesAKeyGrabbercovers both directions: a focused widget that claims Tab receives it, and the same widget with the claim withdrawn leaves traversal exactly as it was.