diff --git a/src/aria/listbox/listbox.spec.ts b/src/aria/listbox/listbox.spec.ts index 8399824b14b4..0a3e3c2c69f2 100644 --- a/src/aria/listbox/listbox.spec.ts +++ b/src/aria/listbox/listbox.spec.ts @@ -811,6 +811,23 @@ describe('Listbox', () => { expect(listboxInstance.value()).toEqual([]); }); }); + + describe('item mutations and focus stability', () => { + it('should recover focus by shifting to the default state if the active option is removed', async () => { + setupListbox({focusMode: 'activedescendant'}); + click(2); + expect(listboxElement.getAttribute('aria-activedescendant')).toBe(optionElements[2].id); + + const testComponent = fixture.componentInstance as ListboxExample; + const updatedOptions = testComponent.options().filter(o => o.value !== 2); + testComponent.options.set(updatedOptions); + fixture.detectChanges(); + await waitForMicrotasks(); + defineTestVariables(fixture); + + expect(listboxElement.getAttribute('aria-activedescendant')).toBe(optionElements[0].id); + }); + }); }); interface TestOption { diff --git a/src/aria/listbox/listbox.ts b/src/aria/listbox/listbox.ts index d237e66aa3c8..afa340675dfd 100644 --- a/src/aria/listbox/listbox.ts +++ b/src/aria/listbox/listbox.ts @@ -201,8 +201,9 @@ export class Listbox implements OnDestroy { const items = inputs.items(); const activeItem = untracked(() => inputs.activeItem()); - if (!items.some(i => i === activeItem) && activeItem) { + if (activeItem && !items.some(i => i === activeItem)) { this._pattern.listBehavior.unfocus(); + this._pattern.setDefaultState(); } }, }); diff --git a/src/aria/tree/tree.spec.ts b/src/aria/tree/tree.spec.ts index b80bfbe4e566..b6485b059e6b 100644 --- a/src/aria/tree/tree.spec.ts +++ b/src/aria/tree/tree.spec.ts @@ -1556,6 +1556,25 @@ describe('Tree', () => { }); } }); + + describe('item mutations and focus stability', () => { + it('should recover focus by shifting to the default state if the active item is removed', async () => { + setupTestTree(); + updateTree({focusMode: 'activedescendant'}); + + const vegetablesEl = getTreeItemElementByValue('vegetables')!; + click(vegetablesEl); + expect(getFocusedTreeItemValue()).toBe('vegetables'); + + const updatedNodes = testComponent.nodes().filter(n => n.value !== 'vegetables'); + testComponent.nodes.set(updatedNodes); + fixture.detectChanges(); + await waitForMicrotasks(); + defineTestVariables(); + + expect(getFocusedTreeItemValue()).toBe('fruits'); + }); + }); }); interface TestTreeNode { diff --git a/src/aria/tree/tree.ts b/src/aria/tree/tree.ts index e018099d6044..9ce5fd04ed1d 100644 --- a/src/aria/tree/tree.ts +++ b/src/aria/tree/tree.ts @@ -212,8 +212,9 @@ export class Tree implements OnDestroy { const items = inputs.items(); const activeItem = untracked(() => inputs.activeItem()); - if (!items.some(i => i === activeItem) && activeItem) { + if (activeItem && !items.some(i => i === activeItem)) { this._pattern.treeBehavior.unfocus(); + this._pattern.setDefaultState(); } }, });