Skip to content

Commit 5306115

Browse files
committed
refactor(aria/tree): replace contentChildren with SortedCollection
1 parent bb4f8ec commit 5306115

5 files changed

Lines changed: 55 additions & 27 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: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,24 @@
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,
1819
numberAttribute,
20+
OnDestroy,
1921
signal,
2022
Signal,
2123
untracked,
2224
} from '@angular/core';
2325
import {_IdGenerator} from '@angular/cdk/a11y';
2426
import {Directionality} from '@angular/cdk/bidi';
25-
import {ComboboxTreePattern, TreeItemPattern, TreePattern, sortDirectives} from '../private';
27+
import {ComboboxTreePattern, TreeItemPattern, TreePattern, SortedCollection} from '../private';
2628
import {ComboboxPopup} from '../combobox';
2729
import type {TreeItem} from './tree-item';
2830

@@ -79,7 +81,7 @@ import type {TreeItem} from './tree-item';
7981
},
8082
hostDirectives: [ComboboxPopup],
8183
})
82-
export class Tree<V> {
84+
export class Tree<V> implements OnDestroy {
8385
/** A reference to the host element. */
8486
private readonly _elementRef = inject(ElementRef);
8587

@@ -91,8 +93,8 @@ export class Tree<V> {
9193
optional: true,
9294
});
9395

94-
/** All TreeItem instances within this tree. */
95-
private readonly _unorderedItems = signal(new Set<TreeItem<V>>());
96+
/** The collection of tree items. */
97+
readonly _collection = new SortedCollection<TreeItem<V>>();
9698

9799
/** A unique identifier for the tree. */
98100
readonly id = input(inject(_IdGenerator).getId('ng-tree-', true));
@@ -165,9 +167,7 @@ export class Tree<V> {
165167
const inputs = {
166168
...this,
167169
id: this.id,
168-
items: computed(() =>
169-
[...this._unorderedItems()].sort(sortDirectives).map(item => item._pattern),
170-
),
170+
items: computed(() => this._collection.orderedItems().map(item => item._pattern)),
171171
activeItem: signal<TreeItemPattern<V> | undefined>(undefined),
172172
combobox: () => this._popup?.combobox?._pattern,
173173
element: () => this.element,
@@ -179,11 +179,15 @@ export class Tree<V> {
179179

180180
this.activeDescendant = computed(() => this._pattern.activeDescendant());
181181

182+
afterNextRender(() => {
183+
this._collection.startObserving(this.element);
184+
});
185+
182186
if (this._popup?.combobox) {
183187
this._popup?._controls?.set(this._pattern as ComboboxTreePattern<V>);
184188
}
185189

186-
// Check for any violationns after the DOM has been updated.
190+
// Check for any violations after the DOM has been updated.
187191
afterRenderEffect({
188192
read: () => {
189193
if (typeof ngDevMode === 'undefined' || ngDevMode) {
@@ -223,14 +227,8 @@ export class Tree<V> {
223227
});
224228
}
225229

226-
_register(child: TreeItem<V>) {
227-
this._unorderedItems().add(child);
228-
this._unorderedItems.set(new Set(this._unorderedItems()));
229-
}
230-
231-
_unregister(child: TreeItem<V>) {
232-
this._unorderedItems().delete(child);
233-
this._unorderedItems.set(new Set(this._unorderedItems()));
230+
ngOnDestroy() {
231+
this._collection.stopObserving();
234232
}
235233

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

0 commit comments

Comments
 (0)