Skip to content

Commit cd62949

Browse files
authored
refactor(multiple): stabilize focus state when active item is deleted (angular#33199)
- Calls setDefaultState instead of dropping focus when active item is removed. - Clears stale reference to prevent memory leaks. - Adds focus stability unit tests for listbox and tree.
1 parent 2b668f2 commit cd62949

4 files changed

Lines changed: 40 additions & 2 deletions

File tree

src/aria/listbox/listbox.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,23 @@ describe('Listbox', () => {
822822
expect(listboxInstance.value()).toEqual([]);
823823
});
824824
});
825+
826+
describe('item mutations and focus stability', () => {
827+
it('should recover focus by shifting to the default state if the active option is removed', async () => {
828+
setupListbox({focusMode: 'activedescendant'});
829+
click(2);
830+
expect(listboxElement.getAttribute('aria-activedescendant')).toBe(optionElements[2].id);
831+
832+
const testComponent = fixture.componentInstance as ListboxExample;
833+
const updatedOptions = testComponent.options().filter(o => o.value !== 2);
834+
testComponent.options.set(updatedOptions);
835+
fixture.detectChanges();
836+
await waitForMicrotasks();
837+
defineTestVariables(fixture);
838+
839+
expect(listboxElement.getAttribute('aria-activedescendant')).toBe(optionElements[0].id);
840+
});
841+
});
825842
});
826843

827844
interface TestOption {

src/aria/listbox/listbox.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,9 @@ export class Listbox<V> implements OnDestroy {
182182
const items = inputs.items();
183183
const activeItem = untracked(() => inputs.activeItem());
184184

185-
if (!items.some(i => i === activeItem) && activeItem) {
185+
if (activeItem && !items.some(i => i === activeItem)) {
186186
this._pattern.listBehavior.unfocus();
187+
this._pattern.setDefaultState();
187188
}
188189
},
189190
});

src/aria/tree/tree.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,6 +1556,25 @@ describe('Tree', () => {
15561556
});
15571557
}
15581558
});
1559+
1560+
describe('item mutations and focus stability', () => {
1561+
it('should recover focus by shifting to the default state if the active item is removed', async () => {
1562+
setupTestTree();
1563+
updateTree({focusMode: 'activedescendant'});
1564+
1565+
const vegetablesEl = getTreeItemElementByValue('vegetables')!;
1566+
click(vegetablesEl);
1567+
expect(getFocusedTreeItemValue()).toBe('vegetables');
1568+
1569+
const updatedNodes = testComponent.nodes().filter(n => n.value !== 'vegetables');
1570+
testComponent.nodes.set(updatedNodes);
1571+
fixture.detectChanges();
1572+
await waitForMicrotasks();
1573+
defineTestVariables();
1574+
1575+
expect(getFocusedTreeItemValue()).toBe('fruits');
1576+
});
1577+
});
15591578
});
15601579

15611580
interface TestTreeNode<V = string> {

src/aria/tree/tree.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,9 @@ export class Tree<V> implements OnDestroy {
192192
const items = inputs.items();
193193
const activeItem = untracked(() => inputs.activeItem());
194194

195-
if (!items.some(i => i === activeItem) && activeItem) {
195+
if (activeItem && !items.some(i => i === activeItem)) {
196196
this._pattern.treeBehavior.unfocus();
197+
this._pattern.setDefaultState();
197198
}
198199
},
199200
});

0 commit comments

Comments
 (0)