Skip to content

Commit 7dc8c1a

Browse files
committed
feat(cdk-experimental/tree): add nav mode
1 parent 7bb0a82 commit 7dc8c1a

File tree

7 files changed

+192
-4
lines changed

7 files changed

+192
-4
lines changed

src/cdk-experimental/tree/tree.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ describe('CdkTree', () => {
8888
skipDisabled?: boolean;
8989
focusMode?: 'roving' | 'activedescendant';
9090
selectionMode?: 'follow' | 'explicit';
91+
nav?: boolean;
92+
currentType?: 'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false';
9193
} = {},
9294
) {
9395
if (config.nodes !== undefined) testComponent.nodes.set(config.nodes);
@@ -99,6 +101,8 @@ describe('CdkTree', () => {
99101
if (config.skipDisabled !== undefined) testComponent.skipDisabled.set(config.skipDisabled);
100102
if (config.focusMode !== undefined) testComponent.focusMode.set(config.focusMode);
101103
if (config.selectionMode !== undefined) testComponent.selectionMode.set(config.selectionMode);
104+
if (config.nav !== undefined) testComponent.nav.set(config.nav);
105+
if (config.currentType !== undefined) testComponent.currentType.set(config.currentType);
102106

103107
fixture.detectChanges();
104108
defineTestVariables();
@@ -305,6 +309,27 @@ describe('CdkTree', () => {
305309
const fruitsItem = getTreeItemElementByValue('fruits')!;
306310
expect(fruitsItem.getAttribute('aria-expanded')).toBe('true');
307311
});
312+
313+
it('should set aria-current to specific current type when nav="true"', () => {
314+
updateTree({nav: true, value: ['apple']});
315+
316+
const appleItem = getTreeItemElementByValue('apple')!;
317+
const bananaItem = getTreeItemElementByValue('banana')!;
318+
expect(appleItem.getAttribute('aria-current')).toBe('page');
319+
expect(bananaItem.hasAttribute('aria-current')).toBe(false);
320+
321+
updateTree({currentType: 'location'});
322+
expect(appleItem.getAttribute('aria-current')).toBe('location');
323+
});
324+
325+
it('should not set aria-selected when nav="true"', () => {
326+
updateTree({value: ['apple'], nav: true});
327+
const appleItem = getTreeItemElementByValue('apple')!;
328+
expect(appleItem.hasAttribute('aria-selected')).toBe(false);
329+
330+
updateTree({nav: false});
331+
expect(appleItem.getAttribute('aria-selected')).toBe('true');
332+
});
308333
});
309334

310335
describe('roving focus mode (focusMode="roving")', () => {
@@ -1310,6 +1335,8 @@ interface TestTreeNode<V = string> {
13101335
[orientation]="orientation()"
13111336
[disabled]="disabled()"
13121337
[(value)]="value"
1338+
[nav]="nav()"
1339+
[currentType]="currentType()"
13131340
>
13141341
@for (node of nodes(); track node.value) {
13151342
<li
@@ -1405,4 +1432,6 @@ class TestTreeComponent {
14051432
skipDisabled = signal(true);
14061433
focusMode = signal<'roving' | 'activedescendant'>('roving');
14071434
selectionMode = signal<'explicit' | 'follow'>('explicit');
1435+
nav = signal(false);
1436+
currentType = signal('page');
14081437
}

src/cdk-experimental/tree/tree.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@ export class CdkTree<V> {
112112
/** Text direction. */
113113
readonly textDirection = inject(Directionality).valueSignal;
114114

115+
/** Whether the tree is in navigation mode. */
116+
readonly nav = input(false);
117+
118+
/** The aria-current type. */
119+
readonly currentType = input<'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false'>(
120+
'page',
121+
);
122+
115123
/** The UI pattern for the tree. */
116124
readonly pattern: TreePattern<V> = new TreePattern<V>({
117125
...this,
@@ -174,6 +182,7 @@ export class CdkTree<V> {
174182
'[id]': 'pattern.id()',
175183
'[attr.aria-expanded]': 'pattern.expandable() ? pattern.expanded() : null',
176184
'[attr.aria-selected]': 'pattern.selected()',
185+
'[attr.aria-current]': 'pattern.current()',
177186
'[attr.aria-disabled]': 'pattern.disabled()',
178187
'[attr.aria-level]': 'pattern.level()',
179188
'[attr.aria-owns]': 'group()?.id',

src/cdk-experimental/ui-patterns/tree/tree.spec.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ describe('Tree Pattern', () => {
146146
typeaheadDelay: signal(0),
147147
value: signal([]),
148148
wrap: signal(false),
149+
nav: signal(false),
150+
currentType: signal('page'),
149151
};
150152
});
151153

@@ -179,6 +181,50 @@ describe('Tree Pattern', () => {
179181
expect(item0_0.posinset()).toBe(1);
180182
expect(item0_1.posinset()).toBe(2);
181183
});
184+
185+
describe('nav mode', () => {
186+
let treeInputs: TestTreeInputs<string>;
187+
188+
beforeEach(() => {
189+
treeInputs = {
190+
activeIndex: signal(0),
191+
disabled: signal(false),
192+
focusMode: signal('roving'),
193+
multi: signal(false),
194+
orientation: signal('vertical'),
195+
selectionMode: signal('follow'),
196+
skipDisabled: signal(true),
197+
textDirection: signal('ltr'),
198+
typeaheadDelay: signal(0),
199+
value: signal([]),
200+
wrap: signal(false),
201+
nav: signal(true),
202+
currentType: signal('page'),
203+
};
204+
});
205+
206+
it('should have undefined selected state', () => {
207+
const {allItems} = createTree(treeExample, treeInputs);
208+
const item0 = getItemByValue(allItems(), 'Item 0');
209+
treeInputs.value.set(['Item 0']);
210+
expect(item0.selected()).toBeUndefined();
211+
});
212+
213+
it('should correctly compute current state', () => {
214+
const {allItems} = createTree(treeExample, treeInputs);
215+
const item0 = getItemByValue(allItems(), 'Item 0');
216+
const item1 = getItemByValue(allItems(), 'Item 1');
217+
218+
treeInputs.value.set(['Item 0']);
219+
expect(item0.current()).toBe('page');
220+
expect(item1.current()).toBeUndefined();
221+
222+
treeInputs.value.set(['Item 1']);
223+
treeInputs.currentType.set('step');
224+
expect(item0.current()).toBeUndefined();
225+
expect(item1.current()).toBe('step');
226+
});
227+
});
182228
});
183229

184230
describe('Keyboard Navigation', () => {
@@ -197,6 +243,8 @@ describe('Tree Pattern', () => {
197243
typeaheadDelay: signal(0),
198244
value: signal([]),
199245
wrap: signal(false),
246+
nav: signal(false),
247+
currentType: signal('page'),
200248
};
201249
});
202250

@@ -379,6 +427,8 @@ describe('Tree Pattern', () => {
379427
typeaheadDelay: signal(0),
380428
value: signal([]),
381429
wrap: signal(false),
430+
nav: signal(false),
431+
currentType: signal('page'),
382432
};
383433
});
384434

@@ -435,6 +485,8 @@ describe('Tree Pattern', () => {
435485
typeaheadDelay: signal(0),
436486
value: signal([]),
437487
wrap: signal(false),
488+
nav: signal(false),
489+
currentType: signal('page'),
438490
};
439491
});
440492

@@ -497,6 +549,8 @@ describe('Tree Pattern', () => {
497549
typeaheadDelay: signal(0),
498550
value: signal([]),
499551
wrap: signal(false),
552+
nav: signal(false),
553+
currentType: signal('page'),
500554
};
501555
});
502556

@@ -653,6 +707,8 @@ describe('Tree Pattern', () => {
653707
typeaheadDelay: signal(0),
654708
value: signal([]),
655709
wrap: signal(false),
710+
nav: signal(false),
711+
currentType: signal('page'),
656712
};
657713
});
658714

@@ -801,6 +857,8 @@ describe('Tree Pattern', () => {
801857
typeaheadDelay: signal(0),
802858
value: signal([]),
803859
wrap: signal(false),
860+
nav: signal(false),
861+
currentType: signal('page'),
804862
};
805863
});
806864

@@ -839,6 +897,8 @@ describe('Tree Pattern', () => {
839897
typeaheadDelay: signal(0),
840898
value: signal([]),
841899
wrap: signal(false),
900+
nav: signal(false),
901+
currentType: signal('page'),
842902
};
843903
});
844904

@@ -881,6 +941,8 @@ describe('Tree Pattern', () => {
881941
typeaheadDelay: signal(0),
882942
value: signal([]),
883943
wrap: signal(false),
944+
nav: signal(false),
945+
currentType: signal('page'),
884946
};
885947
});
886948

@@ -927,6 +989,8 @@ describe('Tree Pattern', () => {
927989
typeaheadDelay: signal(0),
928990
value: signal([]),
929991
wrap: signal(false),
992+
nav: signal(false),
993+
currentType: signal('page'),
930994
};
931995
});
932996

@@ -1007,6 +1071,8 @@ describe('Tree Pattern', () => {
10071071
typeaheadDelay: signal(0),
10081072
value: signal([]),
10091073
wrap: signal(false),
1074+
nav: signal(false),
1075+
currentType: signal('page'),
10101076
};
10111077
});
10121078

@@ -1167,6 +1233,8 @@ describe('Tree Pattern', () => {
11671233
typeaheadDelay: signal(0),
11681234
value: signal([]),
11691235
wrap: signal(false),
1236+
nav: signal(false),
1237+
currentType: signal('page'),
11701238
};
11711239
});
11721240

src/cdk-experimental/ui-patterns/tree/tree.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,20 @@ export class TreeItemPattern<V> implements ExpansionItem {
8787
readonly tabindex = computed(() => this.tree().focusManager.getItemTabindex(this));
8888

8989
/** Whether the item is selected. */
90-
readonly selected = computed(() => this.tree().value().includes(this.value()));
90+
readonly selected = computed(() => {
91+
if (this.tree().nav()) {
92+
return undefined;
93+
}
94+
return this.tree().value().includes(this.value());
95+
});
96+
97+
/** The current type of this item. */
98+
readonly current = computed(() => {
99+
if (!this.tree().nav()) {
100+
return undefined;
101+
}
102+
return this.tree().value().includes(this.value()) ? this.tree().currentType() : undefined;
103+
});
91104

92105
constructor(readonly inputs: TreeItemInputs<V>) {
93106
this.id = inputs.id;
@@ -136,6 +149,12 @@ export interface TreeInputs<V>
136149
> {
137150
/** All items in the tree, in document order (DFS-like, a flattened list). */
138151
allItems: SignalLike<TreeItemPattern<V>[]>;
152+
153+
/** Whether the tree is in navigation mode. */
154+
nav: SignalLike<boolean>;
155+
156+
/** The aria-current type. */
157+
currentType: SignalLike<'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false'>;
139158
}
140159

141160
export interface TreePattern<V> extends TreeInputs<V> {}
@@ -337,6 +356,8 @@ export class TreePattern<V> {
337356
});
338357

339358
constructor(readonly inputs: TreeInputs<V>) {
359+
this.nav = inputs.nav;
360+
this.currentType = inputs.currentType;
340361
this.allItems = inputs.allItems;
341362
this.focusMode = inputs.focusMode;
342363
this.disabled = inputs.disabled;
@@ -345,7 +366,7 @@ export class TreePattern<V> {
345366
this.wrap = inputs.wrap;
346367
this.orientation = inputs.orientation;
347368
this.textDirection = inputs.textDirection;
348-
this.multi = inputs.multi;
369+
this.multi = computed(() => (this.nav() ? false : this.inputs.multi()));
349370
this.value = inputs.value;
350371
this.selectionMode = inputs.selectionMode;
351372
this.typeaheadDelay = inputs.typeaheadDelay;

src/components-examples/cdk-experimental/tree/cdk-tree/cdk-tree-example.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@
3737
color: var(--mat-sys-on-surface-variant);
3838
}
3939

40+
.example-tree-item-content[aria-current] {
41+
background-color: var(--mat-sys-inverse-primary);
42+
}
43+
44+
.example-tree-item-content[aria-disabled='true'] {
45+
background-color: var(--mat-sys-surface-container);
46+
color: var(--mat-sys-on-surface-variant);
47+
}
48+
49+
4050
.example-tree-item-content {
4151
display: flex;
4252
align-items: center;

src/components-examples/cdk-experimental/tree/cdk-tree/cdk-tree-example.html

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<mat-checkbox [formControl]="multi">Multi</mat-checkbox>
44
<mat-checkbox [formControl]="disabled">Disabled</mat-checkbox>
55
<mat-checkbox [formControl]="skipDisabled">Skip Disabled</mat-checkbox>
6+
<mat-checkbox [formControl]="nav">Nav Mode</mat-checkbox>
67

78
<mat-form-field subscriptSizing="dynamic" appearance="outline">
89
<mat-label>Orientation</mat-label>
@@ -43,10 +44,17 @@
4344
[focusMode]="focusMode"
4445
[wrap]="wrap.value"
4546
[skipDisabled]="skipDisabled.value"
47+
[nav]="nav.value"
4648
[(value)]="selectedValues"
4749
#tree="cdkTree"
4850
>
49-
@for (node of treeData; track node) {
50-
<example-node [node]="node" />
51+
@if (nav.value) {
52+
@for (node of treeData; track node) {
53+
<example-nav-node [node]="node" />
54+
}
55+
} @else {
56+
@for (node of treeData; track node) {
57+
<example-node [node]="node" />
58+
}
5159
}
5260
</ul>

0 commit comments

Comments
 (0)