Skip to content

Commit d7e500a

Browse files
committed
refactor(aria/tree): split directives and resolve circular dependencies
Splits up the directives across files and resolve circular dependencies in them.
1 parent d8fbab6 commit d7e500a

File tree

8 files changed

+295
-244
lines changed

8 files changed

+295
-244
lines changed

goldens/aria/tree/index.api.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { OnInit } from '@angular/core';
1717
import { Signal } from '@angular/core';
1818
import { TreeItemPattern } from '@angular/aria/private';
1919
import { TreePattern } from '@angular/aria/private';
20-
import { WritableSignal } from '@angular/core';
2120

2221
// @public
2322
export class Tree<V> {
@@ -37,7 +36,7 @@ export class Tree<V> {
3736
_register(child: TreeItem<V>): void;
3837
// (undocumented)
3938
scrollActiveItemIntoView(options?: ScrollIntoViewOptions): void;
40-
readonly selectionMode: _angular_core.InputSignal<"explicit" | "follow">;
39+
readonly selectionMode: _angular_core.InputSignal<"follow" | "explicit">;
4140
readonly softDisabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
4241
readonly textDirection: _angular_core.WritableSignal<_angular_cdk_bidi.Direction>;
4342
readonly typeaheadDelay: _angular_core.InputSignal<number>;
@@ -86,7 +85,7 @@ export class TreeItem<V> extends DeferredContentAware implements OnInit, OnDestr
8685

8786
// @public
8887
export class TreeItemGroup<V> implements OnInit, OnDestroy {
89-
readonly _childPatterns: Signal<TreeItemPattern<V>[]>;
88+
readonly _childPatterns: _angular_core.Signal<TreeItemPattern<V>[]>;
9089
readonly element: HTMLElement;
9190
// (undocumented)
9291
ngOnDestroy(): void;

src/aria/tree/BUILD.bazel

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ package(default_visibility = ["//visibility:public"])
44

55
ng_project(
66
name = "tree",
7-
srcs = [
8-
"index.ts",
9-
"tree.ts",
10-
],
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
1111
deps = [
1212
"//:node_modules/@angular/core",
1313
"//src/aria/combobox",

src/aria/tree/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
export {TreeItemGroup, Tree, TreeItem} from './tree';
9+
export {Tree} from './tree';
10+
export {TreeItem} from './tree-item';
11+
export {TreeItemGroup} from './tree-item-group';

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

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
Directive,
11+
ElementRef,
12+
computed,
13+
inject,
14+
input,
15+
signal,
16+
OnInit,
17+
OnDestroy,
18+
} from '@angular/core';
19+
import {TreeItemPattern, DeferredContent} from '@angular/aria/private';
20+
import type {TreeItem} from './tree-item';
21+
import {sortDirectives} from './utils';
22+
23+
/**
24+
* Group that contains children tree items.
25+
*
26+
* The `ngTreeItemGroup` structural directive should be applied to an `ng-template` that
27+
* wraps the child `ngTreeItem` elements. It is used to define a group of children for an
28+
* expandable `ngTreeItem`. The `ownedBy` input links the group to its parent `ngTreeItem`.
29+
*
30+
* ```html
31+
* <li ngTreeItem [value]="'parent-id'">
32+
* Parent Item
33+
* <ul role="group">
34+
* <ng-template ngTreeItemGroup [ownedBy]="parentTreeItemRef">
35+
* <li ngTreeItem [value]="'child-id'">Child Item</li>
36+
* </ng-template>
37+
* </ul>
38+
* </li>
39+
* ```
40+
*
41+
* @developerPreview 21.0
42+
*/
43+
@Directive({
44+
selector: 'ng-template[ngTreeItemGroup]',
45+
exportAs: 'ngTreeItemGroup',
46+
hostDirectives: [DeferredContent],
47+
})
48+
export class TreeItemGroup<V> implements OnInit, OnDestroy {
49+
/** A reference to the host element. */
50+
private readonly _elementRef = inject(ElementRef);
51+
52+
/** A reference to the host element. */
53+
readonly element = this._elementRef.nativeElement as HTMLElement;
54+
55+
/** The DeferredContent host directive. */
56+
private readonly _deferredContent = inject(DeferredContent);
57+
58+
/** All groupable items that are descendants of the group. */
59+
private readonly _unorderedItems = signal(new Set<TreeItem<V>>());
60+
61+
/** Child items within this group. */
62+
readonly _childPatterns = computed<TreeItemPattern<V>[]>(() =>
63+
[...this._unorderedItems()].sort(sortDirectives).map(c => c._pattern),
64+
);
65+
66+
/** Tree item that owns the group. */
67+
readonly ownedBy = input.required<TreeItem<V>>();
68+
69+
ngOnInit() {
70+
this._deferredContent.deferredContentAware.set(this.ownedBy());
71+
this.ownedBy()._register(this);
72+
}
73+
74+
ngOnDestroy() {
75+
this.ownedBy()._unregister();
76+
}
77+
78+
_register(child: TreeItem<V>) {
79+
this._unorderedItems().add(child);
80+
this._unorderedItems.set(new Set(this._unorderedItems()));
81+
}
82+
83+
_unregister(child: TreeItem<V>) {
84+
this._unorderedItems().delete(child);
85+
this._unorderedItems.set(new Set(this._unorderedItems()));
86+
}
87+
}

src/aria/tree/tree-item.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
Directive,
11+
ElementRef,
12+
afterRenderEffect,
13+
booleanAttribute,
14+
computed,
15+
inject,
16+
input,
17+
model,
18+
signal,
19+
Signal,
20+
OnInit,
21+
OnDestroy,
22+
afterNextRender,
23+
} from '@angular/core';
24+
import {_IdGenerator} from '@angular/cdk/a11y';
25+
import {ComboboxTreePattern, TreeItemPattern, DeferredContentAware} from '@angular/aria/private';
26+
import {Tree} from './tree';
27+
import {TreeItemGroup} from './tree-item-group';
28+
import {HasElement} from './utils';
29+
30+
/**
31+
* A selectable and expandable item in an `ngTree`.
32+
*
33+
* The `ngTreeItem` directive represents an individual node within an `ngTree`. It can be
34+
* selected, expanded (if it has children), and disabled. The `parent` input establishes
35+
* the hierarchical relationship within the tree.
36+
*
37+
* ```html
38+
* <li ngTreeItem [parent]="parentTreeOrGroup" value="item-id" label="Item Label">
39+
* Item Label
40+
* </li>
41+
* ```
42+
*
43+
* @developerPreview 21.0
44+
*/
45+
@Directive({
46+
selector: '[ngTreeItem]',
47+
exportAs: 'ngTreeItem',
48+
host: {
49+
'[attr.data-active]': 'active()',
50+
'role': 'treeitem',
51+
'[id]': '_pattern.id()',
52+
'[attr.aria-expanded]': '_expanded()',
53+
'[attr.aria-selected]': 'selected()',
54+
'[attr.aria-current]': '_pattern.current()',
55+
'[attr.aria-disabled]': '_pattern.disabled()',
56+
'[attr.aria-level]': 'level()',
57+
'[attr.aria-setsize]': '_pattern.setsize()',
58+
'[attr.aria-posinset]': '_pattern.posinset()',
59+
'[attr.tabindex]': '_pattern.tabIndex()',
60+
},
61+
})
62+
export class TreeItem<V> extends DeferredContentAware implements OnInit, OnDestroy, HasElement {
63+
/** A reference to the host element. */
64+
private readonly _elementRef = inject(ElementRef);
65+
66+
/** A reference to the host element. */
67+
readonly element = this._elementRef.nativeElement as HTMLElement;
68+
69+
/** The owned tree item group. */
70+
private readonly _group = signal<TreeItemGroup<V> | undefined>(undefined);
71+
72+
/** A unique identifier for the tree item. */
73+
readonly id = input(inject(_IdGenerator).getId('ng-tree-item-', true));
74+
75+
/** The value of the tree item. */
76+
readonly value = input.required<V>();
77+
78+
/** The parent tree root or tree item group. */
79+
readonly parent = input.required<Tree<V> | TreeItemGroup<V>>();
80+
81+
/** Whether the tree item is disabled. */
82+
readonly disabled = input(false, {transform: booleanAttribute});
83+
84+
/** Whether the tree item is selectable. */
85+
readonly selectable = input<boolean>(true);
86+
87+
/** Whether the tree item is expanded. */
88+
readonly expanded = model<boolean>(false);
89+
90+
/** Optional label for typeahead. Defaults to the element's textContent. */
91+
readonly label = input<string>();
92+
93+
/** Search term for typeahead. */
94+
readonly searchTerm = computed(() => this.label() ?? this.element.textContent);
95+
96+
/** The tree root. */
97+
readonly tree: Signal<Tree<V>> = computed(() => {
98+
if (this.parent() instanceof Tree) {
99+
return this.parent() as Tree<V>;
100+
}
101+
return (this.parent() as TreeItemGroup<V>).ownedBy().tree();
102+
});
103+
104+
/** Whether the item is active. */
105+
readonly active = computed(() => this._pattern.active());
106+
107+
/** The level of the current item in a tree. */
108+
readonly level = computed(() => this._pattern.level());
109+
110+
/** Whether the item is selected. */
111+
readonly selected = computed(() => this._pattern.selected());
112+
113+
/** Whether this item is visible due to all of its parents being expanded. */
114+
readonly visible = computed(() => this._pattern.visible());
115+
116+
/** Whether the tree is expanded. Use this value for aria-expanded. */
117+
protected readonly _expanded: Signal<boolean | undefined> = computed(() =>
118+
this._pattern.expandable() ? this._pattern.expanded() : undefined,
119+
);
120+
121+
/** The UI pattern for this item. */
122+
_pattern: TreeItemPattern<V>;
123+
124+
constructor() {
125+
super();
126+
afterNextRender(() => {
127+
if (this.tree()._pattern instanceof ComboboxTreePattern) {
128+
this.preserveContent.set(true);
129+
}
130+
});
131+
// Connect the group's hidden state to the DeferredContentAware's visibility.
132+
afterRenderEffect(() => {
133+
this.tree()._pattern instanceof ComboboxTreePattern
134+
? this.contentVisible.set(true)
135+
: this.contentVisible.set(this._pattern.expanded());
136+
});
137+
}
138+
139+
ngOnInit() {
140+
this.parent()._register(this);
141+
this.tree()._register(this);
142+
143+
const treePattern = computed(() => this.tree()._pattern);
144+
const parentPattern = computed(() => {
145+
if (this.parent() instanceof Tree) {
146+
return treePattern();
147+
}
148+
return (this.parent() as TreeItemGroup<V>).ownedBy()._pattern;
149+
});
150+
this._pattern = new TreeItemPattern<V>({
151+
...this,
152+
tree: treePattern,
153+
parent: parentPattern,
154+
children: computed(() => this._group()?._childPatterns() ?? []),
155+
hasChildren: computed(() => !!this._group()),
156+
element: () => this.element,
157+
searchTerm: () => this.searchTerm() ?? '',
158+
});
159+
}
160+
161+
ngOnDestroy() {
162+
this.parent()._unregister(this);
163+
this.tree()._unregister(this);
164+
}
165+
166+
_register(group: TreeItemGroup<V>) {
167+
this._group.set(group);
168+
}
169+
170+
_unregister() {
171+
this._group.set(undefined);
172+
}
173+
}

src/aria/tree/tree.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {ComponentFixture, TestBed} from '@angular/core/testing';
44
import {By} from '@angular/platform-browser';
55
import {Direction} from '@angular/cdk/bidi';
66
import {provideFakeDirectionality, runAccessibilityChecks} from '@angular/cdk/testing/private';
7-
import {Tree, TreeItem, TreeItemGroup} from './tree';
7+
import {Tree} from './tree';
8+
import {TreeItem} from './tree-item';
9+
import {TreeItemGroup} from './tree-item-group';
810

911
interface ModifierKeys {
1012
ctrlKey?: boolean;

0 commit comments

Comments
 (0)