Skip to content

Commit df0d753

Browse files
authored
fix(cdk-experimental/ui-patterns): Tree expand/collapse key should work in follow focus mode (angular#31747)
1 parent 605e2c9 commit df0d753

File tree

2 files changed

+90
-33
lines changed

2 files changed

+90
-33
lines changed

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,74 @@ describe('Tree Pattern', () => {
12161216
tree.onPointerdown(createClickEvent(item0.element()));
12171217
expect(item0.expanded()).toBe(false);
12181218
});
1219+
1220+
describe('follows focus & single select', () => {
1221+
beforeEach(() => {
1222+
treeInputs.selectionMode.set('follow');
1223+
treeInputs.multi.set(false);
1224+
});
1225+
1226+
it('should navigate and select the first child on expandKey if expanded and has children (vertical)', () => {
1227+
treeInputs.orientation.set('vertical');
1228+
const {tree, allItems} = createTree(treeExample, treeInputs);
1229+
const item0 = getItemByValue(allItems(), 'Item 0');
1230+
const item0_0 = getItemByValue(allItems(), 'Item 0-0');
1231+
tree.listBehavior.goto(item0);
1232+
item0.expansion.open();
1233+
1234+
tree.onKeydown(right());
1235+
expect(tree.activeItem()).toBe(item0_0);
1236+
expect(tree.inputs.value()).toEqual(['Item 0-0']);
1237+
});
1238+
1239+
it('should navigate and select the parent on collapseKey if collapsed (vertical)', () => {
1240+
treeInputs.orientation.set('vertical');
1241+
const {tree, allItems} = createTree(treeExample, treeInputs);
1242+
const item0 = getItemByValue(allItems(), 'Item 0');
1243+
const item0_0 = getItemByValue(allItems(), 'Item 0-0');
1244+
item0.expansion.open();
1245+
tree.listBehavior.goto(item0_0);
1246+
1247+
tree.onKeydown(left());
1248+
expect(tree.activeItem()).toBe(item0);
1249+
expect(tree.inputs.value()).toEqual(['Item 0']);
1250+
});
1251+
});
1252+
1253+
describe('follows focus & multi select', () => {
1254+
beforeEach(() => {
1255+
treeInputs.selectionMode.set('follow');
1256+
treeInputs.multi.set(true);
1257+
});
1258+
1259+
it('should navigate without select the first child on Ctrl + expandKey if expanded and has children (vertical)', () => {
1260+
treeInputs.orientation.set('vertical');
1261+
const {tree, allItems} = createTree(treeExample, treeInputs);
1262+
const item0 = getItemByValue(allItems(), 'Item 0');
1263+
const item0_0 = getItemByValue(allItems(), 'Item 0-0');
1264+
tree.listBehavior.goto(item0);
1265+
item0.expansion.open();
1266+
tree.inputs.value.set(['Item 1']); // pre-select something else
1267+
1268+
tree.onKeydown(right({control: true}));
1269+
expect(tree.activeItem()).toBe(item0_0);
1270+
expect(tree.inputs.value()).toEqual(['Item 1']);
1271+
});
1272+
1273+
it('should navigate without select the parent on Ctrl + collapseKey if collapsed (vertical)', () => {
1274+
treeInputs.orientation.set('vertical');
1275+
const {tree, allItems} = createTree(treeExample, treeInputs);
1276+
const item0 = getItemByValue(allItems(), 'Item 0');
1277+
const item0_0 = getItemByValue(allItems(), 'Item 0-0');
1278+
item0.expansion.open();
1279+
tree.listBehavior.goto(item0_0);
1280+
tree.inputs.value.set(['Item 1']); // pre-select something else
1281+
1282+
tree.onKeydown(left({control: true}));
1283+
expect(tree.activeItem()).toBe(item0);
1284+
expect(tree.inputs.value()).toEqual(['Item 1']);
1285+
});
1286+
});
12191287
});
12201288

12211289
describe('#setDefaultState', () => {

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

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -207,23 +207,15 @@ export class TreePattern<V> {
207207
const manager = new KeyboardEventManager();
208208
const list = this.listBehavior;
209209

210-
if (!this.followFocus()) {
211-
manager
212-
.on(this.prevKey, () => list.prev())
213-
.on(this.nextKey, () => list.next())
214-
.on('Home', () => list.first())
215-
.on('End', () => list.last())
216-
.on(this.typeaheadRegexp, e => list.search(e.key));
217-
}
218-
219-
if (this.followFocus()) {
220-
manager
221-
.on(this.prevKey, () => list.prev({selectOne: true}))
222-
.on(this.nextKey, () => list.next({selectOne: true}))
223-
.on('Home', () => list.first({selectOne: true}))
224-
.on('End', () => list.last({selectOne: true}))
225-
.on(this.typeaheadRegexp, e => list.search(e.key, {selectOne: true}));
226-
}
210+
manager
211+
.on(this.prevKey, () => list.prev({selectOne: this.followFocus()}))
212+
.on(this.nextKey, () => list.next({selectOne: this.followFocus()}))
213+
.on('Home', () => list.first({selectOne: this.followFocus()}))
214+
.on('End', () => list.last({selectOne: this.followFocus()}))
215+
.on(this.typeaheadRegexp, e => list.search(e.key, {selectOne: this.followFocus()}))
216+
.on(this.expandKey, () => this.expand({selectOne: this.followFocus()}))
217+
.on(this.collapseKey, () => this.collapse({selectOne: this.followFocus()}))
218+
.on(Modifier.Shift, '*', () => this.expandSiblings());
227219

228220
if (this.inputs.multi()) {
229221
manager
@@ -260,6 +252,8 @@ export class TreePattern<V> {
260252
manager
261253
.on([Modifier.Ctrl, Modifier.Meta], this.prevKey, () => list.prev())
262254
.on([Modifier.Ctrl, Modifier.Meta], this.nextKey, () => list.next())
255+
.on([Modifier.Ctrl, Modifier.Meta], this.expandKey, () => this.expand())
256+
.on([Modifier.Ctrl, Modifier.Meta], this.collapseKey, () => this.collapse())
263257
.on([Modifier.Ctrl, Modifier.Meta], ' ', () => list.toggle())
264258
.on([Modifier.Ctrl, Modifier.Meta], 'Enter', () => list.toggle())
265259
.on([Modifier.Ctrl, Modifier.Meta], 'Home', () => list.first())
@@ -270,11 +264,6 @@ export class TreePattern<V> {
270264
});
271265
}
272266

273-
manager
274-
.on(this.expandKey, () => this.expand())
275-
.on(this.collapseKey, () => this.collapse())
276-
.on(Modifier.Shift, '*', () => this.expandSiblings());
277-
278267
return manager;
279268
});
280269

@@ -403,38 +392,38 @@ export class TreePattern<V> {
403392
}
404393

405394
/** Expands a tree item. */
406-
expand(item?: TreeItemPattern<V>) {
407-
item ??= this.activeItem();
395+
expand(opts?: SelectOptions) {
396+
const item = this.activeItem();
408397
if (!item || !this.listBehavior.isFocusable(item)) return;
409398

410399
if (item.expandable() && !item.expanded()) {
411400
item.expansion.open();
412-
} else if (item.expanded() && item.children().length > 0) {
413-
const firstChild = item.children()[0];
414-
if (this.listBehavior.isFocusable(firstChild)) {
415-
this.listBehavior.goto(firstChild);
416-
}
401+
} else if (
402+
item.expanded() &&
403+
item.children().some(item => this.listBehavior.isFocusable(item))
404+
) {
405+
this.listBehavior.next(opts);
417406
}
418407
}
419408

420409
/** Expands all sibling tree items including itself. */
421410
expandSiblings(item?: TreeItemPattern<V>) {
422411
item ??= this.activeItem();
423412
const siblings = item?.parent()?.children();
424-
siblings?.forEach(item => this.expand(item));
413+
siblings?.forEach(item => item.expansion.open());
425414
}
426415

427416
/** Collapses a tree item. */
428-
collapse(item?: TreeItemPattern<V>) {
429-
item ??= this.activeItem();
417+
collapse(opts?: SelectOptions) {
418+
const item = this.activeItem();
430419
if (!item || !this.listBehavior.isFocusable(item)) return;
431420

432421
if (item.expandable() && item.expanded()) {
433422
item.expansion.close();
434423
} else if (item.parent() && item.parent() !== this) {
435424
const parentItem = item.parent();
436425
if (parentItem instanceof TreeItemPattern && this.listBehavior.isFocusable(parentItem)) {
437-
this.listBehavior.goto(parentItem);
426+
this.listBehavior.goto(parentItem, opts);
438427
}
439428
}
440429
}

0 commit comments

Comments
 (0)