Skip to content

Commit c7068ba

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 c7068ba

File tree

2 files changed

+62
-3
lines changed

2 files changed

+62
-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: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,12 @@ export class ListKeyManager<T extends ListKeyManagerOption> {
7373
.subscribe(inputString => {
7474
const items = this._items.toArray();
7575

76-
for (let i = 0; i < items.length; i++) {
77-
if (items[i].getLabel!().toUpperCase().trim().indexOf(inputString) === 0) {
78-
this.setActiveItem(i);
76+
// Start at 1 because we want to start searching at the item immediately
77+
// following the current active item.
78+
for (let i = 1; i < items.length + 1; i++) {
79+
const index = (this._activeItemIndex + i) % items.length;
80+
if (items[index].getLabel!().toUpperCase().trim().indexOf(inputString) === 0) {
81+
this.setActiveItem(index);
7982
break;
8083
}
8184
}

0 commit comments

Comments
 (0)