@@ -18,8 +18,17 @@ import {
1818 UP_ARROW ,
1919} from '@angular/cdk/keycodes' ;
2020import { 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 ;
2332
2433function coerceObservable < T > ( data : T | Observable < T > ) : Observable < T > {
2534 if ( ! isObservable ( data ) ) {
@@ -111,6 +120,8 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
111120 private _activeItem : T | null = null ;
112121 private _activationFollowsFocus = false ;
113122 private _horizontal : 'ltr' | 'rtl' = 'ltr' ;
123+ private readonly _letterKeyStream = new Subject < string > ( ) ;
124+ private _typeaheadSubscription = Subscription . EMPTY ;
114125
115126 /**
116127 * Predicate function that can be used to check whether an item should be skipped
@@ -121,6 +132,9 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
121132 /** Function to determine equivalent items. */
122133 private _trackByFn : ( item : T ) => unknown = ( item : T ) => item ;
123134
135+ /** Buffer for the letters that the user has pressed when the typeahead option is turned on. */
136+ private _pressedLetters : string [ ] = [ ] ;
137+
124138 private _items : T [ ] = [ ] ;
125139
126140 constructor ( {
@@ -143,6 +157,13 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
143157 if ( typeof activationFollowsFocus !== 'undefined' ) {
144158 this . _activationFollowsFocus = activationFollowsFocus ;
145159 }
160+ if ( typeof typeAheadDebounceInterval !== 'undefined' ) {
161+ this . _setTypeAhead (
162+ typeof typeAheadDebounceInterval === 'number'
163+ ? typeAheadDebounceInterval
164+ : DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL_MS ,
165+ ) ;
166+ }
146167
147168 // We allow for the items to be an array or Observable because, in some cases, the consumer may
148169 // 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> {
288309 }
289310 }
290311
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+
291363 //// Navigational methods
292364
293365 private _focusFirstItem ( ) {
0 commit comments