Skip to content

Commit dc05a73

Browse files
committed
fix(cdk/tree): update active item on focus
Update CdkTreeNode, TreeKeyManager and Tree "Load More" example regarding progrmatic focus. - In CdkTreeNode, update the active item on the focus event. - expose TreeKeyManager.updateActiveItem - remove TreeKeyManager.onClick - In "Load More" example, when clicking "Load More", focus first node that is added.
1 parent a34ad99 commit dc05a73

File tree

7 files changed

+160
-117
lines changed

7 files changed

+160
-117
lines changed

src/cdk/a11y/key-manager/tree-key-manager.spec.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ describe('TreeKeyManager', () => {
164164
});
165165

166166
it('should maintain the active item if the amount of items changes', () => {
167-
keyManager.onClick(itemList.get(0)!);
167+
keyManager.setActiveItem(itemList.get(0)!);
168168

169169
expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(0);
170170
expect(keyManager.getActiveItem()?.getLabel())
@@ -199,7 +199,7 @@ describe('TreeKeyManager', () => {
199199
});
200200

201201
it('should emit an event whenever the active item changes', () => {
202-
keyManager.onClick(itemList.get(0)!);
202+
keyManager.setActiveItem(itemList.get(0)!);
203203

204204
const spy = jasmine.createSpy('change spy');
205205
const subscription = keyManager.change.subscribe(spy);
@@ -214,14 +214,14 @@ describe('TreeKeyManager', () => {
214214
});
215215

216216
it('should emit if the active item changed, but not the active index', () => {
217-
keyManager.onClick(itemList.get(0)!);
217+
keyManager.setActiveItem(itemList.get(0)!);
218218

219219
const spy = jasmine.createSpy('change spy');
220220
const subscription = keyManager.change.subscribe(spy);
221221

222222
itemList.reset([new itemParam.constructor('zero'), ...itemList.toArray()]);
223223
itemList.notifyOnChanges();
224-
keyManager.onClick(itemList.get(0)!);
224+
keyManager.setActiveItem(itemList.get(0)!);
225225

226226
expect(spy).toHaveBeenCalledTimes(1);
227227
subscription.unsubscribe();
@@ -248,7 +248,7 @@ describe('TreeKeyManager', () => {
248248
});
249249

250250
it('should not do anything for unsupported key presses', () => {
251-
keyManager.onClick(itemList.get(1)!);
251+
keyManager.setActiveItem(itemList.get(1)!);
252252

253253
expect(keyManager.getActiveItemIndex()).toBe(1);
254254
expect(fakeKeyEvents.unsupported.defaultPrevented).toBe(false);
@@ -260,7 +260,7 @@ describe('TreeKeyManager', () => {
260260
});
261261

262262
it('should focus the first item when Home is pressed', () => {
263-
keyManager.onClick(itemList.get(1)!);
263+
keyManager.setActiveItem(itemList.get(1)!);
264264
expect(keyManager.getActiveItemIndex()).toBe(1);
265265

266266
keyManager.onKeydown(fakeKeyEvents.home);
@@ -270,7 +270,7 @@ describe('TreeKeyManager', () => {
270270

271271
it('should focus the first non-disabled item when Home is pressed', () => {
272272
itemList.get(0)!.isDisabled = true;
273-
keyManager.onClick(itemList.get(2)!);
273+
keyManager.setActiveItem(itemList.get(2)!);
274274
expect(keyManager.getActiveItemIndex()).toBe(2);
275275

276276
keyManager.onKeydown(fakeKeyEvents.home);
@@ -279,7 +279,7 @@ describe('TreeKeyManager', () => {
279279
});
280280

281281
it('should focus the last item when End is pressed', () => {
282-
keyManager.onClick(itemList.get(0)!);
282+
keyManager.setActiveItem(itemList.get(0)!);
283283
expect(keyManager.getActiveItemIndex()).toBe(0);
284284

285285
keyManager.onKeydown(fakeKeyEvents.end);
@@ -288,7 +288,7 @@ describe('TreeKeyManager', () => {
288288

289289
it('should focus the last non-disabled item when End is pressed', () => {
290290
itemList.get(itemList.length - 1)!.isDisabled = true;
291-
keyManager.onClick(itemList.get(0)!);
291+
keyManager.setActiveItem(itemList.get(0)!);
292292
expect(keyManager.getActiveItemIndex()).toBe(0);
293293

294294
keyManager.onKeydown(fakeKeyEvents.end);
@@ -299,7 +299,7 @@ describe('TreeKeyManager', () => {
299299

300300
describe('up/down key events', () => {
301301
it('should set subsequent items as active when the down key is pressed', () => {
302-
keyManager.onClick(itemList.get(0)!);
302+
keyManager.setActiveItem(itemList.get(0)!);
303303

304304
const spy = jasmine.createSpy('change spy');
305305
const subscription = keyManager.change.subscribe(spy);
@@ -330,7 +330,7 @@ describe('TreeKeyManager', () => {
330330
});
331331

332332
it('should set previous item as active when the up key is pressed', () => {
333-
keyManager.onClick(itemList.get(0)!);
333+
keyManager.setActiveItem(itemList.get(0)!);
334334

335335
const spy = jasmine.createSpy('change spy');
336336
const subscription = keyManager.change.subscribe(spy);
@@ -365,7 +365,7 @@ describe('TreeKeyManager', () => {
365365

366366
it('should skip disabled items', () => {
367367
itemList.get(1)!.isDisabled = true;
368-
keyManager.onClick(itemList.get(0)!);
368+
keyManager.setActiveItem(itemList.get(0)!);
369369

370370
const spy = jasmine.createSpy('change spy');
371371
const subscription = keyManager.change.subscribe(spy);
@@ -393,7 +393,7 @@ describe('TreeKeyManager', () => {
393393
itemList.get(0)!.isDisabled = undefined;
394394
itemList.get(1)!.isDisabled = undefined;
395395
itemList.get(2)!.isDisabled = undefined;
396-
keyManager.onClick(itemList.get(0)!);
396+
keyManager.setActiveItem(itemList.get(0)!);
397397

398398
const spy = jasmine.createSpy('change spy');
399399
const subscription = keyManager.change.subscribe(spy);
@@ -416,7 +416,7 @@ describe('TreeKeyManager', () => {
416416
});
417417

418418
it('should not move active item past either end of the list', () => {
419-
keyManager.onClick(itemList.get(itemList.length - 1)!);
419+
keyManager.setActiveItem(itemList.get(itemList.length - 1)!);
420420

421421
expect(keyManager.getActiveItemIndex())
422422
.withContext('active item index, selecting the last item')
@@ -428,7 +428,7 @@ describe('TreeKeyManager', () => {
428428
.withContext('active item index, last item still selected after a down event')
429429
.toBe(itemList.length - 1);
430430

431-
keyManager.onClick(itemList.get(0)!);
431+
keyManager.setActiveItem(itemList.get(0)!);
432432
keyManager.onKeydown(fakeKeyEvents.upArrow);
433433
expect(keyManager.getActiveItemIndex())
434434
.withContext('active item index, selecting the first item')
@@ -444,7 +444,7 @@ describe('TreeKeyManager', () => {
444444
it('should not move active item to end when the last item is disabled', () => {
445445
itemList.get(itemList.length - 1)!.isDisabled = true;
446446

447-
keyManager.onClick(itemList.get(itemList.length - 2)!);
447+
keyManager.setActiveItem(itemList.get(itemList.length - 2)!);
448448
expect(keyManager.getActiveItemIndex())
449449
.withContext('active item index, last non-disabled item selected')
450450
.toBe(itemList.length - 2);
@@ -555,7 +555,7 @@ describe('TreeKeyManager', () => {
555555
let subscription: Subscription;
556556

557557
beforeEach(() => {
558-
keyManager.onClick(parentItem);
558+
keyManager.setActiveItem(parentItem);
559559
parentItem._isExpanded = true;
560560

561561
spy = jasmine.createSpy('change spy');
@@ -640,7 +640,7 @@ describe('TreeKeyManager', () => {
640640
let subscription: Subscription;
641641

642642
beforeEach(() => {
643-
keyManager.onClick(childItemWithNoChildren);
643+
keyManager.setActiveItem(childItemWithNoChildren);
644644
childItemWithNoChildren._isExpanded = true;
645645

646646
spy = jasmine.createSpy('change spy');
@@ -666,7 +666,7 @@ describe('TreeKeyManager', () => {
666666
let subscription: Subscription;
667667

668668
beforeEach(() => {
669-
keyManager.onClick(childItem);
669+
keyManager.setActiveItem(childItem);
670670
childItem._isExpanded = false;
671671

672672
spy = jasmine.createSpy('change spy');
@@ -733,7 +733,7 @@ describe('TreeKeyManager', () => {
733733
let subscription: Subscription;
734734

735735
beforeEach(() => {
736-
keyManager.onClick(parentItem);
736+
keyManager.setActiveItem(parentItem);
737737
parentItem._isExpanded = false;
738738

739739
spy = jasmine.createSpy('change spy');
@@ -924,7 +924,7 @@ describe('TreeKeyManager', () => {
924924
]);
925925
itemList.notifyOnChanges();
926926

927-
keyManager.onClick(frodo);
927+
keyManager.setActiveItem(frodo);
928928
keyManager.onKeydown(createKeyboardEvent('keydown', 66, 'b'));
929929
tick(debounceInterval);
930930

@@ -942,15 +942,15 @@ describe('TreeKeyManager', () => {
942942
]);
943943
itemList.notifyOnChanges();
944944

945-
keyManager.onClick(boromir);
945+
keyManager.setActiveItem(boromir);
946946
keyManager.onKeydown(createKeyboardEvent('keydown', 66, 'b'));
947947
tick(debounceInterval);
948948

949949
expect(keyManager.getActiveItemIndex()).withContext('active item index').toBe(0);
950950
}));
951951

952952
it('should wrap back around if the last item is active', fakeAsync(() => {
953-
keyManager.onClick(lastItem);
953+
keyManager.setActiveItem(lastItem);
954954
keyManager.onKeydown(createKeyboardEvent('keydown', 79, 'o'));
955955
tick(debounceInterval);
956956

src/cdk/a11y/key-manager/tree-key-manager.ts

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -261,14 +261,6 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
261261
event.preventDefault();
262262
}
263263

264-
/**
265-
* Handles a mouse click on a particular tree item.
266-
* @param treeItem The item that was clicked by the user.
267-
*/
268-
onClick(treeItem: T) {
269-
this._setActiveItem(treeItem);
270-
}
271-
272264
/** Index of the currently active item. */
273265
getActiveItemIndex(): number | null {
274266
return this._activeItemIndex;
@@ -300,7 +292,7 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
300292
*/
301293
focusItem(item: T, options?: {emitChangeEvent?: boolean}): void;
302294
focusItem(itemOrIndex: number | T, options?: {emitChangeEvent?: boolean}): void {
303-
this._setActiveItem(itemOrIndex, options);
295+
this.setActiveItem(itemOrIndex, options);
304296
}
305297

306298
/** Focus the first available item. */
@@ -323,10 +315,10 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
323315
this._focusPreviousItem();
324316
}
325317

326-
private _setActiveItem(index: number, options?: {emitChangeEvent?: boolean}): void;
327-
private _setActiveItem(item: T, options?: {emitChangeEvent?: boolean}): void;
328-
private _setActiveItem(itemOrIndex: number | T, options?: {emitChangeEvent?: boolean}): void;
329-
private _setActiveItem(itemOrIndex: number | T, options: {emitChangeEvent?: boolean} = {}) {
318+
setActiveItem(index: number, options?: {emitChangeEvent?: boolean}): void;
319+
setActiveItem(item: T, options?: {emitChangeEvent?: boolean}): void;
320+
setActiveItem(itemOrIndex: number | T, options?: {emitChangeEvent?: boolean}): void;
321+
setActiveItem(itemOrIndex: number | T, options: {emitChangeEvent?: boolean} = {}) {
330322
// Set default options
331323
options.emitChangeEvent ??= true;
332324

@@ -406,7 +398,7 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
406398
!this._skipPredicateFn(item) &&
407399
item.getLabel?.().toLocaleUpperCase().trim().indexOf(inputString) === 0
408400
) {
409-
this._setActiveItem(index);
401+
this.setActiveItem(index);
410402
break;
411403
}
412404
}
@@ -418,19 +410,19 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
418410
//// Navigational methods
419411

420412
private _focusFirstItem() {
421-
this._setActiveItem(this._findNextAvailableItemIndex(-1));
413+
this.setActiveItem(this._findNextAvailableItemIndex(-1));
422414
}
423415

424416
private _focusLastItem() {
425-
this._setActiveItem(this._findPreviousAvailableItemIndex(this._items.length));
417+
this.setActiveItem(this._findPreviousAvailableItemIndex(this._items.length));
426418
}
427419

428420
private _focusPreviousItem() {
429-
this._setActiveItem(this._findPreviousAvailableItemIndex(this._activeItemIndex));
421+
this.setActiveItem(this._findPreviousAvailableItemIndex(this._activeItemIndex));
430422
}
431423

432424
private _focusNextItem() {
433-
this._setActiveItem(this._findNextAvailableItemIndex(this._activeItemIndex));
425+
this.setActiveItem(this._findNextAvailableItemIndex(this._activeItemIndex));
434426
}
435427

436428
private _findNextAvailableItemIndex(startingIndex: number) {
@@ -466,7 +458,7 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
466458
if (!parent || this._skipPredicateFn(parent as T)) {
467459
return;
468460
}
469-
this._setActiveItem(parent as T);
461+
this.setActiveItem(parent as T);
470462
}
471463
}
472464

@@ -488,7 +480,7 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
488480
if (!firstChild) {
489481
return;
490482
}
491-
this._setActiveItem(firstChild as T);
483+
this.setActiveItem(firstChild as T);
492484
});
493485
}
494486
}

src/cdk/tree/tree.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,6 +1174,18 @@ describe('CdkTree redesign', () => {
11741174
expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['0', '-1', '-1', '-1', '-1', '-1']);
11751175
});
11761176

1177+
it('maintains tabindex when a node is programatically focused', () => {
1178+
// activate the second child by programatically focusing it
1179+
nodes[1].focus();
1180+
1181+
expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['-1', '0', '-1', '-1', '-1', '-1']);
1182+
1183+
// activate the first child by programatically focusing it
1184+
nodes[0].focus();
1185+
1186+
expect(getNodeAttributes(nodes, 'tabindex')).toEqual(['0', '-1', '-1', '-1', '-1', '-1']);
1187+
});
1188+
11771189
it('maintains tabindex when component is blurred', () => {
11781190
// activate the second child by clicking on it
11791191
nodes[1].click();

src/cdk/tree/tree.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1085,6 +1085,7 @@ export class CdkTree<T, K = T>
10851085
'tabindex': '-1',
10861086
'role': 'treeitem',
10871087
'(click)': '_setActiveItem()',
1088+
'(focus)': '_setActiveItem()',
10881089
},
10891090
})
10901091
export class CdkTreeNode<T, K = T> implements OnDestroy, OnInit, TreeKeyManagerItem {
@@ -1290,7 +1291,7 @@ export class CdkTreeNode<T, K = T> implements OnDestroy, OnInit, TreeKeyManagerI
12901291
if (this.isDisabled) {
12911292
return;
12921293
}
1293-
this._tree._keyManager.onClick(this);
1294+
this._tree._keyManager.setActiveItem(this);
12941295
}
12951296

12961297
_emitExpansionState(expanded: boolean) {

src/components-examples/material/tree/tree-loadmore/tree-loadmore-example.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,25 @@
22
<!-- Leaf node -->
33
<mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
44
<button mat-icon-button disabled></button>
5-
{{node.item}}
5+
{{node.name}}
66
</mat-tree-node>
77

88
<!-- expandable node -->
99
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding matTreeNodeToggle
1010
(expandedChange)="loadChildren(node)">
1111
<button mat-icon-button
12-
[attr.aria-label]="'Toggle ' + node.item"
12+
[attr.aria-label]="'Toggle ' + node.name"
1313
matTreeNodeToggle>
1414
<mat-icon class="mat-icon-rtl-mirror">
1515
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
1616
</mat-icon>
1717
</button>
18-
{{node.item}}
18+
{{node.name}}
1919
</mat-tree-node>
2020

2121
<mat-tree-node class="example-load-more" *matTreeNodeDef="let node; when: isLoadMore"
22-
role="button" (click)="loadMoreOnClick($event, node)"
23-
(keydown)="loadMoreOnEnterOrSpace($event, node)">
24-
Load more...
22+
role="button" (click)="loadOnClick($event, node)"
23+
(keydown)="loadOnKeypress($event, node)">
24+
Load more of {{node.parent}}...
2525
</mat-tree-node>
2626
</mat-tree>

0 commit comments

Comments
 (0)