mirror of
https://github.com/anomalyco/opentui.git
synced 2026-04-24 20:45:56 +03:00
[GH-ISSUE #638] Proposal: <focus-scope> element with scope-aware keyboard hooks #169
Labels
No labels
bug
core
documentation
feature
good first issue
help wanted
pull-request
question
react
solid
tmux
windows
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
starred/opentui#169
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @cevr on GitHub (Feb 7, 2026).
Original GitHub issue: https://github.com/anomalyco/opentui/issues/638
Problem
There's currently no way to isolate keyboard events to a subtree. When an overlay/modal is open, keystrokes leak to handlers underneath. There's also no built-in Tab/Shift+Tab focus cycling within a container.
Real example: in a project using opentui, a cross-references popup renders over a Bible reader — but
j/kkeypresses still scroll the reader behind it.Proposal:
<focus-scope>A declarative container element (like Radix FocusScope or React Aria's
FocusScope) that provides:stopPropagationcalled automatically for trapped scopesuseKeyboard— detects nearest scope via a ref, no wrapper components neededUsage
Props
trapFocusbooleantrueautoFocusbooleantrueHook API
useKeyboard(callback, options?)ref() => RenderablereleasebooleanDesign
Keyboard interception via
prependListenerEventEmitter.prependListenerputs a handler at index 0 of the listener array.emitWithPriority()hits it first, it callsstopPropagation(), and the loop exits — no other handlers fire.prependListenergoes before outer → fires first)Scope detection: ref-based tree walking
Consumer passes
{ ref: () => ref }whererefpoints to a renderable inside the scope. AtonMounttime,closestFocusScope()walks up the.parentchain usinginstanceof FocusScopeRenderable. If found, subscribes to scope's local dispatch (.on()/.off()). If not found, falls back to global keyboard handler.Architecture
FocusScopeRenderableextendsBoxRenderable(transparent container)lib/focus-traversal.ts(internal, not exported from public API)Implementation
PR: #646
76 tests across core (49) and solid (17).
@cevr commented on GitHub (Feb 8, 2026):
Implementation update from PR #646:
The final implementation intentionally diverged from the original
<focus-scope>proposal to reduce API surface.What shipped on the branch:
<focus-scope>/<focus_scope>component API.trapFocus(scope + Tab/Shift+Tab cycle + key isolation)autoFocus(optional mount autofocus)currentFocusedRenderableup parent chain (processKeyEvent/processKeyReleaseEvent/processPasteEvent), so scope semantics are tree-native.useKeyboardin React/Solid now supports{ ref }to resolve nearest scoped ancestor; without ref it remains global.Net effect: same user-facing capability (subtree keyboard isolation, scoped tab cycle, focus save/restore), but with less framework/component surface and less migration churn.