@@ -18,8 +18,17 @@ import {
18
18
UP_ARROW ,
19
19
} from '@angular/cdk/keycodes' ;
20
20
import { QueryList } from '@angular/core' ;
21
- import { isObservable , of as observableOf , Observable , Subject } from 'rxjs' ;
22
- import { take } from 'rxjs/operators' ;
21
+ import {
22
+ of as observableOf ,
23
+ isObservable ,
24
+ Observable ,
25
+ Subject ,
26
+ Subscription ,
27
+ throwError ,
28
+ } from 'rxjs' ;
29
+ import { debounceTime , filter , map , switchMap , take , tap } from 'rxjs/operators' ;
30
+
31
+ const DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL_MS = 200 ;
23
32
24
33
function coerceObservable < T > ( data : T | Observable < T > ) : Observable < T > {
25
34
if ( ! isObservable ( data ) ) {
@@ -111,6 +120,8 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
111
120
private _activeItem : T | null = null ;
112
121
private _activationFollowsFocus = false ;
113
122
private _horizontal : 'ltr' | 'rtl' = 'ltr' ;
123
+ private readonly _letterKeyStream = new Subject < string > ( ) ;
124
+ private _typeaheadSubscription = Subscription . EMPTY ;
114
125
115
126
/**
116
127
* Predicate function that can be used to check whether an item should be skipped
@@ -121,6 +132,9 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
121
132
/** Function to determine equivalent items. */
122
133
private _trackByFn : ( item : T ) => unknown = ( item : T ) => item ;
123
134
135
+ /** Buffer for the letters that the user has pressed when the typeahead option is turned on. */
136
+ private _pressedLetters : string [ ] = [ ] ;
137
+
124
138
private _items : T [ ] = [ ] ;
125
139
126
140
constructor ( {
@@ -143,6 +157,13 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
143
157
if ( typeof activationFollowsFocus !== 'undefined' ) {
144
158
this . _activationFollowsFocus = activationFollowsFocus ;
145
159
}
160
+ if ( typeof typeAheadDebounceInterval !== 'undefined' ) {
161
+ this . _setTypeAhead (
162
+ typeof typeAheadDebounceInterval === 'number'
163
+ ? typeAheadDebounceInterval
164
+ : DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL_MS ,
165
+ ) ;
166
+ }
146
167
147
168
// We allow for the items to be an array or Observable because, in some cases, the consumer may
148
169
// not have access to a QueryList of the items they want to manage (e.g. when the
@@ -288,6 +309,57 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
288
309
}
289
310
}
290
311
312
+ private _setTypeAhead ( debounceInterval : number ) {
313
+ this . _typeaheadSubscription . unsubscribe ( ) ;
314
+
315
+ // Debounce the presses of non-navigational keys, collect the ones that correspond to letters
316
+ // and convert those letters back into a string. Afterwards find the first item that starts
317
+ // with that string and select it.
318
+ this . _typeaheadSubscription = this . _getItems ( )
319
+ . pipe (
320
+ switchMap ( items => {
321
+ if (
322
+ ( typeof ngDevMode === 'undefined' || ngDevMode ) &&
323
+ items . length &&
324
+ items . some ( item => typeof item . getLabel !== 'function' )
325
+ ) {
326
+ return throwError (
327
+ new Error (
328
+ 'TreeKeyManager items in typeahead mode must implement the `getLabel` method.' ,
329
+ ) ,
330
+ ) ;
331
+ }
332
+ return observableOf ( items ) as Observable < Array < T & { getLabel ( ) : string } > > ;
333
+ } ) ,
334
+ switchMap ( items => {
335
+ return this . _letterKeyStream . pipe (
336
+ tap ( letter => this . _pressedLetters . push ( letter ) ) ,
337
+ debounceTime ( debounceInterval ) ,
338
+ filter ( ( ) => this . _pressedLetters . length > 0 ) ,
339
+ map ( ( ) => [ this . _pressedLetters . join ( '' ) , items ] as const ) ,
340
+ ) ;
341
+ } ) ,
342
+ )
343
+ . subscribe ( ( [ inputString , items ] ) => {
344
+ // Start at 1 because we want to start searching at the item immediately
345
+ // following the current active item.
346
+ for ( let i = 1 ; i < items . length + 1 ; i ++ ) {
347
+ const index = ( this . _activeItemIndex + i ) % items . length ;
348
+ const item = items [ index ] ;
349
+
350
+ if (
351
+ ! this . _skipPredicateFn ( item ) &&
352
+ item . getLabel ( ) . toUpperCase ( ) . trim ( ) . indexOf ( inputString ) === 0
353
+ ) {
354
+ this . _setActiveItem ( index ) ;
355
+ break ;
356
+ }
357
+ }
358
+
359
+ this . _pressedLetters = [ ] ;
360
+ } ) ;
361
+ }
362
+
291
363
//// Navigational methods
292
364
293
365
private _focusFirstItem ( ) {
0 commit comments