Which package?
@termuijs/core
What happened?
FocusManager.trap() moves focus to the first widget inside the trap container, but never
records which widget had focus before the trap was activated. When release() is called,
it only pops the trap stack — focus remains wherever it was inside the now-dismissed modal
container.
Relevant source (FocusManager.ts):
trap(containerId: string): void {
this._trapStack.push(containerId);
// Moves focus into the trap — pre-trap focus ID is never saved
const trapped = this._getActiveFocusables();
if (trapped.length > 0) {
const first = trapped.find(f => f.focusable);
if (first) {
const idx = this._focusables.findIndex(f => f.id === first.id);
if (idx >= 0) this._changeFocus(idx);
}
}
}
release(): void {
this._trapStack.pop();
// No focus restoration — focus is now floating inside a closed modal
}
This violates the ARIA Authoring Practices Guide for modal dialogs:
"When the dialog closes, focus returns to the element that invoked the dialog."
In a terminal app with keyboard-only navigation, this means after closing a modal the user
has no way to determine where focus is, and the next Tab/arrow keypress resumes navigation
from a widget inside the now-invisible modal container.
Expected: After release(), focus returns to the widget that had focus before trap()
was called.
Actual: Focus remains on the last focused widget inside the trap container.
Steps to reproduce
```typescript
import { FocusManager } from '@termuijs/core';
const fm = new FocusManager();
fm.start();
fm.register({ id: 'open-modal-button', tabIndex: 0, focusable: true });
fm.register({ id: 'modal-input', tabIndex: 1, focusable: true });
fm.register({ id: 'modal-confirm', tabIndex: 2, focusable: true });
fm.focusWidget('open-modal-button');
console.log(fm.currentId); // 'open-modal-button'
fm.registerContainerMembers('dialog', ['modal-input', 'modal-confirm']);
fm.trap('dialog');
console.log(fm.currentId); // 'modal-input' — focus moved into modal
fm.focusNext();
console.log(fm.currentId); // 'modal-confirm'
fm.release();
console.log(fm.currentId);
// Actual: 'modal-confirm' ← still inside the closed modal
// Expected: 'open-modal-button' ← restored to pre-modal trigger
```
Environment
- TermUI version: latest (main branch)
- Node.js 18+
- Any OS / terminal
GSSoC contributor?
Which package?
@termuijs/core
What happened?
FocusManager.trap()moves focus to the first widget inside the trap container, but neverrecords which widget had focus before the trap was activated. When
release()is called,it only pops the trap stack — focus remains wherever it was inside the now-dismissed modal
container.
Relevant source (FocusManager.ts):
This violates the ARIA Authoring Practices Guide for modal dialogs:
In a terminal app with keyboard-only navigation, this means after closing a modal the user
has no way to determine where focus is, and the next Tab/arrow keypress resumes navigation
from a widget inside the now-invisible modal container.
Expected: After
release(), focus returns to the widget that had focus beforetrap()was called.
Actual: Focus remains on the last focused widget inside the trap container.
Steps to reproduce
```typescript
import { FocusManager } from '@termuijs/core';
const fm = new FocusManager();
fm.start();
fm.register({ id: 'open-modal-button', tabIndex: 0, focusable: true });
fm.register({ id: 'modal-input', tabIndex: 1, focusable: true });
fm.register({ id: 'modal-confirm', tabIndex: 2, focusable: true });
fm.focusWidget('open-modal-button');
console.log(fm.currentId); // 'open-modal-button'
fm.registerContainerMembers('dialog', ['modal-input', 'modal-confirm']);
fm.trap('dialog');
console.log(fm.currentId); // 'modal-input' — focus moved into modal
fm.focusNext();
console.log(fm.currentId); // 'modal-confirm'
fm.release();
console.log(fm.currentId);
// Actual: 'modal-confirm' ← still inside the closed modal
// Expected: 'open-modal-button' ← restored to pre-modal trigger
```
Environment
GSSoC contributor?