Skip to content

feat(cdk-experimental/tree): add nav mode #31460

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/cdk-experimental/tree/tree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ describe('CdkTree', () => {
skipDisabled?: boolean;
focusMode?: 'roving' | 'activedescendant';
selectionMode?: 'follow' | 'explicit';
nav?: boolean;
currentType?: 'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false';
} = {},
) {
if (config.nodes !== undefined) testComponent.nodes.set(config.nodes);
Expand All @@ -99,6 +101,8 @@ describe('CdkTree', () => {
if (config.skipDisabled !== undefined) testComponent.skipDisabled.set(config.skipDisabled);
if (config.focusMode !== undefined) testComponent.focusMode.set(config.focusMode);
if (config.selectionMode !== undefined) testComponent.selectionMode.set(config.selectionMode);
if (config.nav !== undefined) testComponent.nav.set(config.nav);
if (config.currentType !== undefined) testComponent.currentType.set(config.currentType);

fixture.detectChanges();
defineTestVariables();
Expand Down Expand Up @@ -305,6 +309,27 @@ describe('CdkTree', () => {
const fruitsItem = getTreeItemElementByValue('fruits')!;
expect(fruitsItem.getAttribute('aria-expanded')).toBe('true');
});

it('should set aria-current to specific current type when nav="true"', () => {
updateTree({nav: true, value: ['apple']});

const appleItem = getTreeItemElementByValue('apple')!;
const bananaItem = getTreeItemElementByValue('banana')!;
expect(appleItem.getAttribute('aria-current')).toBe('page');
expect(bananaItem.hasAttribute('aria-current')).toBe(false);

updateTree({currentType: 'location'});
expect(appleItem.getAttribute('aria-current')).toBe('location');
});

it('should not set aria-selected when nav="true"', () => {
updateTree({value: ['apple'], nav: true});
const appleItem = getTreeItemElementByValue('apple')!;
expect(appleItem.hasAttribute('aria-selected')).toBe(false);

updateTree({nav: false});
expect(appleItem.getAttribute('aria-selected')).toBe('true');
});
});

describe('roving focus mode (focusMode="roving")', () => {
Expand Down Expand Up @@ -1310,6 +1335,8 @@ interface TestTreeNode<V = string> {
[orientation]="orientation()"
[disabled]="disabled()"
[(value)]="value"
[nav]="nav()"
[currentType]="currentType()"
>
@for (node of nodes(); track node.value) {
<li
Expand Down Expand Up @@ -1405,4 +1432,6 @@ class TestTreeComponent {
skipDisabled = signal(true);
focusMode = signal<'roving' | 'activedescendant'>('roving');
selectionMode = signal<'explicit' | 'follow'>('explicit');
nav = signal(false);
currentType = signal('page');
}
9 changes: 9 additions & 0 deletions src/cdk-experimental/tree/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ export class CdkTree<V> {
/** Text direction. */
readonly textDirection = inject(Directionality).valueSignal;

/** Whether the tree is in navigation mode. */
readonly nav = input(false);

/** The aria-current type. */
readonly currentType = input<'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false'>(
'page',
);

/** The UI pattern for the tree. */
readonly pattern: TreePattern<V> = new TreePattern<V>({
...this,
Expand Down Expand Up @@ -174,6 +182,7 @@ export class CdkTree<V> {
'[id]': 'pattern.id()',
'[attr.aria-expanded]': 'pattern.expandable() ? pattern.expanded() : null',
'[attr.aria-selected]': 'pattern.selected()',
'[attr.aria-current]': 'pattern.current()',
'[attr.aria-disabled]': 'pattern.disabled()',
'[attr.aria-level]': 'pattern.level()',
'[attr.aria-owns]': 'group()?.id',
Expand Down
68 changes: 68 additions & 0 deletions src/cdk-experimental/ui-patterns/tree/tree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ describe('Tree Pattern', () => {
typeaheadDelay: signal(0),
value: signal([]),
wrap: signal(false),
nav: signal(false),
currentType: signal('page'),
};
});

Expand Down Expand Up @@ -179,6 +181,50 @@ describe('Tree Pattern', () => {
expect(item0_0.posinset()).toBe(1);
expect(item0_1.posinset()).toBe(2);
});

describe('nav mode', () => {
let treeInputs: TestTreeInputs<string>;

beforeEach(() => {
treeInputs = {
activeIndex: signal(0),
disabled: signal(false),
focusMode: signal('roving'),
multi: signal(false),
orientation: signal('vertical'),
selectionMode: signal('follow'),
skipDisabled: signal(true),
textDirection: signal('ltr'),
typeaheadDelay: signal(0),
value: signal([]),
wrap: signal(false),
nav: signal(true),
currentType: signal('page'),
};
});

it('should have undefined selected state', () => {
const {allItems} = createTree(treeExample, treeInputs);
const item0 = getItemByValue(allItems(), 'Item 0');
treeInputs.value.set(['Item 0']);
expect(item0.selected()).toBeUndefined();
});

it('should correctly compute current state', () => {
const {allItems} = createTree(treeExample, treeInputs);
const item0 = getItemByValue(allItems(), 'Item 0');
const item1 = getItemByValue(allItems(), 'Item 1');

treeInputs.value.set(['Item 0']);
expect(item0.current()).toBe('page');
expect(item1.current()).toBeUndefined();

treeInputs.value.set(['Item 1']);
treeInputs.currentType.set('step');
expect(item0.current()).toBeUndefined();
expect(item1.current()).toBe('step');
});
});
});

describe('Keyboard Navigation', () => {
Expand All @@ -197,6 +243,8 @@ describe('Tree Pattern', () => {
typeaheadDelay: signal(0),
value: signal([]),
wrap: signal(false),
nav: signal(false),
currentType: signal('page'),
};
});

Expand Down Expand Up @@ -379,6 +427,8 @@ describe('Tree Pattern', () => {
typeaheadDelay: signal(0),
value: signal([]),
wrap: signal(false),
nav: signal(false),
currentType: signal('page'),
};
});

Expand Down Expand Up @@ -435,6 +485,8 @@ describe('Tree Pattern', () => {
typeaheadDelay: signal(0),
value: signal([]),
wrap: signal(false),
nav: signal(false),
currentType: signal('page'),
};
});

Expand Down Expand Up @@ -497,6 +549,8 @@ describe('Tree Pattern', () => {
typeaheadDelay: signal(0),
value: signal([]),
wrap: signal(false),
nav: signal(false),
currentType: signal('page'),
};
});

Expand Down Expand Up @@ -653,6 +707,8 @@ describe('Tree Pattern', () => {
typeaheadDelay: signal(0),
value: signal([]),
wrap: signal(false),
nav: signal(false),
currentType: signal('page'),
};
});

Expand Down Expand Up @@ -801,6 +857,8 @@ describe('Tree Pattern', () => {
typeaheadDelay: signal(0),
value: signal([]),
wrap: signal(false),
nav: signal(false),
currentType: signal('page'),
};
});

Expand Down Expand Up @@ -839,6 +897,8 @@ describe('Tree Pattern', () => {
typeaheadDelay: signal(0),
value: signal([]),
wrap: signal(false),
nav: signal(false),
currentType: signal('page'),
};
});

Expand Down Expand Up @@ -881,6 +941,8 @@ describe('Tree Pattern', () => {
typeaheadDelay: signal(0),
value: signal([]),
wrap: signal(false),
nav: signal(false),
currentType: signal('page'),
};
});

Expand Down Expand Up @@ -927,6 +989,8 @@ describe('Tree Pattern', () => {
typeaheadDelay: signal(0),
value: signal([]),
wrap: signal(false),
nav: signal(false),
currentType: signal('page'),
};
});

Expand Down Expand Up @@ -1007,6 +1071,8 @@ describe('Tree Pattern', () => {
typeaheadDelay: signal(0),
value: signal([]),
wrap: signal(false),
nav: signal(false),
currentType: signal('page'),
};
});

Expand Down Expand Up @@ -1167,6 +1233,8 @@ describe('Tree Pattern', () => {
typeaheadDelay: signal(0),
value: signal([]),
wrap: signal(false),
nav: signal(false),
currentType: signal('page'),
};
});

Expand Down
25 changes: 23 additions & 2 deletions src/cdk-experimental/ui-patterns/tree/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,20 @@ export class TreeItemPattern<V> implements ExpansionItem {
readonly tabindex = computed(() => this.tree().focusManager.getItemTabindex(this));

/** Whether the item is selected. */
readonly selected = computed(() => this.tree().value().includes(this.value()));
readonly selected = computed(() => {
if (this.tree().nav()) {
return undefined;
}
return this.tree().value().includes(this.value());
});

/** The current type of this item. */
readonly current = computed(() => {
if (!this.tree().nav()) {
return undefined;
}
return this.tree().value().includes(this.value()) ? this.tree().currentType() : undefined;
});

constructor(readonly inputs: TreeItemInputs<V>) {
this.id = inputs.id;
Expand Down Expand Up @@ -136,6 +149,12 @@ export interface TreeInputs<V>
> {
/** All items in the tree, in document order (DFS-like, a flattened list). */
allItems: SignalLike<TreeItemPattern<V>[]>;

/** Whether the tree is in navigation mode. */
nav: SignalLike<boolean>;

/** The aria-current type. */
currentType: SignalLike<'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false'>;
}

export interface TreePattern<V> extends TreeInputs<V> {}
Expand Down Expand Up @@ -337,6 +356,8 @@ export class TreePattern<V> {
});

constructor(readonly inputs: TreeInputs<V>) {
this.nav = inputs.nav;
this.currentType = inputs.currentType;
this.allItems = inputs.allItems;
this.focusMode = inputs.focusMode;
this.disabled = inputs.disabled;
Expand All @@ -345,7 +366,7 @@ export class TreePattern<V> {
this.wrap = inputs.wrap;
this.orientation = inputs.orientation;
this.textDirection = inputs.textDirection;
this.multi = inputs.multi;
this.multi = computed(() => (this.nav() ? false : this.inputs.multi()));
this.value = inputs.value;
this.selectionMode = inputs.selectionMode;
this.typeaheadDelay = inputs.typeaheadDelay;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@
color: var(--mat-sys-on-surface-variant);
}

.example-tree-item-content[aria-current] {
background-color: var(--mat-sys-inverse-primary);
}

.example-tree-item-content[aria-disabled='true'] {
background-color: var(--mat-sys-surface-container);
color: var(--mat-sys-on-surface-variant);
}


.example-tree-item-content {
display: flex;
align-items: center;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<mat-checkbox [formControl]="multi">Multi</mat-checkbox>
<mat-checkbox [formControl]="disabled">Disabled</mat-checkbox>
<mat-checkbox [formControl]="skipDisabled">Skip Disabled</mat-checkbox>
<mat-checkbox [formControl]="nav">Nav Mode</mat-checkbox>

<mat-form-field subscriptSizing="dynamic" appearance="outline">
<mat-label>Orientation</mat-label>
Expand Down Expand Up @@ -43,10 +44,17 @@
[focusMode]="focusMode"
[wrap]="wrap.value"
[skipDisabled]="skipDisabled.value"
[nav]="nav.value"
[(value)]="selectedValues"
#tree="cdkTree"
>
@for (node of treeData; track node) {
<example-node [node]="node" />
@if (nav.value) {
@for (node of treeData; track node) {
<example-nav-node [node]="node" />
}
} @else {
@for (node of treeData; track node) {
<example-node [node]="node" />
}
}
</ul>
Loading
Loading