Skip to content

Commit 8b34fb7

Browse files
authored
fix(cdk/tree): capturing focus on load (#29641)
The tree implements a roving tabindex which needs to have an initial item with `tabindex = 0` to work correctly. This happens by waiting for the data to be initialized in the `TreeKeyManager` and focusing the active/first item. The problem is that this ends up stealing focus on load. We didn't notice this issue in the demo app, because all the tree are `visibility: hidden` since they're inside closed `mat-expansion-panel`, but the issue is visible in the docs site. These changes resolve the issue by setting the `tabindex` without actually moving focus. Fixes #29628.
1 parent a9da72e commit 8b34fb7

File tree

5 files changed

+35
-12
lines changed

5 files changed

+35
-12
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export interface TreeKeyManagerItem {
4444
* Unfocus the item. This should remove the focus state.
4545
*/
4646
unfocus(): void;
47+
48+
/**
49+
* Sets the item to be focusable without actually focusing it.
50+
*/
51+
makeFocusable?(): void;
4752
}
4853

4954
/**

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

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,24 +57,34 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> implements TreeKeyMana
5757

5858
private _hasInitialFocused = false;
5959

60-
private _initialFocus() {
61-
if (this._hasInitialFocused) {
60+
private _initializeFocus(): void {
61+
if (this._hasInitialFocused || this._items.length === 0) {
6262
return;
6363
}
6464

65-
if (!this._items.length) {
66-
return;
67-
}
68-
69-
let focusIndex = 0;
65+
let activeIndex = 0;
7066
for (let i = 0; i < this._items.length; i++) {
7167
if (!this._skipPredicateFn(this._items[i]) && !this._isItemDisabled(this._items[i])) {
72-
focusIndex = i;
68+
activeIndex = i;
7369
break;
7470
}
7571
}
7672

77-
this.focusItem(focusIndex);
73+
const activeItem = this._items[activeIndex];
74+
75+
// Use `makeFocusable` here, because we want the item to just be focusable, not actually
76+
// capture the focus since the user isn't interacting with it. See #29628.
77+
if (activeItem.makeFocusable) {
78+
this._activeItem?.unfocus();
79+
this._activeItemIndex = activeIndex;
80+
this._activeItem = activeItem;
81+
this._typeahead?.setCurrentSelectedItemIndex(activeIndex);
82+
activeItem.makeFocusable();
83+
} else {
84+
// Backwards compatibility for items that don't implement `makeFocusable`.
85+
this.focusItem(activeIndex);
86+
}
87+
7888
this._hasInitialFocused = true;
7989
}
8090

@@ -96,18 +106,18 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> implements TreeKeyMana
96106
this._items = newItems.toArray();
97107
this._typeahead?.setItems(this._items);
98108
this._updateActiveItemIndex(this._items);
99-
this._initialFocus();
109+
this._initializeFocus();
100110
});
101111
} else if (isObservable(items)) {
102112
items.subscribe(newItems => {
103113
this._items = newItems;
104114
this._typeahead?.setItems(newItems);
105115
this._updateActiveItemIndex(newItems);
106-
this._initialFocus();
116+
this._initializeFocus();
107117
});
108118
} else {
109119
this._items = items;
110-
this._initialFocus();
120+
this._initializeFocus();
111121
}
112122

113123
if (typeof config.shouldActivationFollowFocus === 'boolean') {

src/cdk/tree/tree.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,6 +1414,12 @@ export class CdkTreeNode<T, K = T> implements OnDestroy, OnInit, TreeKeyManagerI
14141414
}
14151415
}
14161416

1417+
/** Makes the node focusable. Implemented for TreeKeyManagerItem. */
1418+
makeFocusable(): void {
1419+
this._tabindex = 0;
1420+
this._changeDetectorRef.markForCheck();
1421+
}
1422+
14171423
_focusItem() {
14181424
if (this.isDisabled) {
14191425
return;

tools/public_api_guard/cdk/a11y.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,7 @@ export interface TreeKeyManagerItem {
505505
getParent(): TreeKeyManagerItem | null;
506506
isDisabled?: (() => boolean) | boolean;
507507
isExpanded: (() => boolean) | boolean;
508+
makeFocusable?(): void;
508509
unfocus(): void;
509510
}
510511

tools/public_api_guard/cdk/tree.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ export class CdkTreeNode<T, K = T> implements OnDestroy, OnInit, TreeKeyManagerI
188188
get isLeafNode(): boolean;
189189
// (undocumented)
190190
get level(): number;
191+
makeFocusable(): void;
191192
static mostRecentTreeNode: CdkTreeNode<any> | null;
192193
// (undocumented)
193194
static ngAcceptInputType_isDisabled: unknown;

0 commit comments

Comments
 (0)