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);
+ }
}