Skip to content
Draft
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
9 changes: 4 additions & 5 deletions goldens/aria/tree/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,28 @@ import { OnInit } from '@angular/core';
import { Signal } from '@angular/core';

// @public
export class Tree<V> {
export class Tree<V> implements OnDestroy {
constructor();
readonly activeDescendant: Signal<string | undefined>;
readonly _collection: SortedCollection<TreeItem<V>>;
readonly currentType: _angular_core.InputSignal<"page" | "step" | "location" | "date" | "time" | "true" | "false">;
readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
readonly element: HTMLElement;
readonly focusMode: _angular_core.InputSignal<"roving" | "activedescendant">;
readonly id: _angular_core.InputSignal<string>;
readonly multi: _angular_core.InputSignalWithTransform<boolean, unknown>;
readonly nav: _angular_core.InputSignalWithTransform<boolean, unknown>;
// (undocumented)
ngOnDestroy(): void;
readonly orientation: _angular_core.InputSignal<"vertical" | "horizontal">;
readonly _pattern: TreePattern<V>;
// (undocumented)
_register(child: TreeItem<V>): void;
// (undocumented)
scrollActiveItemIntoView(options?: ScrollIntoViewOptions): void;
readonly selectionMode: _angular_core.InputSignal<"follow" | "explicit">;
readonly softDisabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
readonly tabIndex: _angular_core.InputSignalWithTransform<number | undefined, string | number | undefined>;
readonly textDirection: _angular_core.WritableSignal<_angular_cdk_bidi.Direction>;
readonly typeaheadDelay: _angular_core.InputSignal<number>;
// (undocumented)
_unregister(child: TreeItem<V>): void;
readonly value: _angular_core.ModelSignal<V[]>;
readonly wrap: _angular_core.InputSignalWithTransform<boolean, unknown>;
// (undocumented)
Expand Down
1 change: 1 addition & 0 deletions src/aria/tree/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ ng_project(
"//:node_modules/@angular/common",
"//:node_modules/@angular/core",
"//:node_modules/@angular/platform-browser",
"//src/aria/private/testing",
"//src/cdk/testing/private",
],
)
Expand Down
12 changes: 8 additions & 4 deletions src/aria/tree/tree-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,10 @@ export class TreeItem<V> extends DeferredContentAware implements OnInit, OnDestr
}

ngOnInit() {
this.parent()._register(this);
this.tree()._register(this);
if (this.parent() instanceof TreeItemGroup) {
(this.parent() as TreeItemGroup<V>)._register(this);
}
this.tree()._collection.register(this);

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

ngOnDestroy() {
this.parent()._unregister(this);
this.tree()._unregister(this);
if (this.parent() instanceof TreeItemGroup) {
(this.parent() as TreeItemGroup<V>)._unregister(this);
}
this.tree()._collection.unregister(this);
}

_register(group: TreeItemGroup<V>) {
Expand Down
26 changes: 26 additions & 0 deletions src/aria/tree/tree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {provideFakeDirectionality, runAccessibilityChecks} from '@angular/cdk/te
import {Tree} from './tree';
import {TreeItem} from './tree-item';
import {TreeItemGroup} from './tree-item-group';
import {waitForMicrotasks} from '../private/testing/test-helpers';

interface ModifierKeys {
ctrlKey?: boolean;
Expand Down Expand Up @@ -165,6 +166,31 @@ describe('Tree', () => {
await runAccessibilityChecks(fixture.nativeElement);
});

describe('dynamic updates', () => {
it('should update item order correctly after items are shuffled', async () => {
setupTestTree();
expandAll();
fixture.detectChanges();

const treeDirective = fixture.debugElement.query(By.directive(Tree)).injector.get(Tree);
const itemsBefore = treeDirective._pattern.inputs.items();
expect(itemsBefore.length).toBe(11);
expect(itemsBefore[0].value()).toBe('fruits');

// Shuffle top-level nodes: move fruits to end
const nodes = testComponent.nodes();
const firstNode = nodes.shift()!;
nodes.push(firstNode);
testComponent.nodes.set([...nodes]);
fixture.detectChanges();
await waitForMicrotasks();

const itemsAfter = treeDirective._pattern.inputs.items();
expect(itemsAfter.length).toBe(11);
expect(itemsAfter[0].value()).toBe('vegetables');
});
});

describe('ARIA attributes and roles', () => {
describe('default configuration', () => {
beforeEach(() => {
Expand Down
34 changes: 16 additions & 18 deletions src/aria/tree/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,24 @@
*/

import {
Directive,
ElementRef,
afterNextRender,
afterRenderEffect,
booleanAttribute,
computed,
Directive,
ElementRef,
inject,
input,
model,
numberAttribute,
OnDestroy,
signal,
Signal,
untracked,
} from '@angular/core';
import {_IdGenerator} from '@angular/cdk/a11y';
import {Directionality} from '@angular/cdk/bidi';
import {ComboboxTreePattern, TreeItemPattern, TreePattern, sortDirectives} from '../private';
import {ComboboxTreePattern, TreeItemPattern, TreePattern, SortedCollection} from '../private';
import {ComboboxPopup} from '../combobox';
import type {TreeItem} from './tree-item';

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

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

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

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

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

afterNextRender(() => {
this._collection.startObserving(this.element);
});

if (this._popup?.combobox) {
this._popup?._controls?.set(this._pattern as ComboboxTreePattern<V>);
}

// Check for any violationns after the DOM has been updated.
// Check for any violations after the DOM has been updated.
afterRenderEffect({
read: () => {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
Expand Down Expand Up @@ -223,14 +227,8 @@ export class Tree<V> {
});
}

_register(child: TreeItem<V>) {
this._unorderedItems().add(child);
this._unorderedItems.set(new Set(this._unorderedItems()));
}

_unregister(child: TreeItem<V>) {
this._unorderedItems().delete(child);
this._unorderedItems.set(new Set(this._unorderedItems()));
ngOnDestroy() {
this._collection.stopObserving();
}

scrollActiveItemIntoView(options: ScrollIntoViewOptions = {block: 'nearest'}) {
Expand Down
Loading