Skip to content

Commit 2f094f2

Browse files
authored
refactor(aria/tree): remove tree group wrapper (#32079)
1 parent 85f596b commit 2f094f2

31 files changed

+82
-152
lines changed

src/aria/combobox/combobox.spec.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {By} from '@angular/platform-browser';
44
import {Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer} from '../combobox';
55
import {Listbox, Option} from '../listbox';
66
import {runAccessibilityChecks} from '@angular/cdk/testing/private';
7-
import {Tree, TreeItem, TreeItemGroup, TreeItemGroupContent} from '../tree';
7+
import {Tree, TreeItem, TreeItemGroup} from '../tree';
88
import {NgTemplateOutlet} from '@angular/common';
99

1010
describe('Combobox', () => {
@@ -1083,8 +1083,8 @@ class ComboboxListboxExample {
10831083
</li>
10841084
10851085
@if (node.children) {
1086-
<ul ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
1087-
<ng-template ngTreeItemGroupContent>
1086+
<ul role="group">
1087+
<ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
10881088
<ng-template
10891089
[ngTemplateOutlet]="treeNodes"
10901090
[ngTemplateOutletContext]="{nodes: node.children, parent: group}"
@@ -1102,7 +1102,6 @@ class ComboboxListboxExample {
11021102
Tree,
11031103
TreeItem,
11041104
TreeItemGroup,
1105-
TreeItemGroupContent,
11061105
NgTemplateOutlet,
11071106
],
11081107
})

src/aria/deferred-content/deferred-content.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,21 @@ export class DeferredContentAware {
4242
*/
4343
@Directive()
4444
export class DeferredContent {
45-
private readonly _deferredContentAware = inject(DeferredContentAware);
45+
private readonly _deferredContentAware = inject(DeferredContentAware, {optional: true});
4646
private readonly _templateRef = inject(TemplateRef);
4747
private readonly _viewContainerRef = inject(ViewContainerRef);
4848
private _isRendered = false;
4949

50+
readonly deferredContentAware = signal(this._deferredContentAware);
51+
5052
constructor() {
5153
afterRenderEffect(() => {
52-
if (this._deferredContentAware.contentVisible()) {
54+
if (this.deferredContentAware()?.contentVisible()) {
5355
if (this._isRendered) return;
5456
this._viewContainerRef.clear();
5557
this._viewContainerRef.createEmbeddedView(this._templateRef);
5658
this._isRendered = true;
57-
} else if (!this._deferredContentAware.preserveContent()) {
59+
} else if (!this.deferredContentAware()?.preserveContent()) {
5860
this._viewContainerRef.clear();
5961
this._isRendered = false;
6062
}

src/aria/tree/index.ts

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

9-
export {TreeItemGroup, TreeItemGroupContent, Tree, TreeItem} from './tree';
9+
export {TreeItemGroup, Tree, TreeItem} from './tree';

src/aria/tree/tree.spec.ts

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ 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, TreeItemGroupContent} from './tree';
7+
import {Tree, TreeItem, TreeItemGroup} from './tree';
88

99
interface ModifierKeys {
1010
ctrlKey?: boolean;
@@ -19,7 +19,6 @@ describe('Tree', () => {
1919
let treeElement: HTMLElement;
2020
let treeInstance: Tree<string>;
2121
let treeItemElements: HTMLElement[];
22-
let treeItemGroupElements: HTMLElement[];
2322

2423
const keydown = (key: string, modifierKeys: ModifierKeys = {}) => {
2524
const event = new KeyboardEvent('keydown', {key, bubbles: true, ...modifierKeys});
@@ -67,12 +66,10 @@ describe('Tree', () => {
6766
function defineTestVariables() {
6867
const treeDebugElement = fixture.debugElement.query(By.directive(Tree));
6968
const treeItemDebugElements = fixture.debugElement.queryAll(By.directive(TreeItem));
70-
const treeItemGroupDebugElements = fixture.debugElement.queryAll(By.directive(TreeItemGroup));
7169

7270
treeElement = treeDebugElement.nativeElement as HTMLElement;
7371
treeInstance = treeDebugElement.componentInstance as Tree<string>;
7472
treeItemElements = treeItemDebugElements.map(debugEl => debugEl.nativeElement);
75-
treeItemGroupElements = treeItemGroupDebugElements.map(debugEl => debugEl.nativeElement);
7673
}
7774

7875
function updateTree(
@@ -131,10 +128,6 @@ describe('Tree', () => {
131128
return treeItemElements.find(el => el.getAttribute('data-value') === String(value));
132129
}
133130

134-
function getTreeItemGroupElementByValue(value: string): HTMLElement | undefined {
135-
return treeItemGroupElements.find(el => el.getAttribute('data-group-for') === String(value));
136-
}
137-
138131
function getFocusedTreeItemValue(): string | undefined {
139132
let item: HTMLElement | undefined;
140133
if (testComponent.focusMode() === 'roving') {
@@ -187,14 +180,6 @@ describe('Tree', () => {
187180
expect(getTreeItemElementByValue('blueberry')!.getAttribute('role')).toBe('treeitem');
188181
});
189182

190-
it('should correctly set the role attribute to "group" for TreeItemGroup', () => {
191-
expandAll();
192-
193-
expect(getTreeItemGroupElementByValue('fruits')!.getAttribute('role')).toBe('group');
194-
expect(getTreeItemGroupElementByValue('vegetables')!.getAttribute('role')).toBe('group');
195-
expect(getTreeItemGroupElementByValue('berries')!.getAttribute('role')).toBe('group');
196-
});
197-
198183
it('should set aria-orientation to "vertical" by default', () => {
199184
expect(treeElement.getAttribute('aria-orientation')).toBe('vertical');
200185
});
@@ -262,12 +247,6 @@ describe('Tree', () => {
262247
expect(strawberry.getAttribute('aria-setsize')).toBe('2');
263248
expect(strawberry.getAttribute('aria-posinset')).toBe('1');
264249
});
265-
266-
it('should set aria-owns on expandable items pointing to their group id', () => {
267-
const fruitsItem = getTreeItemElementByValue('fruits')!;
268-
const group = getTreeItemGroupElementByValue('fruits')!;
269-
expect(fruitsItem.getAttribute('aria-owns')).toBe(group!.id);
270-
});
271250
});
272251

273252
describe('custom configuration', () => {
@@ -1359,12 +1338,11 @@ interface TestTreeNode<V = string> {
13591338
>
13601339
{{ node.label }}
13611340
@if (node.children !== undefined && node.children!.length > 0) {
1362-
<ul
1363-
ngTreeItemGroup
1364-
[ownedBy]="treeItem"
1365-
[attr.data-group-for]="node.value"
1366-
#group="ngTreeItemGroup">
1367-
<ng-template ngTreeItemGroupContent>
1341+
<ul role="group">
1342+
<ng-template
1343+
ngTreeItemGroup
1344+
[ownedBy]="treeItem"
1345+
#group="ngTreeItemGroup">
13681346
@for (node of node.children; track node.value) {
13691347
<ng-template [ngTemplateOutlet]="nodeTemplate" [ngTemplateOutletContext]="{ node: node, parent: group }" />
13701348
}
@@ -1374,7 +1352,7 @@ interface TestTreeNode<V = string> {
13741352
</li>
13751353
</ng-template>
13761354
`,
1377-
imports: [Tree, TreeItem, TreeItemGroup, TreeItemGroupContent, NgTemplateOutlet],
1355+
imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],
13781356
})
13791357
class TestTreeComponent {
13801358
nodes = signal<TestTreeNode[]>([

src/aria/tree/tree.ts

Lines changed: 18 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -218,13 +218,12 @@ export class Tree<V> {
218218
'[attr.aria-current]': 'pattern.current()',
219219
'[attr.aria-disabled]': 'pattern.disabled()',
220220
'[attr.aria-level]': 'pattern.level()',
221-
'[attr.aria-owns]': 'ownsId()',
222221
'[attr.aria-setsize]': 'pattern.setsize()',
223222
'[attr.aria-posinset]': 'pattern.posinset()',
224223
'[attr.tabindex]': 'pattern.tabindex()',
225224
},
226225
})
227-
export class TreeItem<V> implements OnInit, OnDestroy, HasElement {
226+
export class TreeItem<V> extends DeferredContentAware implements OnInit, OnDestroy, HasElement {
228227
/** A reference to the tree item element. */
229228
private readonly _elementRef = inject(ElementRef);
230229

@@ -234,9 +233,6 @@ export class TreeItem<V> implements OnInit, OnDestroy, HasElement {
234233
/** The owned tree item group. */
235234
private readonly _group = signal<TreeItemGroup<V> | undefined>(undefined);
236235

237-
/** The id of the owned group. */
238-
readonly ownsId = computed(() => this._group()?.id);
239-
240236
/** The host native element. */
241237
readonly element = computed(() => this._elementRef.nativeElement);
242238

@@ -267,9 +263,13 @@ export class TreeItem<V> implements OnInit, OnDestroy, HasElement {
267263
pattern: TreeItemPattern<V>;
268264

269265
constructor() {
270-
// Updates the visibility of the owned group.
266+
super();
267+
this.preserveContent.set(true);
268+
// Connect the group's hidden state to the DeferredContentAware's visibility.
271269
afterRenderEffect(() => {
272-
this._group()?.visible.set(this.pattern.expanded());
270+
this.tree().pattern instanceof ComboboxTreePattern
271+
? this.contentVisible.set(true)
272+
: this.contentVisible.set(this.pattern.expanded());
273273
});
274274
}
275275

@@ -289,12 +289,7 @@ export class TreeItem<V> implements OnInit, OnDestroy, HasElement {
289289
id: () => this._id,
290290
tree: treePattern,
291291
parent: parentPattern,
292-
children: computed(
293-
() =>
294-
this._group()
295-
?.children()
296-
.map(item => (item as TreeItem<V>).pattern) ?? [],
297-
),
292+
children: computed(() => this._group()?.children() ?? []),
298293
hasChildren: computed(() => !!this._group()),
299294
});
300295
}
@@ -314,60 +309,30 @@ export class TreeItem<V> implements OnInit, OnDestroy, HasElement {
314309
}
315310

316311
/**
317-
* Container that designates content as a group.
312+
* Contains children tree itmes.
318313
*/
319314
@Directive({
320-
selector: '[ngTreeItemGroup]',
315+
selector: 'ng-template[ngTreeItemGroup]',
321316
exportAs: 'ngTreeItemGroup',
322-
hostDirectives: [
323-
{
324-
directive: DeferredContentAware,
325-
inputs: ['preserveContent'],
326-
},
327-
],
328-
host: {
329-
'class': 'ng-treeitem-group',
330-
'role': 'group',
331-
'[id]': 'id',
332-
'[attr.inert]': 'visible() ? null : true',
333-
},
317+
hostDirectives: [DeferredContent],
334318
})
335-
export class TreeItemGroup<V> implements OnInit, OnDestroy, HasElement {
336-
/** A reference to the group element. */
337-
private readonly _elementRef = inject(ElementRef);
338-
339-
/** The DeferredContentAware host directive. */
340-
private readonly _deferredContentAware = inject(DeferredContentAware);
319+
export class TreeItemGroup<V> implements OnInit, OnDestroy {
320+
/** The DeferredContent host directive. */
321+
private readonly _deferredContent = inject(DeferredContent);
341322

342323
/** All groupable items that are descendants of the group. */
343324
private readonly _unorderedItems = signal(new Set<TreeItem<V>>());
344325

345-
/** The host native element. */
346-
readonly element = computed(() => this._elementRef.nativeElement);
347-
348-
/** Unique ID for the group. */
349-
readonly id = inject(_IdGenerator).getId('ng-tree-group-');
350-
351-
/** Whether the group is visible. */
352-
readonly visible = signal(true);
353-
354326
/** Child items within this group. */
355-
readonly children = computed(() => [...this._unorderedItems()].sort(sortDirectives));
327+
readonly children = computed<TreeItemPattern<V>[]>(() =>
328+
[...this._unorderedItems()].sort(sortDirectives).map(c => c.pattern),
329+
);
356330

357331
/** Tree item that owns the group. */
358332
readonly ownedBy = input.required<TreeItem<V>>();
359333

360-
constructor() {
361-
this._deferredContentAware.preserveContent.set(true);
362-
// Connect the group's hidden state to the DeferredContentAware's visibility.
363-
afterRenderEffect(() => {
364-
this.ownedBy().tree().pattern instanceof ComboboxTreePattern
365-
? this._deferredContentAware.contentVisible.set(true)
366-
: this._deferredContentAware.contentVisible.set(this.visible());
367-
});
368-
}
369-
370334
ngOnInit() {
335+
this._deferredContent.deferredContentAware.set(this.ownedBy());
371336
this.ownedBy().register(this);
372337
}
373338

@@ -385,13 +350,3 @@ export class TreeItemGroup<V> implements OnInit, OnDestroy, HasElement {
385350
this._unorderedItems.set(new Set(this._unorderedItems()));
386351
}
387352
}
388-
389-
/**
390-
* A structural directive that marks the `ng-template` to be used as the content
391-
* for a `TreeItemGroup`. This content can be lazily loaded.
392-
*/
393-
@Directive({
394-
selector: 'ng-template[ngTreeItemGroupContent]',
395-
hostDirectives: [DeferredContent],
396-
})
397-
export class TreeItemGroupContent {}

src/components-examples/aria/combobox/combobox-tree-auto-select/combobox-tree-auto-select-example.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@
4949
</li>
5050

5151
@if (node.children) {
52-
<ul ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
53-
<ng-template ngTreeItemGroupContent>
52+
<ul role="group">
53+
<ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
5454
<ng-template
5555
[ngTemplateOutlet]="treeNodes"
5656
[ngTemplateOutletContext]="{nodes: node.children, parent: group}"

src/components-examples/aria/combobox/combobox-tree-auto-select/combobox-tree-auto-select-example.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
ComboboxPopup,
1313
ComboboxPopupContainer,
1414
} from '@angular/aria/combobox';
15-
import {Tree, TreeItem, TreeItemGroup, TreeItemGroupContent} from '@angular/aria/tree';
15+
import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';
1616
import {
1717
afterRenderEffect,
1818
ChangeDetectionStrategy,
@@ -38,7 +38,6 @@ import {NgTemplateOutlet} from '@angular/common';
3838
Tree,
3939
TreeItem,
4040
TreeItemGroup,
41-
TreeItemGroupContent,
4241
NgTemplateOutlet,
4342
],
4443
changeDetection: ChangeDetectionStrategy.OnPush,

src/components-examples/aria/combobox/combobox-tree-highlight/combobox-tree-highlight-example.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@
4949
</li>
5050

5151
@if (node.children) {
52-
<ul ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
53-
<ng-template ngTreeItemGroupContent>
52+
<ul role="group">
53+
<ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
5454
<ng-template
5555
[ngTemplateOutlet]="treeNodes"
5656
[ngTemplateOutletContext]="{nodes: node.children, parent: group}"

src/components-examples/aria/combobox/combobox-tree-highlight/combobox-tree-highlight-example.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
ComboboxPopup,
1313
ComboboxPopupContainer,
1414
} from '@angular/aria/combobox';
15-
import {Tree, TreeItem, TreeItemGroup, TreeItemGroupContent} from '@angular/aria/tree';
15+
import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';
1616
import {
1717
afterRenderEffect,
1818
ChangeDetectionStrategy,
@@ -38,7 +38,6 @@ import {NgTemplateOutlet} from '@angular/common';
3838
Tree,
3939
TreeItem,
4040
TreeItemGroup,
41-
TreeItemGroupContent,
4241
NgTemplateOutlet,
4342
],
4443
changeDetection: ChangeDetectionStrategy.OnPush,

src/components-examples/aria/combobox/combobox-tree-manual/combobox-tree-manual-example.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@
4949
</li>
5050

5151
@if (node.children) {
52-
<ul ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
53-
<ng-template ngTreeItemGroupContent>
52+
<ul role="group">
53+
<ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
5454
<ng-template
5555
[ngTemplateOutlet]="treeNodes"
5656
[ngTemplateOutletContext]="{nodes: node.children, parent: group}"

0 commit comments

Comments
 (0)