bug: Fix focus trap lock-up when removing focused nodes from JSX DOM (Closes #1355)#1364
bug: Fix focus trap lock-up when removing focused nodes from JSX DOM (Closes #1355)#1364knoxiboy wants to merge 10 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR adds automatic focus cleanup on component unmount by introducing the ChangesFocus Cleanup Utility for Unmount
Sequence DiagramsequenceDiagram
participant App as App Component
participant Focus as FocusContext.Provider
participant useFocus as useFocus Hook
participant autoBlur as autoBlurNode
participant Manager as FocusManager
App->>Focus: Provide focus context
Focus->>useFocus: Mount focused child
useFocus->>Manager: Call focus(childId)
App->>useFocus: Trigger unmount
useFocus->>autoBlur: Call cleanup on unmount
autoBlur->>Manager: Route to ancestor or blur
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~28 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/jsx/src/focus_cleanup.ts`:
- Around line 2-6: The autoBlurNode function uses the wrong property and method
signatures and an unsafe any: change the check from focusManager.activeNodeId to
focusManager.focused, call focusManager.blur() with no arguments, and stop using
plain any by importing and typing the first parameter as FocusContextValue from
focus-context.ts (or add the required inline comment explaining why any is
necessary if you cannot import that type). Ensure you reference the autoBlurNode
function, the FocusContextValue interface (focused: string | null and blur: ()
=> void), and update the implementation accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: c6d171fe-e991-4a2e-aadb-d40ee669d6d0
📒 Files selected for processing (1)
packages/jsx/src/focus_cleanup.ts
4ad4dc7 to
59e044c
Compare
Karanjot786
left a comment
There was a problem hiding this comment.
Thanks for the contribution. There are issues to fix before this merges.
Issues:
-
File is an orphan — not wired into the package public API
File: packages/jsx/src/focus_cleanup.ts
Problem: The new file is never imported or called from the JSX reconciler/unmount path, so the fix for #1355 is never actually invoked. The function exists in isolation.
Fix: Call `autoBlurNode` from the unmount code path in the JSX reconciler (wherever `unmountFiber` or equivalent is called) and export it from `packages/jsx/src/index.ts` if it is part of the public API. -
No tests
File: packages/jsx/src/focus_cleanup.ts
Problem: The fix for a focus trap lock-up should be verified with at least one test that mounts a focused node, unmounts it, and asserts blur was called.
Fix: Add a test file `focus_cleanup.test.ts`. -
CI is missing `build-and-test`
Problem: Only path-labels, star-check, and CodeRabbit passed. The mandatory build-and-test job never ran.
Fix: Rebase onto latest main to trigger a full CI run.
Checklist:
- Wire autoBlurNode into the unmount code path
- Export from package index if public
- Add focus_cleanup.test.ts
- build-and-test CI passes
- Tests pass (bun vitest run)
- PR title: type(scope): description
Karanjot786
left a comment
There was a problem hiding this comment.
Thanks for the contribution. There are issues to fix before this merges.
Issues:
-
New file not exported from package index
File: packages/jsx/src/focus_cleanup.ts
Problem:autoBlurNodeis defined but never re-exported frompackages/jsx/src/index.ts, making it dead code — unreachable to any consumer.
Fix: Addexport { autoBlurNode } from './focus_cleanup.js';topackages/jsx/src/index.ts. -
No integration with JSX unmount lifecycle
Problem: The issue (#1355) asks to "fix focus trap lock-up when removing focused nodes from JSX DOM". The PR only adds a standalone helper but never wires it into the JSX fiber's unmount path (e.g.,destroyFiberor the node removal hook). The bug described in the issue is therefore not fixed.
Fix: CallautoBlurNodefrom the relevant unmount hook/fiber cleanup path so it runs automatically when a focused node is removed, without requiring callers to invoke it manually. -
No tests
Problem: There are no tests verifying that the helper correctly blurs the focused node and is a no-op when the node is not focused.
Fix: Addpackages/jsx/src/focus_cleanup.test.tscovering both branches.
Checklist:
- Fix applied
- Tests pass (bun vitest run)
- PR title: type(scope): description
Karanjot786
left a comment
There was a problem hiding this comment.
Thanks for the contribution. There are issues to fix before this merges.
Issues:
-
New file not exported from package index
File: packages/jsx/src/focus_cleanup.ts
Problem:autoBlurNodeis never re-exported frompackages/jsx/src/index.ts, making it unreachable by consumers. An unexported file in a package src/ directory is effectively dead code.
Fix: Addexport { autoBlurNode } from './focus_cleanup.js';topackages/jsx/src/index.ts. -
Not integrated into the unmount lifecycle
File: packages/jsx/src/focus_cleanup.ts
Problem: The PR description says it fixes focus trap lock-up when removing focused nodes from the JSX DOM, butautoBlurNodeis never called from the fiber unmount path, the JSX reconciler, or any component lifecycle. The bug described in #1355 is not actually fixed.
Fix: Identify where the JSX reconciler tears down nodes (e.g. the fiber destroy / unmount phase) and callautoBlurNodethere with the node's focus ID. -
No tests provided
File: packages/jsx/src/focus_cleanup.ts
Problem: No test file accompanies the new utility.
Fix: Add a test verifying thatautoBlurNodecallsfocusManager.blur()when the focused node matches, and is a no-op when it does not.
Checklist:
- Fix applied
- Tests pass (bun vitest run)
- PR title: type(scope): description
Karanjot786
left a comment
There was a problem hiding this comment.
Thanks for the contribution. There are issues to fix before this merges.
Issues:
-
[Not exported from package public API]
File: packages/jsx/src/index.ts
Problem:focus_cleanup.tsis never exported from the@termuijs/jsxpackage barrel, soautoBlurNodeis unreachable by consumers.
Fix: Addexport { autoBlurNode } from './focus_cleanup.js';to the jsx index. -
[No integration with unmount lifecycle]
File: packages/jsx/src/focus_cleanup.ts
Problem:autoBlurNodeis a standalone helper that is never called during actual component unmount. The fix for #1355 requires hooking into the JSX reconciler's unmount path so that when a focused node is removed from the tree, blur is triggered automatically.
Fix: CallautoBlurNode(or equivalent logic) inside the reconciler's node-removal / unmount routine, not as an isolated export. -
[No tests]
File: packages/jsx/src/focus_cleanup.test.ts (missing)
Problem: No tests verify that blur is called when a focused node is unmounted, or that non-focused nodes are not blurred.
Fix: Add vitest tests covering at least: (a) focused node unmounted → blur called, (b) non-focused node unmounted → blur not called.
Checklist:
- Exported from
@termuijs/jsxbarrel - Integrated into the unmount lifecycle path
- Tests added and passing (
bun vitest run) - PR title: type(scope): description
|
Thanks for the contribution. There are issues to fix before this merges. Issues:
Checklist before re-requesting review:
|
Karanjot786
left a comment
There was a problem hiding this comment.
Thanks for the contribution. There are issues to fix before this merges.
Issues:
-
Implementation does not fulfill the linked issue's requirements / does not fix the lock-up
File: packages/jsx/src/focus_cleanup.ts, packages/jsx/src/hooks/useFocus.ts
Problem: The PR adds anautoBlurNodehelper but never calls it from anywhere. Issue #1355 requires: (1) monitoring the lifecycle of focused nodes in the focus context, (2) automatically invoking blur when a focused node unmounts, and (3) auto-routing focus to the nearest focusable ancestor or app root as fallback.useFocus(packages/jsx/src/hooks/useFocus.ts) has nouseEffectcleanup/unmount handler that callsautoBlurNode, so the original lock-up bug (focus manager pointing at a non-existent node ID after unmount) is not fixed by this PR.
Fix: InuseFocus, add auseEffectcleanup function (returned from the existing effect or a new one) that callsautoBlurNode(ctx, id)on unmount, so focus is released if this node was focused when removed. Additionally implement fallback focus routing to the nearest focusable ancestor (or app root) instead of leaving focus null, per the issue. Add a test simulating conditional unmounting of a focused input and asserting that subsequent keyboard input is not locked up. -
No tests included
File: packages/jsx/src/focus_cleanup.ts
Problem: A critical-severity bugfix PR with zero test coverage, including no regression test for the lock-up scenario described in the issue.
Fix: Add a test (e.g. in useFocus.test.ts or a new focus_cleanup.test.ts) that mounts a focused component, unmounts it, and verifies the focus manager'sfocusedstate is cleared/reassigned and that focus/keyboard handling continues to work afterward.
Checklist before re-requesting review:
- Fix 1 applied (autoBlurNode wired into useFocus unmount cleanup, fallback focus routing implemented)
- Fix 2 applied (regression test for lock-up added)
- Tests pass locally (run: bun vitest run)
- PR title follows format: type(scope): description
Karanjot786
left a comment
There was a problem hiding this comment.
Thanks for the contribution. There are issues to fix before this merges.
Issues:
-
New helper is never wired into the focus manager / unmount lifecycle
File: packages/jsx/src/focus_cleanup.ts, line 1-8
Problem:autoBlurNode()is added but not called from anywhere - it's not hooked into any component unmount lifecycle in @termuijs/jsx. The focus trap lock-up described in #1355 (keyboard input locks up when a focused node is unmounted) is not actually fixed.
Fix: HookautoBlurNode(or equivalent logic) into the JSX reconciler/unmount path so that when a focused node is removed from the tree,focusManager.blur()is invoked automatically and focus is re-routed to the nearest focusable ancestor or app root, per the issue's proposed solution. -
Missing regression test reproducing the lock-up
Problem: No test simulates conditionally unmounting a focused input (e.g. closing a modal or filtering a focused list item) and verifying that subsequent keyboard input is no longer locked up. This is required by #1355 and is critical to confirm the fix works against the real reconciler, not just the standalone helper.
Fix: Add a test in packages/jsx that mounts a focusable component, focuses it, unmounts it, and asserts the focus manager's focused state is cleared/re-routed and that subsequent key events are dispatched normally (no lock-up). -
No fallback/re-routing logic
File: packages/jsx/src/focus_cleanup.ts
Problem:autoBlurNodeonly callsblur(); it does not implement the "auto-route focus to nearest focusable ancestor or fallback to app root" behavior described in #1355.
Fix: Extend the cleanup logic to attempt focusing the nearest focusable ancestor before falling back to blur/app root.
Checklist before re-requesting review:
- Fix 1 applied
- Tests pass locally (run: bun vitest run)
- PR title follows format: type(scope): description
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/jsx/src/focus_cleanup.test.ts`:
- Around line 38-47: The tests in focus_cleanup.test.ts are using direct fiber
and context manipulation (runEffects, destroyFiber) instead of the real
rendering path, which bypasses actual unmount behavior. Replace the current test
implementations with integration-style tests that mount focused UI components
using a real Screen instance, render them via updateRect() and render() calls,
conditionally unmount the focused component, and then assert the observable
focus routing or blur behavior. This ensures tests validate the real
conditional-unmount behavior as described in the bug, following the requirement
that packages/**/*.test.{ts,tsx} tests must use real Screen and render cycles
rather than internal fiber/context manipulation.
- Line 65: Remove or properly justify all `as any` type assertions in the test
file per TypeScript guidelines. At line 65 in the assignment `mockWidget1.parent
= null as any`, remove the unnecessary `as any` assertion since the parent
property is already typed as null on line 63. At lines 70 and 78 where
`(globalThis as any)` is used to access non-standard properties, add inline
comments explaining why the type assertion is required (e.g., accessing
properties that don't exist on the standard globalThis type). At line 74 in the
`autoBlurNode(mockContextValue as any, ...)` call, remove the `as any` assertion
since mockContextValue is already properly typed, or alternatively add an inline
comment if the assertion is genuinely necessary for the test.
In `@packages/jsx/src/focus_cleanup.ts`:
- Around line 35-43: The fallback focus routing in the loop starting at the
instances Map iteration is non-deterministic because it picks the first
focusable widget based on Map insertion order rather than a stable, app-level
fallback target. Replace the logic that iterates through instances.keys() and
selects the first focusable widget (where w.focusable && w.id !== nodeId) with a
deterministic approach that routes focus to a consistent, root-level fallback
node or a designated primary focus target, ensuring predictable focus behavior
during unmount cleanup instead of relying on Map iteration order.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d206cc00-7a6d-4105-9572-5bd0282f25cb
📒 Files selected for processing (4)
packages/jsx/src/focus_cleanup.test.tspackages/jsx/src/focus_cleanup.tspackages/jsx/src/hooks/useFocus.tspackages/jsx/src/index.ts
…-style tests and fix non-deterministic fallback routing
|
@Karanjot786 I have addressed the review suggestions for the focus cleanup logic. The fallback routing is now deterministic (using alphabetical ID sorting), and the unit tests have been rewritten to use clean integration-style rendering and unmounting instead of manual fiber/context manipulation. Please review and merge! |
Karanjot786
left a comment
There was a problem hiding this comment.
Thanks for the contribution. There are issues to fix before this merges.
Issues:
-
as anycasts in production source file
File: packages/jsx/src/focus_cleanup.ts, lines 9 and 13
Problem:const instances = (globalThis as any).__termuijs_instancesandlet widget: any = nulluse unsafeanycasts in a public-facing production source file. This bypasses TypeScript's type safety and signals that the implementation is coupling to undocumented internals without a proper typed contract.
Fix: Declare a typed interface for the global (e.g.interface TermuiGlobal { __termuijs_instances?: Map<{ id: string; focusable?: boolean; parent: unknown }, unknown> }) and cast via(globalThis as unknown as TermuiGlobal), or import the widget type from the reconciler. Replacelet widget: anywith the proper typed variable. -
Private global access via
as anyin tests
File: packages/jsx/src/focus_cleanup.test.ts, multiple lines
Problem: Tests use(globalThis as any).__termuijs_instancesto read and overwrite reconciler-internal state. This is private field access throughas anyin tests — a blocking pattern in this repo. Tests must exercise observable behavior through the public API (mount, render, unmount cycles), not by reaching into undocumented globals.
Fix: Replace the third test (falls back to routing focus to fallback deterministically) with an integration-style test that renders two focusable components, focuses one, unmounts it, and asserts the other is now focused using public hook return values or render output. Do not manipulate__termuijs_instancesdirectly. -
PR will regress the exhaustive-deps fix from PR #1430
File: packages/jsx/src/hooks/useFocus.ts
Problem: This PR was branched from the originaluseFocuscommit (before PR #1430 landed). The diff showsuseEffectwith[]deps and// eslint-disable-next-line react-hooks/exhaustive-deps. Main now has[autoFocus, focused, focus, id]as the dependency array (merged via #1430). When this PR merges, the diff will reapply the old[]deps, silently regressing the fix.
Fix: Rebase this branch onto current main (git rebase main), resolve the conflict inuseFocus.tsso that the cleanup return is added inside the existing effect that has the correct[autoFocus, focused, focus, id]deps array. Theeslint-disablecomment and the[]deps must not be present in the merged result.
Checklist before re-requesting review:
-
as anyremoved from focus_cleanup.ts (use typed interface for globalThis access) - Test for fallback routing rewritten using public API only — no direct
__termuijs_instancesmanipulation - Branch rebased onto current main; useFocus.ts conflict resolved keeping exhaustive-deps fix
- Tests pass locally (run: bun vitest run)
- PR title follows format: type(scope): description
# Conflicts: # packages/jsx/src/hooks/useFocus.ts
Karanjot786
left a comment
There was a problem hiding this comment.
Thanks for the contribution. There are issues to fix before this merges.
Issues:
-
as anycasts on public exported function to access internal globals
File: packages/jsx/src/focus_cleanup.ts, lines 8-16, 32-48
Problem:autoBlurNodeis a public API (exported from index.ts) and its entire implementation relies on(globalThis as any).__termuijs_instances— a private internal global — pluslet widget: any,w.id as any,w.focusable as any,current.parent as any, etc. Usingas anyto reach into undocumented internal state makes the function fragile, untestable via the public API, and tightly couples the cleanup logic to an implementation detail that can change at any time. If__termuijs_instancesis a stable internal registry it should be typed and exposed through a proper internal accessor (e.g.getInstanceRegistry(): Map<Widget, Instance>in reconciler.ts), andautoBlurNodeshould accept a typed widget tree / registry parameter rather than reading an untyped global.
Fix: Either (a) pass the focusable widget list as an explicit typed parameter toautoBlurNodeso callers provide the data, or (b) create a typed internal accessor in reconciler.ts and import it — removing allas anycasts. The public signature ofautoBlurNodeshould not require knowledge of an untyped internal global. -
Tests access the same untyped internal global directly
File: packages/jsx/src/focus_cleanup.test.ts, lines 16, 29, 31, 64, 66, 103, 105
Problem:(globalThis as any).__termuijs_instancesin tests is equivalent to the banned(widget as any)._fieldpattern — it bypasses the public API to reach internal state and makes tests dependent on implementation details rather than observable behaviour. The test for "clears focus on unmount" also reconstructs arootInstancefrom the internal map instead of triggering the unmount through the public JSX render API.
Fix: Drive unmount behaviour through the publicreconcile/reRenderComponentsurface with a component that conditionally renders, then assert on the mockblur/focuscalls. Remove all direct reads/writes of__termuijs_instancesfrom test code.
Checklist before re-requesting review:
- Remove all
as anycasts from focus_cleanup.ts (replace with typed interfaces/accessors) - Remove
(globalThis as any).__termuijs_instancesreads/writes from test file - Tests pass locally (run: bun vitest run)
- PR title follows format: type(scope): description
|
Thanks for the contribution. There are issues to fix before this merges. Issues:
Checklist before re-requesting review:
|
|
Two blocking issues:
Checklist:
|
…any casts, add safe emitA11y fallback
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
packages/jsx/src/focus_cleanup.ts (1)
12-21: 💤 Low valueLinear search through instance map is O(n) but may be acceptable.
The code iterates through all widgets in the instance map to find one matching
nodeId. Since the map is keyed byWidget(not byid), this linear search is unavoidable without maintaining a separate id→widget index. For typical widget counts in a TUI application, this overhead is likely acceptable, but consider adding a comment explaining the tradeoff if this is intentional.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/jsx/src/focus_cleanup.ts` around lines 12 - 21, The linear search through the instance map by iterating through instances.keys() to find a widget matching nodeId lacks context about the design decision. Add a comment above the for loop that explains the tradeoff: the instance map is keyed by Widget objects (not by id), which makes a linear search necessary, and note that while this is O(n), it is generally acceptable for typical widget counts in TUI applications. This comment will clarify the intentional design decision for future maintainers.packages/jsx/src/hooks.ts (1)
345-347: ⚡ Quick winEmpty block following conflict detection comment.
The comment indicates conflicting keybindings should be handled, but the block is empty. Consider either removing the empty block or adding a comment explaining why no action is taken.
♻️ Suggested cleanup
const key = `${b.key}|${b.ctrl ?? false}|${b.alt ?? false}|${b.shift ?? false}`; if (seen.has(key)) { - // Conflicting keybinding — silently ignore in dev mode + // Conflicting keybinding detected but intentionally ignored } seen.set(key, b);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/jsx/src/hooks.ts` around lines 345 - 347, The if block checking for `seen.has(key)` is empty and provides no implementation despite the comment indicating conflicting keybindings should be silently ignored. Either remove the entire empty if block containing the conflict detection check, or add a clarifying comment inside the empty block (such as a JSDoc or inline comment) that explicitly explains why no action is being taken for conflicting keybindings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/jsx/src/focus_cleanup.ts`:
- Line 13: Simplify the type annotation for the widget variable declaration by
removing the redundant type intersection. Since the Widget base class already
declares the id, focusable, and parent properties, change the widget variable
type from the full intersection type to simply Widget | null, eliminating the
unnecessary object type literal that duplicates properties already present in
the Widget class definition.
In `@packages/jsx/src/reconciler.test.ts`:
- Around line 38-40: The getInstanceMap() function in the test file is
recursively calling itself with no base case, causing a stack overflow. Remove
the broken local getInstanceMap() function definition (lines 38-40) and instead
import the real getInstanceMap() from the reconciler module which exports it at
line 49 of reconciler.ts. Update the import statement to include getInstanceMap
from the reconciler module so that all test calls to getInstanceMap() (at lines
52, 86, 117, 127, 136, 145, 159, 166) will use the correct implementation.
In `@packages/testing/src/render.ts`:
- Around line 312-314: The file inconsistently accesses the instance map by
using both direct reads of globalThis.__termuijs_instances and the
getInstanceMap() accessor function. Replace all direct accesses to (globalThis
as any).__termuijs_instances at lines 312, 426, 516, and 576 with calls to
getInstanceMap() to match the pattern already established in the fireMouse
function, ensuring consistent and centralized instance map access throughout the
render.ts file.
---
Nitpick comments:
In `@packages/jsx/src/focus_cleanup.ts`:
- Around line 12-21: The linear search through the instance map by iterating
through instances.keys() to find a widget matching nodeId lacks context about
the design decision. Add a comment above the for loop that explains the
tradeoff: the instance map is keyed by Widget objects (not by id), which makes a
linear search necessary, and note that while this is O(n), it is generally
acceptable for typical widget counts in TUI applications. This comment will
clarify the intentional design decision for future maintainers.
In `@packages/jsx/src/hooks.ts`:
- Around line 345-347: The if block checking for `seen.has(key)` is empty and
provides no implementation despite the comment indicating conflicting
keybindings should be silently ignored. Either remove the entire empty if block
containing the conflict detection check, or add a clarifying comment inside the
empty block (such as a JSDoc or inline comment) that explicitly explains why no
action is being taken for conflicting keybindings.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: f0568c97-a2de-4de8-a43e-6b6e15c20b67
📒 Files selected for processing (10)
packages/jsx/src/focus_cleanup.test.tspackages/jsx/src/focus_cleanup.tspackages/jsx/src/hooks.tspackages/jsx/src/hooks/useFocus.tspackages/jsx/src/index.tspackages/jsx/src/reconciler.test.tspackages/jsx/src/reconciler.tspackages/jsx/src/render.tspackages/testing/src/render.tspackages/widgets/src/base/Widget.ts
💤 Files with no reviewable changes (1)
- packages/widgets/src/base/Widget.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/jsx/src/index.ts
- packages/jsx/src/hooks/useFocus.ts
- packages/jsx/src/focus_cleanup.test.ts
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 3
🧹 Nitpick comments (2)
packages/jsx/src/focus_cleanup.ts (1)
12-21: 💤 Low valueLinear search through instance map is O(n) but may be acceptable.
The code iterates through all widgets in the instance map to find one matching
nodeId. Since the map is keyed byWidget(not byid), this linear search is unavoidable without maintaining a separate id→widget index. For typical widget counts in a TUI application, this overhead is likely acceptable, but consider adding a comment explaining the tradeoff if this is intentional.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/jsx/src/focus_cleanup.ts` around lines 12 - 21, The linear search through the instance map by iterating through instances.keys() to find a widget matching nodeId lacks context about the design decision. Add a comment above the for loop that explains the tradeoff: the instance map is keyed by Widget objects (not by id), which makes a linear search necessary, and note that while this is O(n), it is generally acceptable for typical widget counts in TUI applications. This comment will clarify the intentional design decision for future maintainers.packages/jsx/src/hooks.ts (1)
345-347: ⚡ Quick winEmpty block following conflict detection comment.
The comment indicates conflicting keybindings should be handled, but the block is empty. Consider either removing the empty block or adding a comment explaining why no action is taken.
♻️ Suggested cleanup
const key = `${b.key}|${b.ctrl ?? false}|${b.alt ?? false}|${b.shift ?? false}`; if (seen.has(key)) { - // Conflicting keybinding — silently ignore in dev mode + // Conflicting keybinding detected but intentionally ignored } seen.set(key, b);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/jsx/src/hooks.ts` around lines 345 - 347, The if block checking for `seen.has(key)` is empty and provides no implementation despite the comment indicating conflicting keybindings should be silently ignored. Either remove the entire empty if block containing the conflict detection check, or add a clarifying comment inside the empty block (such as a JSDoc or inline comment) that explicitly explains why no action is being taken for conflicting keybindings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/jsx/src/focus_cleanup.ts`:
- Line 13: Simplify the type annotation for the widget variable declaration by
removing the redundant type intersection. Since the Widget base class already
declares the id, focusable, and parent properties, change the widget variable
type from the full intersection type to simply Widget | null, eliminating the
unnecessary object type literal that duplicates properties already present in
the Widget class definition.
In `@packages/jsx/src/reconciler.test.ts`:
- Around line 38-40: The getInstanceMap() function in the test file is
recursively calling itself with no base case, causing a stack overflow. Remove
the broken local getInstanceMap() function definition (lines 38-40) and instead
import the real getInstanceMap() from the reconciler module which exports it at
line 49 of reconciler.ts. Update the import statement to include getInstanceMap
from the reconciler module so that all test calls to getInstanceMap() (at lines
52, 86, 117, 127, 136, 145, 159, 166) will use the correct implementation.
In `@packages/testing/src/render.ts`:
- Around line 312-314: The file inconsistently accesses the instance map by
using both direct reads of globalThis.__termuijs_instances and the
getInstanceMap() accessor function. Replace all direct accesses to (globalThis
as any).__termuijs_instances at lines 312, 426, 516, and 576 with calls to
getInstanceMap() to match the pattern already established in the fireMouse
function, ensuring consistent and centralized instance map access throughout the
render.ts file.
---
Nitpick comments:
In `@packages/jsx/src/focus_cleanup.ts`:
- Around line 12-21: The linear search through the instance map by iterating
through instances.keys() to find a widget matching nodeId lacks context about
the design decision. Add a comment above the for loop that explains the
tradeoff: the instance map is keyed by Widget objects (not by id), which makes a
linear search necessary, and note that while this is O(n), it is generally
acceptable for typical widget counts in TUI applications. This comment will
clarify the intentional design decision for future maintainers.
In `@packages/jsx/src/hooks.ts`:
- Around line 345-347: The if block checking for `seen.has(key)` is empty and
provides no implementation despite the comment indicating conflicting
keybindings should be silently ignored. Either remove the entire empty if block
containing the conflict detection check, or add a clarifying comment inside the
empty block (such as a JSDoc or inline comment) that explicitly explains why no
action is being taken for conflicting keybindings.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: f0568c97-a2de-4de8-a43e-6b6e15c20b67
📒 Files selected for processing (10)
packages/jsx/src/focus_cleanup.test.tspackages/jsx/src/focus_cleanup.tspackages/jsx/src/hooks.tspackages/jsx/src/hooks/useFocus.tspackages/jsx/src/index.tspackages/jsx/src/reconciler.test.tspackages/jsx/src/reconciler.tspackages/jsx/src/render.tspackages/testing/src/render.tspackages/widgets/src/base/Widget.ts
💤 Files with no reviewable changes (1)
- packages/widgets/src/base/Widget.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/jsx/src/index.ts
- packages/jsx/src/hooks/useFocus.ts
- packages/jsx/src/focus_cleanup.test.ts
🛑 Comments failed to post (3)
packages/jsx/src/focus_cleanup.ts (1)
13-13:
⚠️ Potential issue | 🟡 Minor🧩 Analysis chain
🏁 Script executed:
#!/bin/bash # Check Widget base class type signature for id, focusable, parent properties ast-grep --pattern 'class Widget { $$$ }' # Also check for property declarations rg -n "^\s+(id|focusable|parent):" packages/widgets/src/base/Widget.ts -A1Repository: Karanjot786/TermUI
Length of output: 103
🏁 Script executed:
# Get more context on Widget class definition - check full properties rg -n "(id|focusable|parent):" packages/widgets/src/base/Widget.ts -B2 -A2 # Also check the focus_cleanup.ts file context cat -n packages/jsx/src/focus_cleanup.ts | head -30Repository: Karanjot786/TermUI
Length of output: 1562
🏁 Script executed:
# Search for focusable property in Widget class definition rg -n "focusable" packages/widgets/src/base/Widget.ts -B2 -A2 # Also check the full Widget class structure cat -n packages/widgets/src/base/Widget.ts | head -100Repository: Karanjot786/TermUI
Length of output: 4084
Remove the redundant type intersection.
The
Widgetbase class already declares all three properties:id: string(line 79),focusable = false(line 100), andparent: Widget | null(line 88). The explicit intersection type adds no value. Change line 13 to:let widget: Widget | null = null;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/jsx/src/focus_cleanup.ts` at line 13, Simplify the type annotation for the widget variable declaration by removing the redundant type intersection. Since the Widget base class already declares the id, focusable, and parent properties, change the widget variable type from the full intersection type to simply Widget | null, eliminating the unnecessary object type literal that duplicates properties already present in the Widget class definition.packages/jsx/src/reconciler.test.ts (1)
38-40:
⚠️ Potential issue | 🔴 Critical🧩 Analysis chain
🏁 Script executed:
#!/bin/bash # Check if getInstanceMap is imported in the test file rg -n "import.*getInstanceMap.*from.*reconciler" packages/jsx/src/reconciler.test.ts # Verify the self-referential call exists rg -n -A2 "^function getInstanceMap\(\)" packages/jsx/src/reconciler.test.tsRepository: Karanjot786/TermUI
Length of output: 145
🏁 Script executed:
# Read the test file around the problematic lines sed -n '30,50p' packages/jsx/src/reconciler.test.ts # Check if reconciler.ts exports getInstanceMap rg -n "export.*getInstanceMap" packages/jsx/src/reconciler.ts # Look for where getInstanceMap is called/used in the test file rg -n "getInstanceMap()" packages/jsx/src/reconciler.test.tsRepository: Karanjot786/TermUI
Length of output: 1123
Infinite recursion in test helper causes stack overflow.
The
getInstanceMap()test helper (lines 38–40) calls itself recursively with no base case. This breaks thebeforeEachhook (line 44) and all tests that callgetInstanceMap()(lines 52, 86, 117, 127, 136, 145, 159, 166), causing immediate stack overflow at runtime.The reconciler exports the real
getInstanceMap()at line 49 ofreconciler.ts. Import and use it instead of the broken local function.🔧 Proposed fix
+import { getInstanceMap as getInstanceMapFromReconciler } from './reconciler.js'; + function getInstanceMap(): Map<any, any> { - return getInstanceMap(); + return getInstanceMapFromReconciler(); }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.import { getInstanceMap as getInstanceMapFromReconciler } from './reconciler.js'; function getInstanceMap(): Map<any, any> { return getInstanceMapFromReconciler(); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/jsx/src/reconciler.test.ts` around lines 38 - 40, The getInstanceMap() function in the test file is recursively calling itself with no base case, causing a stack overflow. Remove the broken local getInstanceMap() function definition (lines 38-40) and instead import the real getInstanceMap() from the reconciler module which exports it at line 49 of reconciler.ts. Update the import statement to include getInstanceMap from the reconciler module so that all test calls to getInstanceMap() (at lines 52, 86, 117, 127, 136, 145, 159, 166) will use the correct implementation.packages/testing/src/render.ts (1)
312-314:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winIncomplete refactoring: mixed use of
globalThis.__termuijs_instancesandgetInstanceMap().This file inconsistently accesses the instance map. Line 482 (
fireMouse) uses the newgetInstanceMap()accessor, while lines 312, 426, 516, and 576 still readglobalThis.__termuijs_instancesdirectly. According to the review stack context, this layer should refactor all direct reads to usegetInstanceMap().♻️ Refactor to use getInstanceMap() consistently
// Set up re-render callback — use fiber-preserving reRenderComponent setRequestRender(() => { - const instances: Map<Widget, any> = (globalThis as any) // as any: set by `@termuijs/jsx` reconciler at runtime; not in globalThis type - .__termuijs_instances; + const instances: Map<Widget, any> = getInstanceMap(); const rootInstance = instances?.get(rootWidget);Apply the same pattern at lines 426–427, 516–517, and 576–577.
Also applies to: 426-428, 516-518, 576-578
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/testing/src/render.ts` around lines 312 - 314, The file inconsistently accesses the instance map by using both direct reads of globalThis.__termuijs_instances and the getInstanceMap() accessor function. Replace all direct accesses to (globalThis as any).__termuijs_instances at lines 312, 426, 516, and 576 with calls to getInstanceMap() to match the pattern already established in the fireMouse function, ensuring consistent and centralized instance map access throughout the render.ts file.
Done |
What problem does this solve?
This Pull Request Closes #1355. It implements core layout/rendering capabilities inside TermUI packages.
Proposed solution
Created focus_cleanup.ts executing blur calls when focused inputs are conditionally unmounted.
Which package would this belong in?
GSSoC contributor?
Summary by CodeRabbit
autoBlurNodehelper, exposed in the public JSX API.getInstanceMap) for integrations.