Skip to content

fix(list-key-manager): infinite loop if all items are disabled #9981

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/cdk/a11y/key-manager/list-key-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,16 @@ describe('Key managers', () => {
expect(keyManager.setActiveItem).toHaveBeenCalledWith(0);
});

// This test should pass if all items are disabled and the down arrow key got pressed.
// If the test setup crashes or this test times out, this test can be considered as failed.
it('should not get into an infinite loop if all items are disabled', () => {
keyManager.withWrap();
keyManager.setActiveItem(0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can chain the withWrap and setActiveItem.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to keep it consistent with the rest of the tests. All of those don't seem to chain either.


itemList.items.forEach(item => item.disabled = true);

keyManager.onKeydown(fakeKeyEvents.downArrow);
});
});

describe('typeahead mode', () => {
Expand Down
36 changes: 20 additions & 16 deletions src/cdk/a11y/key-manager/list-key-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ export class ListKeyManager<T extends ListKeyManagerOption> {
* currently active item and the new active item. It will calculate differently
* depending on whether wrap mode is turned on.
*/
private _setActiveItemByDelta(delta: number, items = this._items.toArray()): void {
private _setActiveItemByDelta(delta: -1 | 1, items = this._items.toArray()): void {
this._wrap ? this._setActiveInWrapMode(delta, items)
: this._setActiveInDefaultMode(delta, items);
}
Expand All @@ -269,16 +269,15 @@ export class ListKeyManager<T extends ListKeyManagerOption> {
* down the list until it finds an item that is not disabled, and it will wrap if it
* encounters either end of the list.
*/
private _setActiveInWrapMode(delta: number, items: T[]): void {
// when active item would leave menu, wrap to beginning or end
this._activeItemIndex =
(this._activeItemIndex + delta + items.length) % items.length;

// skip all disabled menu items recursively until an enabled one is reached
if (items[this._activeItemIndex].disabled) {
this._setActiveInWrapMode(delta, items);
} else {
this.setActiveItem(this._activeItemIndex);
private _setActiveInWrapMode(delta: -1 | 1, items: T[]): void {
for (let i = 1; i <= items.length; i++) {
const index = (this._activeItemIndex + (delta * i) + items.length) % items.length;
const item = items[index];

if (!item.disabled) {
this.setActiveItem(index);
return;
}
}
}

Expand All @@ -287,7 +286,7 @@ export class ListKeyManager<T extends ListKeyManagerOption> {
* continue to move down the list until it finds an item that is not disabled. If
* it encounters either end of the list, it will stop and not wrap.
*/
private _setActiveInDefaultMode(delta: number, items: T[]): void {
private _setActiveInDefaultMode(delta: -1 | 1, items: T[]): void {
this._setActiveItemByIndex(this._activeItemIndex + delta, delta, items);
}

Expand All @@ -296,13 +295,18 @@ export class ListKeyManager<T extends ListKeyManagerOption> {
* item is disabled, it will move in the fallbackDelta direction until it either
* finds an enabled item or encounters the end of the list.
*/
private _setActiveItemByIndex(index: number, fallbackDelta: number,
items = this._items.toArray()): void {
if (!items[index]) { return; }
private _setActiveItemByIndex(index: number, fallbackDelta: -1 | 1,
items = this._items.toArray()): void {
if (!items[index]) {
return;
}

while (items[index].disabled) {
index += fallbackDelta;
if (!items[index]) { return; }

if (!items[index]) {
return;
}
}

this.setActiveItem(index);
Expand Down