Skip to content

[bug] FocusManager.release() has no guard against empty trap stack or mismatched container #1744

@palak170306-design

Description

@palak170306-design

Which package?

@termuijs/core

What happened?

FocusManager.release() unconditionally pops the trap stack with no validation:

release(): void {
    this._trapStack.pop();
}

This causes two distinct bugs:

Bug A — Calling release() on an empty stack is silently ignored.
If a component's cleanup logic calls release() more than once (e.g., due to double-unmount
in a dev hot-reload or an error boundary teardown), there is no error or warning. The stack
silently stays empty. This makes debugging focus state corruption nearly impossible.

Bug B — release() has no containerId parameter, making nested modal cleanup order-sensitive
with no enforcement.

release() always pops the top of the stack regardless of which container is releasing.
If two nested modals are open (trap stack: ['outer-modal', 'inner-modal']) and the outer
modal's cleanup fires before the inner's (which can happen in React-style teardown), it pops
'inner-modal' off the top — the wrong entry. The inner modal then pops 'outer-modal'.
The trap stack ends up in a state that neither modal intended, and the bug produces no error.

Expected: release() should accept a containerId, verify it matches the top of the
stack, and emit a warning/throw if it does not.
Actual: Any call to release() pops the stack top silently — wrong container, empty
stack, or double-release are all silent no-ops.

Steps to reproduce

```typescript
import { FocusManager } from '@termuijs/core';

const fm = new FocusManager();
fm.register({ id: 'a', tabIndex: 0, focusable: true });
fm.register({ id: 'modal-btn', tabIndex: 0, focusable: true });

fm.registerContainerMembers('my-modal', ['modal-btn']);
fm.trap('my-modal');

console.log(fm.isTrapped); // true

// Simulate double-release (e.g. cleanup fired twice)
fm.release();
fm.release(); // No error. Stack is now empty. Silent bug.

console.log(fm.isTrapped); // false — correct
// But if there was a second trap underneath, it was silently destroyed.
```

Environment

  • TermUI version: latest (main branch)
  • Node.js 18+
  • Any OS / terminal

GSSoC contributor?

  • Yes. You contribute under GSSoC 2026.
  • No.

Metadata

Metadata

Labels

assignedIssue claimed by a contributor.type:bug+10 pts. Bug fix.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions