[PR #646] [CLOSED] Renderable-scoped keyboard focus #1464

Closed
opened 2026-03-14 09:38:11 +03:00 by kerem · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/anomalyco/opentui/pull/646
Author: @cevr
Created: 2/7/2026
Status: Closed

Base: mainHead: feat/focus-scope


📝 Commits (6)

  • 9a1ff63 feat(core): add renderable-scoped keyboard focus behavior
  • 01f374f feat(frameworks): support scope-aware keyboard hooks
  • 57f2f63 refactor(frameworks): simplify scope keyboard hooks and isolate react tests
  • a69082f fix(core): dispatch order for global preventDefault vs scope isolation
  • a2c4205 fix(core): isolate paste events in trap-focus scopes and simplify onParentAdded
  • ef80af6 refactor(core): remove default tab cycling from trapFocus, rename focus utilities

📊 Changes

12 files changed (+1758 additions, -118 deletions)

View changed files

📝 packages/core/src/Renderable.ts (+186 -38)
📝 packages/core/src/lib/KeyHandler.ts (+134 -34)
packages/core/src/lib/focus-traversal.test.ts (+157 -0)
packages/core/src/lib/focus-traversal.ts (+78 -0)
📝 packages/core/src/lib/index.ts (+1 -0)
📝 packages/core/src/renderer.ts (+7 -9)
packages/core/src/tests/renderable.scope-focus.test.ts (+650 -0)
📝 packages/core/src/types.ts (+2 -3)
📝 packages/react/src/hooks/use-keyboard.ts (+13 -6)
packages/react/tests/renderable.scope-focus.test.tsx (+66 -0)
📝 packages/solid/src/elements/hooks.ts (+17 -28)
packages/solid/tests/renderable.scope-focus.test.tsx (+447 -0)

📄 Description

Closes #638

Problem / Intent

Keyboard events need to be isolatable to a renderable subtree — modals, dialogs, and panels that trap focus within their boundaries, with automatic focus save/restore on scope teardown.

Approach

Scope behavior lives directly on Renderable via two new options:

  • trapFocus — isolates keyboard events to the subtree. Key, release, and paste events bubble up from the focused renderable and stop at the scope boundary via stopPropagation. Ctrl+C is the one exception — it always passes through.
  • autoFocus — focuses the first focusable descendant (or self) on mount.

No default keybindings are imposed. Focus cycling utilities (nextFocusable, prevFocusable) are exported for consumers to wire to whatever keys make sense for their app.

Event dispatch in InternalKeyHandler uses a dual-path strategy: trap-focus scopes dispatch to the renderable first (so the scope can stop propagation before globals see it), while regular focused renderables dispatch to globals first (so preventDefault works for things like Input shortcuts).

Framework hooks (useKeyboard in both React and Solid) accept an optional ref to discover the nearest keyboard scope via parent-walking, falling back to global when none exists.

Focus save/restore is handled per-scope: activating a scope snapshots the current focus, deactivating restores it if the target is still alive and focusable.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/anomalyco/opentui/pull/646 **Author:** [@cevr](https://github.com/cevr) **Created:** 2/7/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `feat/focus-scope` --- ### 📝 Commits (6) - [`9a1ff63`](https://github.com/anomalyco/opentui/commit/9a1ff63974f405845cf468e8759a460437fb730b) feat(core): add renderable-scoped keyboard focus behavior - [`01f374f`](https://github.com/anomalyco/opentui/commit/01f374f100b6e41b675f59f6da31d336d5378ef0) feat(frameworks): support scope-aware keyboard hooks - [`57f2f63`](https://github.com/anomalyco/opentui/commit/57f2f6382c652c41ffac114b31158bf596a9bc76) refactor(frameworks): simplify scope keyboard hooks and isolate react tests - [`a69082f`](https://github.com/anomalyco/opentui/commit/a69082f66100e8f984407bd99044c82c15bdc711) fix(core): dispatch order for global preventDefault vs scope isolation - [`a2c4205`](https://github.com/anomalyco/opentui/commit/a2c42056b382f69a876b04e8f83cc5a478af42c8) fix(core): isolate paste events in trap-focus scopes and simplify onParentAdded - [`ef80af6`](https://github.com/anomalyco/opentui/commit/ef80af6bd6f58ed9e8afb948218b524ee4276337) refactor(core): remove default tab cycling from trapFocus, rename focus utilities ### 📊 Changes **12 files changed** (+1758 additions, -118 deletions) <details> <summary>View changed files</summary> 📝 `packages/core/src/Renderable.ts` (+186 -38) 📝 `packages/core/src/lib/KeyHandler.ts` (+134 -34) ➕ `packages/core/src/lib/focus-traversal.test.ts` (+157 -0) ➕ `packages/core/src/lib/focus-traversal.ts` (+78 -0) 📝 `packages/core/src/lib/index.ts` (+1 -0) 📝 `packages/core/src/renderer.ts` (+7 -9) ➕ `packages/core/src/tests/renderable.scope-focus.test.ts` (+650 -0) 📝 `packages/core/src/types.ts` (+2 -3) 📝 `packages/react/src/hooks/use-keyboard.ts` (+13 -6) ➕ `packages/react/tests/renderable.scope-focus.test.tsx` (+66 -0) 📝 `packages/solid/src/elements/hooks.ts` (+17 -28) ➕ `packages/solid/tests/renderable.scope-focus.test.tsx` (+447 -0) </details> ### 📄 Description Closes #638 ## Problem / Intent Keyboard events need to be isolatable to a renderable subtree — modals, dialogs, and panels that trap focus within their boundaries, with automatic focus save/restore on scope teardown. ## Approach Scope behavior lives directly on `Renderable` via two new options: - **`trapFocus`** — isolates keyboard events to the subtree. Key, release, and paste events bubble up from the focused renderable and stop at the scope boundary via `stopPropagation`. Ctrl+C is the one exception — it always passes through. - **`autoFocus`** — focuses the first focusable descendant (or self) on mount. No default keybindings are imposed. Focus cycling utilities (`nextFocusable`, `prevFocusable`) are exported for consumers to wire to whatever keys make sense for their app. Event dispatch in `InternalKeyHandler` uses a dual-path strategy: trap-focus scopes dispatch to the renderable first (so the scope can stop propagation before globals see it), while regular focused renderables dispatch to globals first (so `preventDefault` works for things like Input shortcuts). Framework hooks (`useKeyboard` in both React and Solid) accept an optional `ref` to discover the nearest keyboard scope via parent-walking, falling back to global when none exists. Focus save/restore is handled per-scope: activating a scope snapshots the current focus, deactivating restores it if the target is still alive and focusable. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
kerem 2026-03-14 09:38:11 +03:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/opentui#1464
No description provided.