Skip to content

bug: Fix focus trap lock-up when removing focused nodes from JSX DOM (Closes #1355)#1364

Open
knoxiboy wants to merge 10 commits into
Karanjot786:mainfrom
knoxiboy:feat/focus-lockup-1355
Open

bug: Fix focus trap lock-up when removing focused nodes from JSX DOM (Closes #1355)#1364
knoxiboy wants to merge 10 commits into
Karanjot786:mainfrom
knoxiboy:feat/focus-lockup-1355

Conversation

@knoxiboy

@knoxiboy knoxiboy commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

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?

  • packages/jsx/src/focus_cleanup.ts

GSSoC contributor?

  • Yes. You contribute under GSSoC 2026 and want to work on this.

Summary by CodeRabbit

  • Bug Fixes
    • Improved focus cleanup on unmount: when the removed node was focused, focus now moves to the nearest available focusable target; otherwise it uses a deterministic fallback (or clears focus if none exists).
    • Hardened accessibility event emission so it safely no-ops when accessibility callbacks aren’t functions.
  • New Features
    • Added an autoBlurNode helper, exposed in the public JSX API.
    • Updated focus hooks to automatically perform cleanup on unmount.
    • Added a public instance-registry accessor (getInstanceMap) for integrations.
  • Tests
    • Added integration coverage for unmount behavior and fallback focus redirection.

@knoxiboy knoxiboy requested a review from Karanjot786 as a code owner June 11, 2026 10:40
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds automatic focus cleanup on component unmount by introducing the autoBlurNode helper, which detects when a focused node is being removed from the DOM and routes focus to the nearest focusable ancestor or fallback focusable widget. The utility is integrated into the useFocus hook and exposed via the JSX package public API. Infrastructure is refactored to use a typed getInstanceMap() accessor instead of direct global access, and Widget accessibility emissions are guarded. Comprehensive test coverage validates unmount behavior and focus routing logic.

Changes

Focus Cleanup Utility for Unmount

Layer / File(s) Summary
Instance map accessor and refactoring across modules
packages/jsx/src/reconciler.ts, packages/jsx/src/hooks.ts, packages/jsx/src/render.ts, packages/jsx/src/reconciler.test.ts, packages/testing/src/render.ts
Creates exported getInstanceMap() accessor in reconciler and refactors all modules to call it instead of directly reading globalThis.__termuijs_instances. Minor casting update for grid rows property access. Provides typed access to the widget-to-instance registry across reconciliation, rendering, hooks lifecycle, and test utilities.
autoBlurNode implementation and API export
packages/jsx/src/focus_cleanup.ts, packages/jsx/src/index.ts
New autoBlurNode(focusManager, nodeId) function implements focus cleanup on unmount by locating the widget via instance map, walking its parent chain for focusable ancestors, selecting deterministic fallback focusable widgets by sorted ID, and routing focus or blurring. Exported in JSX package public API.
useFocus hook unmount cleanup
packages/jsx/src/hooks/useFocus.ts
useFocus imports autoBlurNode and extends its useEffect to return a cleanup function that calls autoBlurNode(ctx, id) during unmount, preserving existing conditional auto-focus behavior on mount.
Comprehensive test coverage for autoBlurNode and unmount behavior
packages/jsx/src/focus_cleanup.test.ts
Vitest suite validates that unmount clears focus when the unmounted node is currently focused, preserves focus when a different node is focused, and autoBlurNode correctly routes focus to sibling or fallback focusable widgets. Tests manage mocked FocusContext and global instance map across lifecycle.
Widget accessibility guards and formatting
packages/widgets/src/base/Widget.ts
Wraps emitA11y start and end phase calls in Widget.render() with runtime function-type guards, falling back to no-op when unavailable. Reformats _applyRect() early-return checks and config-selection region for consistency.

Sequence Diagram

sequenceDiagram
  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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Possibly related PRs

  • Karanjot786/TermUI#1577: Both PRs modify packages/widgets/src/base/Widget.ts accessibility emissions around emitA11y "start"/"end" phase calls; this PR guards the calls while the related PR may introduce the lifecycle emissions.
  • Karanjot786/TermUI#1430: Both PRs modify useFocus hook in packages/jsx/src/hooks/useFocus.ts, with this PR adding unmount cleanup logic via autoBlurNode while the other refactors effect dependency handling.
  • Karanjot786/TermUI#1469: Both PRs modify the JSX reconciler teardown path in packages/jsx/src/hooks.ts around destroyFiber instance/fiber bookkeeping, with this PR refactoring to use getInstanceMap() accessor and the other addressing memory-leak fixes in the same logic.

Suggested labels

type:bug, type:feature, level:intermediate, gssoc:approved

Suggested reviewers

  • Karanjot786

Poem

When nodes unmount and focus fades away,
A blur-time helper comes to save the day.
Walk the parent chain, find safe harbor's gleam,
Focus flows gently—no more lockup dream! 🐰✨

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete and deviates significantly from the required template, missing critical sections like Package specification, Type of Change checkboxes, Checklist items, and proper formatting. Complete the PR description by filling all required sections: specify packages (@termuijs/jsx), check the Bug fix type, verify all checklist items, and confirm GSSoC 2026 participation with profile link.
Docstring Coverage ⚠️ Warning Docstring coverage is 53.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive While most changes directly support focus cleanup, the Widget.ts emitA11y guarding and reconciler.test.ts self-referential change are not clearly justified by #1355 requirements and warrant clarification. Clarify whether emitA11y guarding and the reconciler.test.ts getInstanceMap change are necessary for the focus cleanup feature or if they should be split into separate PRs.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'bug: Fix focus trap lock-up when removing focused nodes from JSX DOM' clearly and specifically describes the primary fix addressing issue #1355, follows the required format, and accurately reflects the changeset.
Linked Issues check ✅ Passed The PR implementation addresses all four core objectives from #1355: lifecycle monitoring via useFocus hook, automatic blur invocation in unmount cleanup, fallback focus routing with alphabetical sorting, and regression test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the area:jsx @termuijs/jsx label Jun 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between f8169a4 and 4ad4dc7.

📒 Files selected for processing (1)
  • packages/jsx/src/focus_cleanup.ts

Comment thread packages/jsx/src/focus_cleanup.ts Outdated
@knoxiboy knoxiboy force-pushed the feat/focus-lockup-1355 branch from 4ad4dc7 to 59e044c Compare June 11, 2026 11:05

@Karanjot786 Karanjot786 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution. There are issues to fix before this merges.

Issues:

  1. 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.

  2. 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`.

  3. 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 Karanjot786 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution. There are issues to fix before this merges.

Issues:

  1. New file not exported from package index
    File: packages/jsx/src/focus_cleanup.ts
    Problem: autoBlurNode is defined but never re-exported from packages/jsx/src/index.ts, making it dead code — unreachable to any consumer.
    Fix: Add export { autoBlurNode } from './focus_cleanup.js'; to packages/jsx/src/index.ts.

  2. 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., destroyFiber or the node removal hook). The bug described in the issue is therefore not fixed.
    Fix: Call autoBlurNode from the relevant unmount hook/fiber cleanup path so it runs automatically when a focused node is removed, without requiring callers to invoke it manually.

  3. 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: Add packages/jsx/src/focus_cleanup.test.ts covering both branches.

Checklist:

  • Fix applied
  • Tests pass (bun vitest run)
  • PR title: type(scope): description

@Karanjot786 Karanjot786 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution. There are issues to fix before this merges.

Issues:

  1. New file not exported from package index
    File: packages/jsx/src/focus_cleanup.ts
    Problem: autoBlurNode is never re-exported from packages/jsx/src/index.ts, making it unreachable by consumers. An unexported file in a package src/ directory is effectively dead code.
    Fix: Add export { autoBlurNode } from './focus_cleanup.js'; to packages/jsx/src/index.ts.

  2. 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, but autoBlurNode is 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 call autoBlurNode there with the node's focus ID.

  3. No tests provided
    File: packages/jsx/src/focus_cleanup.ts
    Problem: No test file accompanies the new utility.
    Fix: Add a test verifying that autoBlurNode calls focusManager.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 Karanjot786 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution. There are issues to fix before this merges.

Issues:

  1. [Not exported from package public API]
    File: packages/jsx/src/index.ts
    Problem: focus_cleanup.ts is never exported from the @termuijs/jsx package barrel, so autoBlurNode is unreachable by consumers.
    Fix: Add export { autoBlurNode } from './focus_cleanup.js'; to the jsx index.

  2. [No integration with unmount lifecycle]
    File: packages/jsx/src/focus_cleanup.ts
    Problem: autoBlurNode is 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: Call autoBlurNode (or equivalent logic) inside the reconciler's node-removal / unmount routine, not as an isolated export.

  3. [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/jsx barrel
  • Integrated into the unmount lifecycle path
  • Tests added and passing (bun vitest run)
  • PR title: type(scope): description

@Karanjot786

Copy link
Copy Markdown
Owner

Thanks for the contribution. There are issues to fix before this merges.

Issues:

  1. Implementation is a stub — no integration, no tests
    File: packages/jsx/src/focus_cleanup.ts
    Problem: autoBlurNode is an 8-line helper not exported from packages/jsx/src/index.ts and not called anywhere in the JSX reconciler or focus context. The PR title claims this fixes a focus trap lock-up when removing focused nodes from the JSX DOM, but the relevant unmount path in the reconciler is not modified.
    Fix: Find the unmount / node-removal path in the JSX reconciler (likely in packages/jsx/src/reconciler.ts or similar), call autoBlurNode there when a focused node is being removed, export it from the package index, and add a test that mounts a focused node, unmounts it, and verifies that focus is cleared.

Checklist before re-requesting review:

  • Wire autoBlurNode into the JSX unmount path
  • Export from packages/jsx/src/index.ts
  • Add regression test for focus cleanup on node removal
  • Tests pass locally: bun vitest run
  • PR title follows format: type(scope): description

@Karanjot786 Karanjot786 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution. There are issues to fix before this merges.

Issues:

  1. 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 an autoBlurNode helper 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 no useEffect cleanup/unmount handler that calls autoBlurNode, so the original lock-up bug (focus manager pointing at a non-existent node ID after unmount) is not fixed by this PR.
    Fix: In useFocus, add a useEffect cleanup function (returned from the existing effect or a new one) that calls autoBlurNode(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.

  2. 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's focused state 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 Karanjot786 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution. There are issues to fix before this merges.

Issues:

  1. 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: Hook autoBlurNode (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.

  2. 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).

  3. No fallback/re-routing logic
    File: packages/jsx/src/focus_cleanup.ts
    Problem: autoBlurNode only calls blur(); 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

@github-actions github-actions Bot added the type:testing +10 pts. Tests. label Jun 14, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4ad4dc7 and ee02924.

📒 Files selected for processing (4)
  • packages/jsx/src/focus_cleanup.test.ts
  • packages/jsx/src/focus_cleanup.ts
  • packages/jsx/src/hooks/useFocus.ts
  • packages/jsx/src/index.ts

Comment thread packages/jsx/src/focus_cleanup.test.ts
Comment thread packages/jsx/src/focus_cleanup.test.ts Outdated
Comment thread packages/jsx/src/focus_cleanup.ts Outdated
…-style tests and fix non-deterministic fallback routing
@knoxiboy

Copy link
Copy Markdown
Contributor Author

@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 Karanjot786 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution. There are issues to fix before this merges.

Issues:

  1. as any casts in production source file
    File: packages/jsx/src/focus_cleanup.ts, lines 9 and 13
    Problem: const instances = (globalThis as any).__termuijs_instances and let widget: any = null use unsafe any casts 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. Replace let widget: any with the proper typed variable.

  2. Private global access via as any in tests
    File: packages/jsx/src/focus_cleanup.test.ts, multiple lines
    Problem: Tests use (globalThis as any).__termuijs_instances to read and overwrite reconciler-internal state. This is private field access through as any in 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_instances directly.

  3. PR will regress the exhaustive-deps fix from PR #1430
    File: packages/jsx/src/hooks/useFocus.ts
    Problem: This PR was branched from the original useFocus commit (before PR #1430 landed). The diff shows useEffect with [] 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 in useFocus.ts so that the cleanup return is added inside the existing effect that has the correct [autoFocus, focused, focus, id] deps array. The eslint-disable comment and the [] deps must not be present in the merged result.

Checklist before re-requesting review:

  • as any removed from focus_cleanup.ts (use typed interface for globalThis access)
  • Test for fallback routing rewritten using public API only — no direct __termuijs_instances manipulation
  • 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 Karanjot786 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution. There are issues to fix before this merges.

Issues:

  1. as any casts on public exported function to access internal globals
    File: packages/jsx/src/focus_cleanup.ts, lines 8-16, 32-48
    Problem: autoBlurNode is a public API (exported from index.ts) and its entire implementation relies on (globalThis as any).__termuijs_instances — a private internal global — plus let widget: any, w.id as any, w.focusable as any, current.parent as any, etc. Using as any to 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_instances is a stable internal registry it should be typed and exposed through a proper internal accessor (e.g. getInstanceRegistry(): Map<Widget, Instance> in reconciler.ts), and autoBlurNode should 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 to autoBlurNode so callers provide the data, or (b) create a typed internal accessor in reconciler.ts and import it — removing all as any casts. The public signature of autoBlurNode should not require knowledge of an untyped internal global.

  2. 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_instances in tests is equivalent to the banned (widget as any)._field pattern — 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 a rootInstance from the internal map instead of triggering the unmount through the public JSX render API.
    Fix: Drive unmount behaviour through the public reconcile/reRenderComponent surface with a component that conditionally renders, then assert on the mock blur/focus calls. Remove all direct reads/writes of __termuijs_instances from test code.

Checklist before re-requesting review:

  • Remove all as any casts from focus_cleanup.ts (replace with typed interfaces/accessors)
  • Remove (globalThis as any).__termuijs_instances reads/writes from test file
  • Tests pass locally (run: bun vitest run)
  • PR title follows format: type(scope): description

@Karanjot786

Copy link
Copy Markdown
Owner

Thanks for the contribution. There are issues to fix before this merges.

Issues:

  1. focus_cleanup.ts uses (globalThis as any).__termuijs_instances to access the internal widget registry in a publicly exported function. The global should be typed (e.g. declare the map type) rather than cast with as any.
    File: packages/jsx/src/focus_cleanup.ts
    Fix: Declare a typed interface for the widget map entries (e.g. interface FocusableWidget { id: string; focusable: boolean; parent: FocusableWidget | null }) and export a typed accessor (e.g. getInstanceMap(): Map<FocusableWidget, unknown>) from the reconciler rather than casting globalThis as any.

  2. let widget: any = null in focus_cleanup.ts (non-test production file) on a variable whose fields .id, .focusable, and .parent are accessed without typing — equivalent to (widget as any)._field access on internal widget internals from the global map.
    File: packages/jsx/src/focus_cleanup.ts
    Fix: Type widget as FocusableWidget | null using the interface described above, eliminating the implicit any and making the field accesses type-safe.

Checklist before re-requesting review:

  • All blocking issues fixed
  • Tests pass locally: bun vitest run
  • PR title follows: type(scope): description

@Karanjot786

Copy link
Copy Markdown
Owner

Two blocking issues:

  1. (globalThis as any).__termuijs_instances in production code (packages/jsx/src/focus_cleanup.ts)
    Problem: Accessing internal registry via globalThis as any breaks encapsulation and is fragile.
    Fix: Export a proper getRegisteredWidgets() function from the JSX reconciler and import it.

  2. let widget: any = null in production code
    Problem: Bypasses TypeScript safety on a variable used for .id, .focusable, .parent.
    Fix: Type as let widget: Widget | null = null

Checklist:

  • Replace globalThis hack with proper exported API
  • Type widget as Widget | null
  • Tests pass

@github-actions github-actions Bot added the area:widgets @termuijs/widgets label Jun 19, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
packages/jsx/src/focus_cleanup.ts (1)

12-21: 💤 Low value

Linear 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 by Widget (not by id), 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 win

Empty 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0d53675 and c0e1ba4.

📒 Files selected for processing (10)
  • packages/jsx/src/focus_cleanup.test.ts
  • packages/jsx/src/focus_cleanup.ts
  • packages/jsx/src/hooks.ts
  • packages/jsx/src/hooks/useFocus.ts
  • packages/jsx/src/index.ts
  • packages/jsx/src/reconciler.test.ts
  • packages/jsx/src/reconciler.ts
  • packages/jsx/src/render.ts
  • packages/testing/src/render.ts
  • packages/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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 value

Linear 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 by Widget (not by id), 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 win

Empty 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0d53675 and c0e1ba4.

📒 Files selected for processing (10)
  • packages/jsx/src/focus_cleanup.test.ts
  • packages/jsx/src/focus_cleanup.ts
  • packages/jsx/src/hooks.ts
  • packages/jsx/src/hooks/useFocus.ts
  • packages/jsx/src/index.ts
  • packages/jsx/src/reconciler.test.ts
  • packages/jsx/src/reconciler.ts
  • packages/jsx/src/render.ts
  • packages/testing/src/render.ts
  • packages/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 -A1

Repository: 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 -30

Repository: 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 -100

Repository: Karanjot786/TermUI

Length of output: 4084


Remove the redundant type intersection.

The Widget base class already declares all three properties: id: string (line 79), focusable = false (line 100), and parent: 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.ts

Repository: 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.ts

Repository: 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 the beforeEach hook (line 44) and all tests that call getInstanceMap() (lines 52, 86, 117, 127, 136, 145, 159, 166), causing immediate stack overflow at runtime.

The reconciler exports the real getInstanceMap() at line 49 of reconciler.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 win

Incomplete refactoring: mixed use of globalThis.__termuijs_instances and getInstanceMap().

This file inconsistently accesses the instance map. Line 482 (fireMouse) uses the new getInstanceMap() accessor, while lines 312, 426, 516, and 576 still read globalThis.__termuijs_instances directly. According to the review stack context, this layer should refactor all direct reads to use getInstanceMap().

♻️ 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.

@knoxiboy

Copy link
Copy Markdown
Contributor Author

Two blocking issues:

  1. (globalThis as any).__termuijs_instances in production code (packages/jsx/src/focus_cleanup.ts)
    Problem: Accessing internal registry via globalThis as any breaks encapsulation and is fragile.
    Fix: Export a proper getRegisteredWidgets() function from the JSX reconciler and import it.
  2. let widget: any = null in production code
    Problem: Bypasses TypeScript safety on a variable used for .id, .focusable, .parent.
    Fix: Type as let widget: Widget | null = null

Checklist:

  • Replace globalThis hack with proper exported API
  • Type widget as Widget | null
  • Tests pass

Done

@github-actions github-actions Bot added the area:website Docs website. label Jun 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:jsx @termuijs/jsx area:website Docs website. area:widgets @termuijs/widgets quality:needs-work Needs changes before merge. type:testing +10 pts. Tests.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] Fix focus trap lock-up when removing focused nodes from JSX DOM

2 participants