Skip to content

Commit

Permalink
fix(cdk/tree): capturing focus on load (#29641)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
crisbeto committed Aug 26, 2024
1 parent a9da72e commit 8b34fb7
Show file tree
Hide file tree
Showing 5 changed files with 35 additions and 12 deletions.
5 changes: 5 additions & 0 deletions src/cdk/a11y/key-manager/tree-key-manager-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export interface TreeKeyManagerItem {
* Unfocus the item. This should remove the focus state.
*/
unfocus(): void;

/**
* Sets the item to be focusable without actually focusing it.
*/
makeFocusable?(): void;
}

/**
Expand Down
34 changes: 22 additions & 12 deletions src/cdk/a11y/key-manager/tree-key-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,24 +57,34 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> implements TreeKeyMana

private _hasInitialFocused = false;

private _initialFocus() {
if (this._hasInitialFocused) {
private _initializeFocus(): void {
if (this._hasInitialFocused || this._items.length === 0) {
return;
}

if (!this._items.length) {
return;
}

let focusIndex = 0;
let activeIndex = 0;
for (let i = 0; i < this._items.length; i++) {
if (!this._skipPredicateFn(this._items[i]) && !this._isItemDisabled(this._items[i])) {
focusIndex = i;
activeIndex = i;
break;
}
}

this.focusItem(focusIndex);
const activeItem = this._items[activeIndex];

// Use `makeFocusable` here, because we want the item to just be focusable, not actually
// capture the focus since the user isn't interacting with it. See #29628.
if (activeItem.makeFocusable) {
this._activeItem?.unfocus();
this._activeItemIndex = activeIndex;
this._activeItem = activeItem;
this._typeahead?.setCurrentSelectedItemIndex(activeIndex);
activeItem.makeFocusable();
} else {
// Backwards compatibility for items that don't implement `makeFocusable`.
this.focusItem(activeIndex);
}

this._hasInitialFocused = true;
}

Expand All @@ -96,18 +106,18 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> implements TreeKeyMana
this._items = newItems.toArray();
this._typeahead?.setItems(this._items);
this._updateActiveItemIndex(this._items);
this._initialFocus();
this._initializeFocus();
});
} else if (isObservable(items)) {
items.subscribe(newItems => {
this._items = newItems;
this._typeahead?.setItems(newItems);
this._updateActiveItemIndex(newItems);
this._initialFocus();
this._initializeFocus();
});
} else {
this._items = items;
this._initialFocus();
this._initializeFocus();
}

if (typeof config.shouldActivationFollowFocus === 'boolean') {
Expand Down
6 changes: 6 additions & 0 deletions src/cdk/tree/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1414,6 +1414,12 @@ export class CdkTreeNode<T, K = T> implements OnDestroy, OnInit, TreeKeyManagerI
}
}

/** Makes the node focusable. Implemented for TreeKeyManagerItem. */
makeFocusable(): void {
this._tabindex = 0;
this._changeDetectorRef.markForCheck();
}

_focusItem() {
if (this.isDisabled) {
return;
Expand Down
1 change: 1 addition & 0 deletions tools/public_api_guard/cdk/a11y.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ export interface TreeKeyManagerItem {
getParent(): TreeKeyManagerItem | null;
isDisabled?: (() => boolean) | boolean;
isExpanded: (() => boolean) | boolean;
makeFocusable?(): void;
unfocus(): void;
}

Expand Down
1 change: 1 addition & 0 deletions tools/public_api_guard/cdk/tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ export class CdkTreeNode<T, K = T> implements OnDestroy, OnInit, TreeKeyManagerI
get isLeafNode(): boolean;
// (undocumented)
get level(): number;
makeFocusable(): void;
static mostRecentTreeNode: CdkTreeNode<any> | null;
// (undocumented)
static ngAcceptInputType_isDisabled: unknown;
Expand Down

0 comments on commit 8b34fb7

Please sign in to comment.