From 58b0d12c06e159688111bd294aaa2b78765704a4 Mon Sep 17 00:00:00 2001 From: Zainab Travadi Date: Mon, 22 Jun 2026 13:18:32 +0000 Subject: [PATCH 1/2] fix(core): preserve focused widget during registration reorder --- packages/core/src/events/FocusManager.test.ts | 29 +++++++++++++++++++ packages/core/src/events/FocusManager.ts | 11 +++++++ 2 files changed, 40 insertions(+) diff --git a/packages/core/src/events/FocusManager.test.ts b/packages/core/src/events/FocusManager.test.ts index b735f6a3..287a9ac0 100644 --- a/packages/core/src/events/FocusManager.test.ts +++ b/packages/core/src/events/FocusManager.test.ts @@ -264,4 +264,33 @@ describe('FocusManager Re-entrancy', () => { // After re-entrant focusNext, should end up on 'c' expect(fm.currentId).toBe('c'); }); + + it('registering a new focusable that sorts before current does not change current focus', () => { + const fm = new FocusManager(); + // A (10), B (20) + fm.register(makeWidget('a', 10, true)); + fm.register(makeWidget('b', 20, true)); + + // Focus B + fm.focusWidget('b'); + expect(fm.currentId).toBe('b'); + + // Inspect internal state before insertion + const beforeFocusables = (fm as any)._focusables.map((f: any) => f.id); + const beforeIndex = (fm as any)._currentIndex; + + // Register C with lower tabIndex that sorts before existing items + fm.register(makeWidget('c', 5, true)); + + const afterFocusables = (fm as any)._focusables.map((f: any) => f.id); + const afterIndex = (fm as any)._currentIndex; + + // Current focused ID should remain 'b' + expect(fm.currentId).toBe('b'); + + // For debugging/diagnostic purposes, ensure the array changed order + expect(beforeFocusables).not.toEqual(afterFocusables); + // And the index should have been adjusted so that it still points to 'b' + expect((fm as any)._focusables[afterIndex].id).toBe('b'); + }); }); \ No newline at end of file diff --git a/packages/core/src/events/FocusManager.ts b/packages/core/src/events/FocusManager.ts index 805edd3a..bd496bf4 100644 --- a/packages/core/src/events/FocusManager.ts +++ b/packages/core/src/events/FocusManager.ts @@ -93,9 +93,20 @@ export class FocusManager { * they are not lost when App has not yet subscribed to them. */ register(focusable: Focusable): void { + // Preserve currently focused id so that sorting the master list + // does not accidentally change which widget is focused. + const prevFocusedId = this.currentId; + this._focusables.push(focusable); this._focusables.sort((a, b) => a.tabIndex - b.tabIndex); + // If there was a previously focused widget, relocate _currentIndex + // to point to the same widget after the sort. + if (prevFocusedId) { + const newIdx = this._focusables.findIndex(f => f.id === prevFocusedId); + if (newIdx >= 0) this._currentIndex = newIdx; + } + // Auto-focus the first widget if nothing is focused if (this._currentIndex < 0 && focusable.focusable) { this._currentIndex = this._focusables.indexOf(focusable); From bd94d0fa7aff0d465652815578192cd42e68bae5 Mon Sep 17 00:00:00 2001 From: Zainab Travadi Date: Mon, 22 Jun 2026 13:24:03 +0000 Subject: [PATCH 2/2] test(core): simplify FocusManager regression test --- packages/core/src/events/FocusManager.test.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/core/src/events/FocusManager.test.ts b/packages/core/src/events/FocusManager.test.ts index 287a9ac0..fb18aada 100644 --- a/packages/core/src/events/FocusManager.test.ts +++ b/packages/core/src/events/FocusManager.test.ts @@ -275,22 +275,10 @@ describe('FocusManager Re-entrancy', () => { fm.focusWidget('b'); expect(fm.currentId).toBe('b'); - // Inspect internal state before insertion - const beforeFocusables = (fm as any)._focusables.map((f: any) => f.id); - const beforeIndex = (fm as any)._currentIndex; - // Register C with lower tabIndex that sorts before existing items fm.register(makeWidget('c', 5, true)); - const afterFocusables = (fm as any)._focusables.map((f: any) => f.id); - const afterIndex = (fm as any)._currentIndex; - - // Current focused ID should remain 'b' + // Observable behavior: focused id must remain 'b' expect(fm.currentId).toBe('b'); - - // For debugging/diagnostic purposes, ensure the array changed order - expect(beforeFocusables).not.toEqual(afterFocusables); - // And the index should have been adjusted so that it still points to 'b' - expect((fm as any)._focusables[afterIndex].id).toBe('b'); }); }); \ No newline at end of file