Skip to content

Commit 08a6673

Browse files
crisbetoandrewseguin
authored andcommitted
fix(list-key-manager): typehead not handling non-English input (#6463)
Since `String.fromCharCode` only maps the key codes to their English letter counterparts, the typeahead option in the `ListKeyManager` won't work for non-English input. These changes switch to using `event.key` and falling back to using `keyCode` and `fromCharCode`.
1 parent da7cb2d commit 08a6673

File tree

3 files changed

+50
-29
lines changed

3 files changed

+50
-29
lines changed

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

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -408,9 +408,9 @@ describe('Key managers', () => {
408408
});
409409

410410
it('should debounce the input key presses', fakeAsync(() => {
411-
keyManager.onKeydown(createKeyboardEvent('keydown', 79)); // types "o"
412-
keyManager.onKeydown(createKeyboardEvent('keydown', 78)); // types "n"
413-
keyManager.onKeydown(createKeyboardEvent('keydown', 69)); // types "e"
411+
keyManager.onKeydown(createKeyboardEvent('keydown', 79, undefined, 'o')); // types "o"
412+
keyManager.onKeydown(createKeyboardEvent('keydown', 78, undefined, 'n')); // types "n"
413+
keyManager.onKeydown(createKeyboardEvent('keydown', 69, undefined, 'e')); // types "e"
414414

415415
expect(keyManager.activeItem).not.toBe(itemList.items[0]);
416416

@@ -420,32 +420,47 @@ describe('Key managers', () => {
420420
}));
421421

422422
it('should focus the first item that starts with a letter', fakeAsync(() => {
423-
keyManager.onKeydown(createKeyboardEvent('keydown', 84)); // types "t"
423+
keyManager.onKeydown(createKeyboardEvent('keydown', 84, undefined, 't')); // types "t"
424424

425425
tick(debounceInterval);
426426

427427
expect(keyManager.activeItem).toBe(itemList.items[1]);
428428
}));
429429

430430
it('should focus the first item that starts with sequence of letters', fakeAsync(() => {
431-
keyManager.onKeydown(createKeyboardEvent('keydown', 84)); // types "t"
432-
keyManager.onKeydown(createKeyboardEvent('keydown', 72)); // types "h"
431+
keyManager.onKeydown(createKeyboardEvent('keydown', 84, undefined, 't')); // types "t"
432+
keyManager.onKeydown(createKeyboardEvent('keydown', 72, undefined, 'h')); // types "h"
433433

434434
tick(debounceInterval);
435435

436436
expect(keyManager.activeItem).toBe(itemList.items[2]);
437437
}));
438438

439439
it('should cancel any pending timers if a navigation key is pressed', fakeAsync(() => {
440-
keyManager.onKeydown(createKeyboardEvent('keydown', 84)); // types "t"
441-
keyManager.onKeydown(createKeyboardEvent('keydown', 72)); // types "h"
440+
keyManager.onKeydown(createKeyboardEvent('keydown', 84, undefined, 't')); // types "t"
441+
keyManager.onKeydown(createKeyboardEvent('keydown', 72, undefined, 'h')); // types "h"
442442
keyManager.onKeydown(fakeKeyEvents.downArrow);
443443

444444
tick(debounceInterval);
445445

446446
expect(keyManager.activeItem).toBe(itemList.items[0]);
447447
}));
448448

449+
it('should handle non-English input', fakeAsync(() => {
450+
itemList.items = [
451+
new FakeFocusable('едно'),
452+
new FakeFocusable('две'),
453+
new FakeFocusable('три')
454+
];
455+
456+
const keyboardEvent = createKeyboardEvent('keydown', 68, undefined, 'д');
457+
458+
keyManager.onKeydown(keyboardEvent); // types "д"
459+
tick(debounceInterval);
460+
461+
expect(keyManager.activeItem).toBe(itemList.items[1]);
462+
}));
463+
449464
});
450465

451466
});

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

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
*/
88

99
import {QueryList} from '@angular/core';
10-
import {Observable} from 'rxjs/Observable';
1110
import {Subject} from 'rxjs/Subject';
1211
import {Subscription} from 'rxjs/Subscription';
1312
import {UP_ARROW, DOWN_ARROW, TAB, A, Z} from '@angular/cdk/keycodes';
@@ -29,14 +28,20 @@ export class ListKeyManager<T extends ListKeyManagerOption> {
2928
private _activeItemIndex = -1;
3029
private _activeItem: T;
3130
private _wrap = false;
32-
private _nonNavigationKeyStream = new Subject<number>();
31+
private _letterKeyStream = new Subject<string>();
3332
private _typeaheadSubscription: Subscription;
3433

3534
// Buffer for the letters that the user has pressed when the typeahead option is turned on.
36-
private _pressedInputKeys: number[] = [];
35+
private _pressedLetters: string[] = [];
3736

3837
constructor(private _items: QueryList<T>) { }
3938

39+
/**
40+
* Stream that emits any time the TAB key is pressed, so components can react
41+
* when focus is shifted off of the list.
42+
*/
43+
tabOut: Subject<void> = new Subject<void>();
44+
4045
/**
4146
* Turns on wrapping mode, which ensures that the active item will wrap to
4247
* the other end of list when there are no more items in the given direction.
@@ -62,12 +67,11 @@ export class ListKeyManager<T extends ListKeyManagerOption> {
6267
// Debounce the presses of non-navigational keys, collect the ones that correspond to letters
6368
// and convert those letters back into a string. Afterwards find the first item that starts
6469
// with that string and select it.
65-
this._typeaheadSubscription = RxChain.from(this._nonNavigationKeyStream)
66-
.call(filter, keyCode => keyCode >= A && keyCode <= Z)
67-
.call(doOperator, keyCode => this._pressedInputKeys.push(keyCode))
70+
this._typeaheadSubscription = RxChain.from(this._letterKeyStream)
71+
.call(doOperator, keyCode => this._pressedLetters.push(keyCode))
6872
.call(debounceTime, debounceInterval)
69-
.call(filter, () => this._pressedInputKeys.length > 0)
70-
.call(map, () => String.fromCharCode(...this._pressedInputKeys))
73+
.call(filter, () => this._pressedLetters.length > 0)
74+
.call(map, () => this._pressedLetters.join(''))
7175
.subscribe(inputString => {
7276
const items = this._items.toArray();
7377

@@ -78,7 +82,7 @@ export class ListKeyManager<T extends ListKeyManagerOption> {
7882
}
7983
}
8084

81-
this._pressedInputKeys = [];
85+
this._pressedLetters = [];
8286
});
8387

8488
return this;
@@ -101,13 +105,22 @@ export class ListKeyManager<T extends ListKeyManagerOption> {
101105
switch (event.keyCode) {
102106
case DOWN_ARROW: this.setNextItemActive(); break;
103107
case UP_ARROW: this.setPreviousItemActive(); break;
108+
case TAB: this.tabOut.next(); return;
109+
default:
110+
if (event.keyCode >= A && event.keyCode <= Z) {
111+
// Attempt to use the `event.key` which also maps it to the user's keyboard language,
112+
// otherwise fall back to `keyCode` and `fromCharCode` which always resolve to English.
113+
this._letterKeyStream.next(event.key ?
114+
event.key.toLocaleUpperCase() :
115+
String.fromCharCode(event.keyCode));
116+
}
104117

105118
// Note that we return here, in order to avoid preventing
106-
// the default action of unsupported keys.
107-
default: this._nonNavigationKeyStream.next(event.keyCode); return;
119+
// the default action of non-navigational keys.
120+
return;
108121
}
109122

110-
this._pressedInputKeys = [];
123+
this._pressedLetters = [];
111124
event.preventDefault();
112125
}
113126

@@ -150,14 +163,6 @@ export class ListKeyManager<T extends ListKeyManagerOption> {
150163
this._activeItemIndex = index;
151164
}
152165

153-
/**
154-
* Observable that emits any time the TAB key is pressed, so components can react
155-
* when focus is shifted off of the list.
156-
*/
157-
get tabOut(): Observable<void> {
158-
return filter.call(this._nonNavigationKeyStream, keyCode => keyCode === TAB);
159-
}
160-
161166
/**
162167
* This method sets the active item, given a list of items and the delta between the
163168
* currently active item and the new active item. It will calculate differently

src/cdk/testing/event-objects.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function createMouseEvent(type: string, x = 0, y = 0) {
3030
}
3131

3232
/** Dispatches a keydown event from an element. */
33-
export function createKeyboardEvent(type: string, keyCode: number, target?: Element) {
33+
export function createKeyboardEvent(type: string, keyCode: number, target?: Element, key?: string) {
3434
let event = document.createEvent('KeyboardEvent') as any;
3535
// Firefox does not support `initKeyboardEvent`, but supports `initKeyEvent`.
3636
let initEventFn = (event.initKeyEvent || event.initKeyboardEvent).bind(event);
@@ -42,6 +42,7 @@ export function createKeyboardEvent(type: string, keyCode: number, target?: Elem
4242
// See related bug https://bugs.webkit.org/show_bug.cgi?id=16735
4343
Object.defineProperties(event, {
4444
keyCode: { get: () => keyCode },
45+
key: { get: () => key },
4546
target: { get: () => target }
4647
});
4748

0 commit comments

Comments
 (0)