From a8ebb46521808ef352ddf06888dc6ce554663d9b Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 22 Oct 2026 09:10:37 +0200 Subject: [PATCH] fix(aria/menu): avoid re-rendering when unrelated elements are added Updates the `SortedCollection` so it doesn't emit changes when a DOM node that's not a member of the collection is added. I also did some internal cleanup so we don't need to re-create the `Set` all the time and to keep a version counter. Fixes #33976. --- src/aria/private/utils/collection.spec.ts | 14 ++++-- src/aria/private/utils/collection.ts | 52 ++++++++++++----------- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/aria/private/utils/collection.spec.ts b/src/aria/private/utils/collection.spec.ts index 0745e1e3e0d2..f3fb312e2c75 100644 --- a/src/aria/private/utils/collection.spec.ts +++ b/src/aria/private/utils/collection.spec.ts @@ -74,6 +74,16 @@ describe('SortedCollection with real directives', () => { expect(orderedItems[0].value).toBe('C'); expect(orderedItems[2].value).toBe('A'); }); + + it('should not update collection if the DOM changes but the items have not moved', async () => { + const collection = collectionDirective._collection; + const previousItems = collection.orderedItems(); + + collectionDirective.element.appendChild(document.createElement('div')); + await waitForMicrotasks(); + + expect(collection.orderedItems()).toBe(previousItems); + }); }); @Component({ @@ -97,9 +107,7 @@ class TestCollectionComponent implements HasElement, OnDestroy { @Component({ selector: 'testItem', - template: ` -
{{value}}
- `, + template: `
{{value}}
`, }) class TestItemComponent implements HasElement, OnDestroy { readonly element = inject(ElementRef).nativeElement as HTMLElement; diff --git a/src/aria/private/utils/collection.ts b/src/aria/private/utils/collection.ts index 0cabc6343401..94639250844b 100644 --- a/src/aria/private/utils/collection.ts +++ b/src/aria/private/utils/collection.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {signal, computed, Signal} from '@angular/core'; +import {signal, Signal} from '@angular/core'; import {sortDirectives, HasElement} from './element'; /** @@ -17,49 +17,53 @@ import {sortDirectives, HasElement} from './element'; * TODO(ok7sai): replace Mutation Observer with internal API. */ export class SortedCollection { - private readonly _items = signal>(new Set()); - private readonly _version = signal(0); + private readonly _items = new Set(); + private readonly _sortedItems = signal([]); private _observer?: MutationObserver; - readonly orderedItems: Signal = computed(() => { - this._version(); // Track DOM changes - const itemsArray = Array.from(this._items()); - return itemsArray.sort(sortDirectives); - }); + readonly orderedItems: Signal = this._sortedItems.asReadonly(); register(item: T) { - this._items.update(set => { - const newSet = new Set(set); - newSet.add(item); - return newSet; - }); + if (!this._items.has(item)) { + this._items.add(item); + this._sortedItems.set(this._getSortedItems()); + } } unregister(item: T) { - this._items.update(set => { - const newSet = new Set(set); - newSet.delete(item); - return newSet; - }); + if (this._items.has(item)) { + this._items.delete(item); + this._sortedItems.set(this._getSortedItems()); + } } startObserving(element: HTMLElement) { - if (this._observer) { - this._observer.disconnect(); - } - + this.stopObserving(); this._observer = new MutationObserver(mutations => { const hasStructuralChange = mutations.some(m => m.addedNodes.length || m.removedNodes.length); + if (hasStructuralChange) { - this._version.update(v => v + 1); + const newItems = this._getSortedItems(); + const currentItems = this._sortedItems(); + const hasChanged = + newItems.length !== currentItems.length || + newItems.some((item, index) => currentItems[index] !== item); + + if (hasChanged) { + this._sortedItems.set(newItems); + } } }); - this._observer.observe(element, {childList: true, subtree: true}); + this._observer.observe(element, {childList: true, subtree: true, attributes: false}); } stopObserving() { this._observer?.disconnect(); this._observer = undefined; } + + private _getSortedItems() { + return Array.from(this._items).sort(sortDirectives); + } }