Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions src/aria/combobox/combobox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {By} from '@angular/platform-browser';
import {Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer} from '../combobox';
import {Listbox, Option} from '../listbox';
import {runAccessibilityChecks} from '@angular/cdk/testing/private';
import {Tree, TreeItem, TreeItemGroup, TreeItemGroupContent} from '../tree';
import {Tree, TreeItem, TreeItemGroup} from '../tree';
import {NgTemplateOutlet} from '@angular/common';

describe('Combobox', () => {
Expand Down Expand Up @@ -1083,8 +1083,8 @@ class ComboboxListboxExample {
</li>

@if (node.children) {
<ul ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
<ng-template ngTreeItemGroupContent>
<ul role="group">
<ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
<ng-template
[ngTemplateOutlet]="treeNodes"
[ngTemplateOutletContext]="{nodes: node.children, parent: group}"
Expand All @@ -1102,7 +1102,6 @@ class ComboboxListboxExample {
Tree,
TreeItem,
TreeItemGroup,
TreeItemGroupContent,
NgTemplateOutlet,
],
})
Expand Down
8 changes: 5 additions & 3 deletions src/aria/deferred-content/deferred-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,21 @@ export class DeferredContentAware {
*/
@Directive()
export class DeferredContent {
private readonly _deferredContentAware = inject(DeferredContentAware);
private readonly _deferredContentAware = inject(DeferredContentAware, {optional: true});
private readonly _templateRef = inject(TemplateRef);
private readonly _viewContainerRef = inject(ViewContainerRef);
private _isRendered = false;

readonly deferredContentAware = signal(this._deferredContentAware);

constructor() {
afterRenderEffect(() => {
if (this._deferredContentAware.contentVisible()) {
if (this.deferredContentAware()?.contentVisible()) {
if (this._isRendered) return;
this._viewContainerRef.clear();
this._viewContainerRef.createEmbeddedView(this._templateRef);
this._isRendered = true;
} else if (!this._deferredContentAware.preserveContent()) {
} else if (!this.deferredContentAware()?.preserveContent()) {
this._viewContainerRef.clear();
this._isRendered = false;
}
Expand Down
2 changes: 1 addition & 1 deletion src/aria/tree/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
* found in the LICENSE file at https://angular.dev/license
*/

export {TreeItemGroup, TreeItemGroupContent, Tree, TreeItem} from './tree';
export {TreeItemGroup, Tree, TreeItem} from './tree';
36 changes: 7 additions & 29 deletions src/aria/tree/tree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {ComponentFixture, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {Direction} from '@angular/cdk/bidi';
import {provideFakeDirectionality, runAccessibilityChecks} from '@angular/cdk/testing/private';
import {Tree, TreeItem, TreeItemGroup, TreeItemGroupContent} from './tree';
import {Tree, TreeItem, TreeItemGroup} from './tree';

interface ModifierKeys {
ctrlKey?: boolean;
Expand All @@ -19,7 +19,6 @@ describe('Tree', () => {
let treeElement: HTMLElement;
let treeInstance: Tree<string>;
let treeItemElements: HTMLElement[];
let treeItemGroupElements: HTMLElement[];

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

treeElement = treeDebugElement.nativeElement as HTMLElement;
treeInstance = treeDebugElement.componentInstance as Tree<string>;
treeItemElements = treeItemDebugElements.map(debugEl => debugEl.nativeElement);
treeItemGroupElements = treeItemGroupDebugElements.map(debugEl => debugEl.nativeElement);
}

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

function getTreeItemGroupElementByValue(value: string): HTMLElement | undefined {
return treeItemGroupElements.find(el => el.getAttribute('data-group-for') === String(value));
}

function getFocusedTreeItemValue(): string | undefined {
let item: HTMLElement | undefined;
if (testComponent.focusMode() === 'roving') {
Expand Down Expand Up @@ -187,14 +180,6 @@ describe('Tree', () => {
expect(getTreeItemElementByValue('blueberry')!.getAttribute('role')).toBe('treeitem');
});

it('should correctly set the role attribute to "group" for TreeItemGroup', () => {
expandAll();

expect(getTreeItemGroupElementByValue('fruits')!.getAttribute('role')).toBe('group');
expect(getTreeItemGroupElementByValue('vegetables')!.getAttribute('role')).toBe('group');
expect(getTreeItemGroupElementByValue('berries')!.getAttribute('role')).toBe('group');
});

it('should set aria-orientation to "vertical" by default', () => {
expect(treeElement.getAttribute('aria-orientation')).toBe('vertical');
});
Expand Down Expand Up @@ -262,12 +247,6 @@ describe('Tree', () => {
expect(strawberry.getAttribute('aria-setsize')).toBe('2');
expect(strawberry.getAttribute('aria-posinset')).toBe('1');
});

it('should set aria-owns on expandable items pointing to their group id', () => {
const fruitsItem = getTreeItemElementByValue('fruits')!;
const group = getTreeItemGroupElementByValue('fruits')!;
expect(fruitsItem.getAttribute('aria-owns')).toBe(group!.id);
});
});

describe('custom configuration', () => {
Expand Down Expand Up @@ -1359,12 +1338,11 @@ interface TestTreeNode<V = string> {
>
{{ node.label }}
@if (node.children !== undefined && node.children!.length > 0) {
<ul
ngTreeItemGroup
[ownedBy]="treeItem"
[attr.data-group-for]="node.value"
#group="ngTreeItemGroup">
<ng-template ngTreeItemGroupContent>
<ul role="group">
<ng-template
ngTreeItemGroup
[ownedBy]="treeItem"
#group="ngTreeItemGroup">
@for (node of node.children; track node.value) {
<ng-template [ngTemplateOutlet]="nodeTemplate" [ngTemplateOutletContext]="{ node: node, parent: group }" />
}
Expand All @@ -1374,7 +1352,7 @@ interface TestTreeNode<V = string> {
</li>
</ng-template>
`,
imports: [Tree, TreeItem, TreeItemGroup, TreeItemGroupContent, NgTemplateOutlet],
imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],
})
class TestTreeComponent {
nodes = signal<TestTreeNode[]>([
Expand Down
81 changes: 18 additions & 63 deletions src/aria/tree/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,12 @@ export class Tree<V> {
'[attr.aria-current]': 'pattern.current()',
'[attr.aria-disabled]': 'pattern.disabled()',
'[attr.aria-level]': 'pattern.level()',
'[attr.aria-owns]': 'ownsId()',
'[attr.aria-setsize]': 'pattern.setsize()',
'[attr.aria-posinset]': 'pattern.posinset()',
'[attr.tabindex]': 'pattern.tabindex()',
},
})
export class TreeItem<V> implements OnInit, OnDestroy, HasElement {
export class TreeItem<V> extends DeferredContentAware implements OnInit, OnDestroy, HasElement {
/** A reference to the tree item element. */
private readonly _elementRef = inject(ElementRef);

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

/** The id of the owned group. */
readonly ownsId = computed(() => this._group()?.id);

/** The host native element. */
readonly element = computed(() => this._elementRef.nativeElement);

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

constructor() {
// Updates the visibility of the owned group.
super();
this.preserveContent.set(true);
// Connect the group's hidden state to the DeferredContentAware's visibility.
afterRenderEffect(() => {
this._group()?.visible.set(this.pattern.expanded());
this.tree().pattern instanceof ComboboxTreePattern
? this.contentVisible.set(true)
: this.contentVisible.set(this.pattern.expanded());
});
}

Expand All @@ -289,12 +289,7 @@ export class TreeItem<V> implements OnInit, OnDestroy, HasElement {
id: () => this._id,
tree: treePattern,
parent: parentPattern,
children: computed(
() =>
this._group()
?.children()
.map(item => (item as TreeItem<V>).pattern) ?? [],
),
children: computed(() => this._group()?.children() ?? []),
hasChildren: computed(() => !!this._group()),
});
}
Expand All @@ -314,60 +309,30 @@ export class TreeItem<V> implements OnInit, OnDestroy, HasElement {
}

/**
* Container that designates content as a group.
* Contains children tree itmes.
*/
@Directive({
selector: '[ngTreeItemGroup]',
selector: 'ng-template[ngTreeItemGroup]',
exportAs: 'ngTreeItemGroup',
hostDirectives: [
{
directive: DeferredContentAware,
inputs: ['preserveContent'],
},
],
host: {
'class': 'ng-treeitem-group',
'role': 'group',
'[id]': 'id',
'[attr.inert]': 'visible() ? null : true',
},
hostDirectives: [DeferredContent],
})
export class TreeItemGroup<V> implements OnInit, OnDestroy, HasElement {
/** A reference to the group element. */
private readonly _elementRef = inject(ElementRef);

/** The DeferredContentAware host directive. */
private readonly _deferredContentAware = inject(DeferredContentAware);
export class TreeItemGroup<V> implements OnInit, OnDestroy {
/** The DeferredContent host directive. */
private readonly _deferredContent = inject(DeferredContent);

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

/** The host native element. */
readonly element = computed(() => this._elementRef.nativeElement);

/** Unique ID for the group. */
readonly id = inject(_IdGenerator).getId('ng-tree-group-');

/** Whether the group is visible. */
readonly visible = signal(true);

/** Child items within this group. */
readonly children = computed(() => [...this._unorderedItems()].sort(sortDirectives));
readonly children = computed<TreeItemPattern<V>[]>(() =>
[...this._unorderedItems()].sort(sortDirectives).map(c => c.pattern),
);

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

constructor() {
this._deferredContentAware.preserveContent.set(true);
// Connect the group's hidden state to the DeferredContentAware's visibility.
afterRenderEffect(() => {
this.ownedBy().tree().pattern instanceof ComboboxTreePattern
? this._deferredContentAware.contentVisible.set(true)
: this._deferredContentAware.contentVisible.set(this.visible());
});
}

ngOnInit() {
this._deferredContent.deferredContentAware.set(this.ownedBy());
this.ownedBy().register(this);
}

Expand All @@ -385,13 +350,3 @@ export class TreeItemGroup<V> implements OnInit, OnDestroy, HasElement {
this._unorderedItems.set(new Set(this._unorderedItems()));
}
}

/**
* A structural directive that marks the `ng-template` to be used as the content
* for a `TreeItemGroup`. This content can be lazily loaded.
*/
@Directive({
selector: 'ng-template[ngTreeItemGroupContent]',
hostDirectives: [DeferredContent],
})
export class TreeItemGroupContent {}
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
</li>

@if (node.children) {
<ul ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
<ng-template ngTreeItemGroupContent>
<ul role="group">
<ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
<ng-template
[ngTemplateOutlet]="treeNodes"
[ngTemplateOutletContext]="{nodes: node.children, parent: group}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
ComboboxPopup,
ComboboxPopupContainer,
} from '@angular/aria/combobox';
import {Tree, TreeItem, TreeItemGroup, TreeItemGroupContent} from '@angular/aria/tree';
import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';
import {
afterRenderEffect,
ChangeDetectionStrategy,
Expand All @@ -38,7 +38,6 @@ import {NgTemplateOutlet} from '@angular/common';
Tree,
TreeItem,
TreeItemGroup,
TreeItemGroupContent,
NgTemplateOutlet,
],
changeDetection: ChangeDetectionStrategy.OnPush,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
</li>

@if (node.children) {
<ul ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
<ng-template ngTreeItemGroupContent>
<ul role="group">
<ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
<ng-template
[ngTemplateOutlet]="treeNodes"
[ngTemplateOutletContext]="{nodes: node.children, parent: group}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
ComboboxPopup,
ComboboxPopupContainer,
} from '@angular/aria/combobox';
import {Tree, TreeItem, TreeItemGroup, TreeItemGroupContent} from '@angular/aria/tree';
import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';
import {
afterRenderEffect,
ChangeDetectionStrategy,
Expand All @@ -38,7 +38,6 @@ import {NgTemplateOutlet} from '@angular/common';
Tree,
TreeItem,
TreeItemGroup,
TreeItemGroupContent,
NgTemplateOutlet,
],
changeDetection: ChangeDetectionStrategy.OnPush,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
</li>

@if (node.children) {
<ul ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
<ng-template ngTreeItemGroupContent>
<ul role="group">
<ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
<ng-template
[ngTemplateOutlet]="treeNodes"
[ngTemplateOutletContext]="{nodes: node.children, parent: group}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
ComboboxPopup,
ComboboxPopupContainer,
} from '@angular/aria/combobox';
import {Tree, TreeItem, TreeItemGroup, TreeItemGroupContent} from '@angular/aria/tree';
import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';
import {
afterRenderEffect,
ChangeDetectionStrategy,
Expand All @@ -38,7 +38,6 @@ import {NgTemplateOutlet} from '@angular/common';
Tree,
TreeItem,
TreeItemGroup,
TreeItemGroupContent,
NgTemplateOutlet,
],
changeDetection: ChangeDetectionStrategy.OnPush,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
</li>

@if (node.children) {
<ul ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
<ng-template ngTreeItemGroupContent>
<ul role="group">
<ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
<ng-template
[ngTemplateOutlet]="treeNodes"
[ngTemplateOutletContext]="{nodes: node.children, parent: group}"
Expand Down
Loading
Loading