Skip to content

Commit e122fba

Browse files
committed
fix(list-key-manager): align matching logic with native listbox
Currently the typeahead option of the `ListKeyManager` looks for matches from start to end which can lead to some weird behavior where the user might be half-way down the list, but be sent up to the first item (e.g. in a list of `b, a, ba` it would always find `b`). These changes align the behavior closer to the native `listbox` by looking for the next match after the active item. If no match is found, the selection wraps around and starts looking from the beginning.
1 parent dcf3b27 commit e122fba

File tree

2 files changed

+69
-3
lines changed

2 files changed

+69
-3
lines changed

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,62 @@ describe('Key managers', () => {
481481
expect(keyManager.activeItem).toBe(itemList.items[0]);
482482
}));
483483

484+
it('should start looking for matches after the active item', fakeAsync(() => {
485+
itemList.items = [
486+
new FakeFocusable('Bilbo'),
487+
new FakeFocusable('Frodo'),
488+
new FakeFocusable('Pippin'),
489+
new FakeFocusable('Boromir'),
490+
new FakeFocusable('Aragorn')
491+
];
492+
493+
keyManager.setActiveItem(1);
494+
keyManager.onKeydown(createKeyboardEvent('keydown', 66, undefined, 'b'));
495+
tick(debounceInterval);
496+
497+
expect(keyManager.activeItem).toBe(itemList.items[3]);
498+
}));
499+
500+
it('should wrap back around if there were no matches after the active item', fakeAsync(() => {
501+
itemList.items = [
502+
new FakeFocusable('Bilbo'),
503+
new FakeFocusable('Frodo'),
504+
new FakeFocusable('Pippin'),
505+
new FakeFocusable('Boromir'),
506+
new FakeFocusable('Aragorn')
507+
];
508+
509+
keyManager.setActiveItem(3);
510+
keyManager.onKeydown(createKeyboardEvent('keydown', 66, undefined, 'b'));
511+
tick(debounceInterval);
512+
513+
expect(keyManager.activeItem).toBe(itemList.items[0]);
514+
}));
515+
516+
it('should wrap back around if the last item is active', fakeAsync(() => {
517+
keyManager.setActiveItem(2);
518+
keyManager.onKeydown(createKeyboardEvent('keydown', 79, undefined, 'o'));
519+
tick(debounceInterval);
520+
521+
expect(keyManager.activeItem).toBe(itemList.items[0]);
522+
}));
523+
524+
it('should be able to select the first item', fakeAsync(() => {
525+
keyManager.setActiveItem(-1);
526+
keyManager.onKeydown(createKeyboardEvent('keydown', 79, undefined, 'o'));
527+
tick(debounceInterval);
528+
529+
expect(keyManager.activeItem).toBe(itemList.items[0]);
530+
}));
531+
532+
it('should not do anything if there is no match', fakeAsync(() => {
533+
keyManager.setActiveItem(1);
534+
keyManager.onKeydown(createKeyboardEvent('keydown', 87, undefined, 'w'));
535+
tick(debounceInterval);
536+
537+
expect(keyManager.activeItem).toBe(itemList.items[1]);
538+
}));
539+
484540
});
485541

486542
});

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,22 @@ export class ListKeyManager<T extends ListKeyManagerOption> {
7171
.call(filter, () => this._pressedLetters.length > 0)
7272
.call(map, () => this._pressedLetters.join(''))
7373
.subscribe(inputString => {
74-
const items = this._items.toArray();
75-
76-
for (let i = 0; i < items.length; i++) {
74+
let items = this._items.toArray();
75+
let total = items.length;
76+
let hasWrapped = false;
77+
let startIndex = this._activeItemIndex + 1 >= total ? 0 : this._activeItemIndex + 1;
78+
79+
// Start looking for the first item that matches the input string
80+
// after the active one. If we reach the end before finding a match,
81+
// wrap back and look through the items before the active one.
82+
for (let i = startIndex; i < total; i++) {
7783
if (items[i].getLabel!().toUpperCase().trim().indexOf(inputString) === 0) {
7884
this.setActiveItem(i);
7985
break;
86+
} else if (!hasWrapped && startIndex > 0 && i === total - 1) {
87+
i = -1;
88+
total = startIndex;
89+
hasWrapped = true;
8090
}
8191
}
8292

0 commit comments

Comments
 (0)