@@ -22,8 +22,17 @@ import {
2222 ZERO ,
2323} from '@angular/cdk/keycodes' ;
2424import { QueryList } from '@angular/core' ;
25- import { isObservable , of as observableOf , Observable , Subject } from 'rxjs' ;
26- import { take } from 'rxjs/operators' ;
25+ import {
26+ of as observableOf ,
27+ isObservable ,
28+ Observable ,
29+ Subject ,
30+ Subscription ,
31+ throwError ,
32+ } from 'rxjs' ;
33+ import { debounceTime , filter , map , switchMap , take , tap } from 'rxjs/operators' ;
34+
35+ const DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL_MS = 200 ;
2736
2837function coerceObservable < T > ( data : T | Observable < T > ) : Observable < T > {
2938 if ( ! isObservable ( data ) ) {
@@ -115,6 +124,8 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
115124 private _activeItem : T | null = null ;
116125 private _activationFollowsFocus = false ;
117126 private _horizontal : 'ltr' | 'rtl' = 'ltr' ;
127+ private readonly _letterKeyStream = new Subject < string > ( ) ;
128+ private _typeaheadSubscription = Subscription . EMPTY ;
118129
119130 /**
120131 * Predicate function that can be used to check whether an item should be skipped
@@ -125,6 +136,9 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
125136 /** Function to determine equivalent items. */
126137 private _trackByFn : ( item : T ) => unknown = ( item : T ) => item ;
127138
139+ /** Buffer for the letters that the user has pressed when the typeahead option is turned on. */
140+ private _pressedLetters : string [ ] = [ ] ;
141+
128142 private _items : T [ ] = [ ] ;
129143
130144 constructor ( {
@@ -147,6 +161,13 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
147161 if ( typeof activationFollowsFocus !== 'undefined' ) {
148162 this . _activationFollowsFocus = activationFollowsFocus ;
149163 }
164+ if ( typeof typeAheadDebounceInterval !== 'undefined' ) {
165+ this . _setTypeAhead (
166+ typeof typeAheadDebounceInterval === 'number'
167+ ? typeAheadDebounceInterval
168+ : DEFAULT_TYPEAHEAD_DEBOUNCE_INTERVAL_MS ,
169+ ) ;
170+ }
150171
151172 // We allow for the items to be an array or Observable because, in some cases, the consumer may
152173 // not have access to a QueryList of the items they want to manage (e.g. when the
@@ -292,6 +313,57 @@ export class TreeKeyManager<T extends TreeKeyManagerItem> {
292313 }
293314 }
294315
316+ private _setTypeAhead ( debounceInterval : number ) {
317+ this . _typeaheadSubscription . unsubscribe ( ) ;
318+
319+ // Debounce the presses of non-navigational keys, collect the ones that correspond to letters
320+ // and convert those letters back into a string. Afterwards find the first item that starts
321+ // with that string and select it.
322+ this . _typeaheadSubscription = this . _getItems ( )
323+ . pipe (
324+ switchMap ( items => {
325+ if (
326+ ( typeof ngDevMode === 'undefined' || ngDevMode ) &&
327+ items . length &&
328+ items . some ( item => typeof item . getLabel !== 'function' )
329+ ) {
330+ return throwError (
331+ new Error (
332+ 'TreeKeyManager items in typeahead mode must implement the `getLabel` method.' ,
333+ ) ,
334+ ) ;
335+ }
336+ return observableOf ( items ) as Observable < Array < T & { getLabel ( ) : string } > > ;
337+ } ) ,
338+ switchMap ( items => {
339+ return this . _letterKeyStream . pipe (
340+ tap ( letter => this . _pressedLetters . push ( letter ) ) ,
341+ debounceTime ( debounceInterval ) ,
342+ filter ( ( ) => this . _pressedLetters . length > 0 ) ,
343+ map ( ( ) => [ this . _pressedLetters . join ( '' ) , items ] as const ) ,
344+ ) ;
345+ } ) ,
346+ )
347+ . subscribe ( ( [ inputString , items ] ) => {
348+ // Start at 1 because we want to start searching at the item immediately
349+ // following the current active item.
350+ for ( let i = 1 ; i < items . length + 1 ; i ++ ) {
351+ const index = ( this . _activeItemIndex + i ) % items . length ;
352+ const item = items [ index ] ;
353+
354+ if (
355+ ! this . _skipPredicateFn ( item ) &&
356+ item . getLabel ( ) . toUpperCase ( ) . trim ( ) . indexOf ( inputString ) === 0
357+ ) {
358+ this . _setActiveItem ( index ) ;
359+ break ;
360+ }
361+ }
362+
363+ this . _pressedLetters = [ ] ;
364+ } ) ;
365+ }
366+
295367 //// Navigational methods
296368
297369 private _focusFirstItem ( ) {
0 commit comments