Skip to content

Commit d167476

Browse files
authored
refactor(aria/tree): use SortedCollection (#33174)
1 parent 28626bd commit d167476

5 files changed

Lines changed: 56 additions & 28 deletions

File tree

‎goldens/aria/tree/index.api.md‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,28 @@ import { OnInit } from '@angular/core';
1111
import { Signal } from '@angular/core';
1212

1313
// @public
14-
export class Tree<V> {
14+
export class Tree<V> implements OnDestroy {
1515
constructor();
1616
readonly activeDescendant: Signal<string | undefined>;
17+
readonly _collection: SortedCollection<TreeItem<V>>;
1718
readonly currentType: _angular_core.InputSignal<"page" | "step" | "location" | "date" | "time" | "true" | "false">;
1819
readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
1920
readonly element: HTMLElement;
2021
readonly focusMode: _angular_core.InputSignal<"roving" | "activedescendant">;
2122
readonly id: _angular_core.InputSignal<string>;
2223
readonly multi: _angular_core.InputSignalWithTransform<boolean, unknown>;
2324
readonly nav: _angular_core.InputSignalWithTransform<boolean, unknown>;
25+
// (undocumented)
26+
ngOnDestroy(): void;
2427
readonly orientation: _angular_core.InputSignal<"vertical" | "horizontal">;
2528
readonly _pattern: TreePattern<V>;
2629
// (undocumented)
27-
_register(child: TreeItem<V>): void;
28-
// (undocumented)
2930
scrollActiveItemIntoView(options?: ScrollIntoViewOptions): void;
3031
readonly selectionMode: _angular_core.InputSignal<"follow" | "explicit">;
3132
readonly softDisabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
3233
readonly tabIndex: _angular_core.InputSignalWithTransform<number | undefined, string | number | undefined>;
3334
readonly textDirection: _angular_core.WritableSignal<_angular_cdk_bidi.Direction>;
3435
readonly typeaheadDelay: _angular_core.InputSignal<number>;
35-
// (undocumented)
36-
_unregister(child: TreeItem<V>): void;
3736
readonly value: _angular_core.ModelSignal<V[]>;
3837
readonly wrap: _angular_core.InputSignalWithTransform<boolean, unknown>;
3938
// (undocumented)

‎src/aria/tree/BUILD.bazel‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ ng_project(
2828
"//:node_modules/@angular/common",
2929
"//:node_modules/@angular/core",
3030
"//:node_modules/@angular/platform-browser",
31+
"//src/aria/private/testing",
3132
"//src/cdk/testing/private",
3233
],
3334
)

‎src/aria/tree/tree-item.ts‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,10 @@ export class TreeItem<V> extends DeferredContentAware implements OnInit, OnDestr
138138
}
139139

140140
ngOnInit() {
141-
this.parent()._register(this);
142-
this.tree()._register(this);
141+
if (this.parent() instanceof TreeItemGroup) {
142+
(this.parent() as TreeItemGroup<V>)._register(this);
143+
}
144+
this.tree()._collection.register(this);
143145

144146
const treePattern = computed(() => this.tree()._pattern);
145147
const parentPattern = computed(() => {
@@ -160,8 +162,10 @@ export class TreeItem<V> extends DeferredContentAware implements OnInit, OnDestr
160162
}
161163

162164
ngOnDestroy() {
163-
this.parent()._unregister(this);
164-
this.tree()._unregister(this);
165+
if (this.parent() instanceof TreeItemGroup) {
166+
(this.parent() as TreeItemGroup<V>)._unregister(this);
167+
}
168+
this.tree()._collection.unregister(this);
165169
}
166170

167171
_register(group: TreeItemGroup<V>) {

‎src/aria/tree/tree.spec.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {provideFakeDirectionality, runAccessibilityChecks} from '@angular/cdk/te
77
import {Tree} from './tree';
88
import {TreeItem} from './tree-item';
99
import {TreeItemGroup} from './tree-item-group';
10+
import {waitForMicrotasks} from '../private/testing/test-helpers';
1011

1112
interface ModifierKeys {
1213
ctrlKey?: boolean;
@@ -165,6 +166,31 @@ describe('Tree', () => {
165166
await runAccessibilityChecks(fixture.nativeElement);
166167
});
167168

169+
describe('dynamic updates', () => {
170+
it('should update item order correctly after items are shuffled', async () => {
171+
setupTestTree();
172+
expandAll();
173+
fixture.detectChanges();
174+
175+
const treeDirective = fixture.debugElement.query(By.directive(Tree)).injector.get(Tree);
176+
const itemsBefore = treeDirective._pattern.inputs.items();
177+
expect(itemsBefore.length).toBe(11);
178+
expect(itemsBefore[0].value()).toBe('fruits');
179+
180+
// Shuffle top-level nodes: move fruits to end
181+
const nodes = testComponent.nodes();
182+
const firstNode = nodes.shift()!;
183+
nodes.push(firstNode);
184+
testComponent.nodes.set([...nodes]);
185+
fixture.detectChanges();
186+
await waitForMicrotasks();
187+
188+
const itemsAfter = treeDirective._pattern.inputs.items();
189+
expect(itemsAfter.length).toBe(11);
190+
expect(itemsAfter[0].value()).toBe('vegetables');
191+
});
192+
});
193+
168194
describe('ARIA attributes and roles', () => {
169195
describe('default configuration', () => {
170196
beforeEach(() => {

‎src/aria/tree/tree.ts‎

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77
*/
88

99
import {
10-
Directive,
11-
ElementRef,
10+
afterNextRender,
1211
afterRenderEffect,
1312
booleanAttribute,
1413
computed,
14+
Directive,
15+
ElementRef,
1516
inject,
1617
input,
1718
model,
19+
OnDestroy,
1820
signal,
1921
Signal,
2022
untracked,
@@ -23,10 +25,10 @@ import {_IdGenerator} from '@angular/cdk/a11y';
2325
import {Directionality} from '@angular/cdk/bidi';
2426
import {
2527
ComboboxTreePattern,
28+
SortedCollection,
29+
tabIndexTransform,
2630
TreeItemPattern,
2731
TreePattern,
28-
sortDirectives,
29-
tabIndexTransform,
3032
} from '../private';
3133
import {ComboboxPopup} from '../combobox';
3234
import type {TreeItem} from './tree-item';
@@ -84,7 +86,7 @@ import type {TreeItem} from './tree-item';
8486
},
8587
hostDirectives: [ComboboxPopup],
8688
})
87-
export class Tree<V> {
89+
export class Tree<V> implements OnDestroy {
8890
/** A reference to the host element. */
8991
private readonly _elementRef = inject(ElementRef);
9092

@@ -96,8 +98,8 @@ export class Tree<V> {
9698
optional: true,
9799
});
98100

99-
/** All TreeItem instances within this tree. */
100-
private readonly _unorderedItems = signal(new Set<TreeItem<V>>());
101+
/** The collection of tree items. */
102+
readonly _collection = new SortedCollection<TreeItem<V>>();
101103

102104
/** A unique identifier for the tree. */
103105
readonly id = input(inject(_IdGenerator).getId('ng-tree-', true));
@@ -170,9 +172,7 @@ export class Tree<V> {
170172
const inputs = {
171173
...this,
172174
id: this.id,
173-
items: computed(() =>
174-
[...this._unorderedItems()].sort(sortDirectives).map(item => item._pattern),
175-
),
175+
items: computed(() => this._collection.orderedItems().map(item => item._pattern)),
176176
activeItem: signal<TreeItemPattern<V> | undefined>(undefined),
177177
combobox: () => this._popup?.combobox?._pattern,
178178
element: () => this.element,
@@ -184,11 +184,15 @@ export class Tree<V> {
184184

185185
this.activeDescendant = computed(() => this._pattern.activeDescendant());
186186

187+
afterNextRender(() => {
188+
this._collection.startObserving(this.element);
189+
});
190+
187191
if (this._popup?.combobox) {
188192
this._popup?._controls?.set(this._pattern as ComboboxTreePattern<V>);
189193
}
190194

191-
// Check for any violationns after the DOM has been updated.
195+
// Check for any violations after the DOM has been updated.
192196
afterRenderEffect({
193197
read: () => {
194198
if (typeof ngDevMode === 'undefined' || ngDevMode) {
@@ -228,14 +232,8 @@ export class Tree<V> {
228232
});
229233
}
230234

231-
_register(child: TreeItem<V>) {
232-
this._unorderedItems().add(child);
233-
this._unorderedItems.set(new Set(this._unorderedItems()));
234-
}
235-
236-
_unregister(child: TreeItem<V>) {
237-
this._unorderedItems().delete(child);
238-
this._unorderedItems.set(new Set(this._unorderedItems()));
235+
ngOnDestroy() {
236+
this._collection.stopObserving();
239237
}
240238

241239
scrollActiveItemIntoView(options: ScrollIntoViewOptions = {block: 'nearest'}) {

0 commit comments

Comments
 (0)