Skip to content

Commit 2f17450

Browse files
crisbetojelbourn
authored andcommitted
fix(select): don't handle open key presses while the user is typing (#17785)
We support typing into a select to skip to an item, as well as pressing space to open the select, however if the user is typing something that has a space in it, the select will open and interrupt the user. These changes add some logic so that we don't trigger the panel while the user is typing. Fixes #17774.
1 parent d299a31 commit 2f17450

File tree

5 files changed

+52
-5
lines changed

5 files changed

+52
-5
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,18 @@ describe('Key managers', () => {
818818
expect(keyManager.activeItem).toBe(itemList.items[1]);
819819
}));
820820

821+
it('should expose whether the user is currently typing', fakeAsync(() => {
822+
expect(keyManager.isTyping()).toBe(false);
823+
824+
keyManager.onKeydown(createKeyboardEvent('keydown', 79, 'o')); // types "o"
825+
826+
expect(keyManager.isTyping()).toBe(true);
827+
828+
tick(debounceInterval);
829+
830+
expect(keyManager.isTyping()).toBe(false);
831+
}));
832+
821833
});
822834

823835
});

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ export class ListKeyManager<T extends ListKeyManagerOption> {
147147
// and convert those letters back into a string. Afterwards find the first item that starts
148148
// with that string and select it.
149149
this._typeaheadSubscription = this._letterKeyStream.pipe(
150-
tap(keyCode => this._pressedLetters.push(keyCode)),
150+
tap(letter => this._pressedLetters.push(letter)),
151151
debounceTime(debounceInterval),
152152
filter(() => this._pressedLetters.length > 0),
153153
map(() => this._pressedLetters.join(''))
@@ -274,6 +274,11 @@ export class ListKeyManager<T extends ListKeyManagerOption> {
274274
return this._activeItem;
275275
}
276276

277+
/** Gets whether the user is currently typing into the manager using the typeahead feature. */
278+
isTyping(): boolean {
279+
return this._pressedLetters.length > 0;
280+
}
281+
277282
/** Sets the active item to the first enabled item in the list. */
278283
setFirstItemActive(): void {
279284
this._setActiveItemByIndex(0, 1);

src/material/select/select.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,31 @@ describe('MatSelect', () => {
511511
'Expected value from sixth option to have been set on the model.');
512512
}));
513513

514+
it('should not open the select when pressing space while typing', fakeAsync(() => {
515+
const selectInstance = fixture.componentInstance.select;
516+
517+
fixture.componentInstance.typeaheadDebounceInterval = DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL;
518+
fixture.detectChanges();
519+
520+
expect(selectInstance.panelOpen).toBe(false, 'Expected select to be closed on init.');
521+
522+
dispatchEvent(select, createKeyboardEvent('keydown', 80, 'p'));
523+
tick(DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL / 2);
524+
fixture.detectChanges();
525+
526+
dispatchKeyboardEvent(select, 'keydown', SPACE);
527+
fixture.detectChanges();
528+
529+
expect(selectInstance.panelOpen).toBe(false,
530+
'Expected select to remain closed after space was pressed.');
531+
532+
tick(DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL / 2);
533+
fixture.detectChanges();
534+
535+
expect(selectInstance.panelOpen).toBe(false,
536+
'Expected select to be closed when the timer runs out.');
537+
}));
538+
514539
it('should be able to customize the typeahead debounce interval', fakeAsync(() => {
515540
const formControl = fixture.componentInstance.control;
516541
const options = fixture.componentInstance.options.toArray();

src/material/select/select.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,8 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
721721
const manager = this._keyManager;
722722

723723
// Open the select on ALT + arrow key to match the native <select>
724-
if ((isOpenKey && !hasModifierKey(event)) || ((this.multiple || event.altKey) && isArrowKey)) {
724+
if (!manager.isTyping() && (isOpenKey && !hasModifierKey(event)) ||
725+
((this.multiple || event.altKey) && isArrowKey)) {
725726
event.preventDefault(); // prevents the page from scrolling down when pressing space
726727
this.open();
727728
} else if (!this.multiple) {
@@ -747,9 +748,10 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
747748

748749
/** Handles keyboard events when the selected is open. */
749750
private _handleOpenKeydown(event: KeyboardEvent): void {
751+
const manager = this._keyManager;
750752
const keyCode = event.keyCode;
751753
const isArrowKey = keyCode === DOWN_ARROW || keyCode === UP_ARROW;
752-
const manager = this._keyManager;
754+
const isTyping = manager.isTyping();
753755

754756
if (keyCode === HOME || keyCode === END) {
755757
event.preventDefault();
@@ -758,11 +760,13 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
758760
// Close the select on ALT + arrow key to match the native <select>
759761
event.preventDefault();
760762
this.close();
761-
} else if ((keyCode === ENTER || keyCode === SPACE) && manager.activeItem &&
763+
// Don't do anything in this case if the user is typing,
764+
// because the typing sequence can include the space key.
765+
} else if (!isTyping && (keyCode === ENTER || keyCode === SPACE) && manager.activeItem &&
762766
!hasModifierKey(event)) {
763767
event.preventDefault();
764768
manager.activeItem._selectViaInteraction();
765-
} else if (this._multiple && keyCode === A && event.ctrlKey) {
769+
} else if (!isTyping && this._multiple && keyCode === A && event.ctrlKey) {
766770
event.preventDefault();
767771
const hasDeselectedOptions = this.options.some(opt => !opt.disabled && !opt.selected);
768772

tools/public_api_guard/cdk/a11y.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export declare class ListKeyManager<T extends ListKeyManagerOption> {
144144
change: Subject<number>;
145145
tabOut: Subject<void>;
146146
constructor(_items: QueryList<T> | T[]);
147+
isTyping(): boolean;
147148
onKeydown(event: KeyboardEvent): void;
148149
setActiveItem(index: number): void;
149150
setActiveItem(item: T): void;

0 commit comments

Comments
 (0)