@@ -15,6 +15,7 @@ import {QueryList} from '@angular/core';
1515import { take } from 'rxjs/operators' ;
1616import { TreeKeyManager , TreeKeyManagerItem } from './tree-key-manager' ;
1717import { Observable , of as observableOf , Subscription } from 'rxjs' ;
18+ import { fakeAsync , tick } from '@angular/core/testing' ;
1819
1920class FakeBaseTreeKeyManagerItem {
2021 _isExpanded = false ;
@@ -115,14 +116,19 @@ describe('TreeKeyManager', () => {
115116 FakeArrayTreeKeyManagerItem | FakeObservableTreeKeyManagerItem
116117 > ;
117118
119+ let parentItem : FakeArrayTreeKeyManagerItem | FakeObservableTreeKeyManagerItem ; // index 0
120+ let childItem : FakeArrayTreeKeyManagerItem | FakeObservableTreeKeyManagerItem ; // index 1
121+ let childItemWithNoChildren : FakeArrayTreeKeyManagerItem | FakeObservableTreeKeyManagerItem ; // index 3
122+ let lastItem : FakeArrayTreeKeyManagerItem | FakeObservableTreeKeyManagerItem ; // index 5
123+
118124 beforeEach ( ( ) => {
119125 itemList = new QueryList < FakeArrayTreeKeyManagerItem | FakeObservableTreeKeyManagerItem > ( ) ;
120- const parent1 = new itemParam . constructor ( 'parent1 ' ) ;
121- const parent1Child1 = new itemParam . constructor ( 'parent1Child1 ' ) ;
122- const parent1Child1Child1 = new itemParam . constructor ( 'parent1Child1Child1 ' ) ;
123- const parent1Child2 = new itemParam . constructor ( 'parent1Child2 ' ) ;
124- const parent2 = new itemParam . constructor ( 'parent2 ' ) ;
125- const parent2Child1 = new itemParam . constructor ( 'parent2Child1 ' ) ;
126+ const parent1 = new itemParam . constructor ( 'one ' ) ;
127+ const parent1Child1 = new itemParam . constructor ( 'two ' ) ;
128+ const parent1Child1Child1 = new itemParam . constructor ( 'three ' ) ;
129+ const parent1Child2 = new itemParam . constructor ( 'four ' ) ;
130+ const parent2 = new itemParam . constructor ( 'five ' ) ;
131+ const parent2Child1 = new itemParam . constructor ( 'six ' ) ;
126132
127133 parent1 . _children = [ parent1Child1 , parent1Child2 ] ;
128134 parent1Child1 . _parent = parent1 ;
@@ -132,6 +138,11 @@ describe('TreeKeyManager', () => {
132138 parent2 . _children = [ parent2Child1 ] ;
133139 parent2Child1 . _parent = parent2 ;
134140
141+ parentItem = parent1 ;
142+ childItem = parent1Child1 ;
143+ childItemWithNoChildren = parent1Child2 ;
144+ lastItem = parent2Child1 ;
145+
135146 itemList . reset ( [
136147 parent1 ,
137148 parent1Child1 ,
@@ -155,16 +166,12 @@ describe('TreeKeyManager', () => {
155166 keyManager . onClick ( itemList . get ( 0 ) ! ) ;
156167
157168 expect ( keyManager . getActiveItemIndex ( ) ) . withContext ( 'active item index' ) . toBe ( 0 ) ;
158- expect ( keyManager . getActiveItem ( ) ?. getLabel ( ) )
159- . withContext ( 'active item label' )
160- . toBe ( 'parent1' ) ;
169+ expect ( keyManager . getActiveItem ( ) ?. getLabel ( ) ) . withContext ( 'active item label' ) . toBe ( 'one' ) ;
161170 itemList . reset ( [ new FakeObservableTreeKeyManagerItem ( 'parent0' ) , ...itemList . toArray ( ) ] ) ;
162171 itemList . notifyOnChanges ( ) ;
163172
164173 expect ( keyManager . getActiveItemIndex ( ) ) . withContext ( 'active item index' ) . toBe ( 1 ) ;
165- expect ( keyManager . getActiveItem ( ) ?. getLabel ( ) )
166- . withContext ( 'active item label' )
167- . toBe ( 'parent1' ) ;
174+ expect ( keyManager . getActiveItem ( ) ?. getLabel ( ) ) . withContext ( 'active item label' ) . toBe ( 'one' ) ;
168175 } ) ;
169176
170177 describe ( 'Key events' , ( ) => {
@@ -728,6 +735,217 @@ describe('TreeKeyManager', () => {
728735 } ) ;
729736 }
730737 } ) ;
738+
739+ describe ( 'typeahead mode' , ( ) => {
740+ const debounceInterval = 300 ;
741+
742+ beforeEach ( ( ) => {
743+ keyManager = new TreeKeyManager ( {
744+ items : itemList ,
745+ typeAheadDebounceInterval : debounceInterval ,
746+ } ) ;
747+ } ) ;
748+
749+ it ( 'should throw if the items do not implement the getLabel method' , ( ) => {
750+ const invalidQueryList = new QueryList < any > ( ) ;
751+ invalidQueryList . reset ( [ { disabled : false } ] ) ;
752+
753+ expect (
754+ ( ) =>
755+ new TreeKeyManager ( {
756+ items : invalidQueryList ,
757+ typeAheadDebounceInterval : true ,
758+ } ) ,
759+ ) . toThrowError ( / m u s t i m p l e m e n t / ) ;
760+ } ) ;
761+
762+ it ( 'should debounce the input key presses' , fakeAsync ( ( ) => {
763+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 79 , 'o' ) ) ; // types "o"
764+ tick ( 1 ) ;
765+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 78 , 'n' ) ) ; // types "n"
766+ tick ( 1 ) ;
767+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 69 , 'e' ) ) ; // types "e"
768+
769+ expect ( keyManager . getActiveItemIndex ( ) )
770+ . withContext ( 'active item index, before debounce interval' )
771+ . not . toBe ( 0 ) ;
772+
773+ tick ( debounceInterval - 1 ) ;
774+
775+ expect ( keyManager . getActiveItemIndex ( ) )
776+ . withContext ( 'active item index, after partial debounce interval' )
777+ . not . toBe ( 0 ) ;
778+
779+ tick ( 1 ) ;
780+
781+ expect ( keyManager . getActiveItemIndex ( ) )
782+ . withContext ( 'active item index, after full debounce interval' )
783+ . toBe ( 0 ) ;
784+ } ) ) ;
785+
786+ it ( 'uses a default debounce interval' , fakeAsync ( ( ) => {
787+ const defaultInterval = 200 ;
788+ keyManager = new TreeKeyManager ( {
789+ items : itemList ,
790+ typeAheadDebounceInterval : true ,
791+ } ) ;
792+
793+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 79 , 'o' ) ) ; // types "o"
794+ tick ( 1 ) ;
795+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 78 , 'n' ) ) ; // types "n"
796+ tick ( 1 ) ;
797+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 69 , 'e' ) ) ; // types "e"
798+
799+ expect ( keyManager . getActiveItemIndex ( ) )
800+ . withContext ( 'active item index, before debounce interval' )
801+ . not . toBe ( 0 ) ;
802+
803+ tick ( defaultInterval - 1 ) ;
804+
805+ expect ( keyManager . getActiveItemIndex ( ) )
806+ . withContext ( 'active item index, after partial debounce interval' )
807+ . not . toBe ( 0 ) ;
808+
809+ tick ( 1 ) ;
810+
811+ expect ( keyManager . getActiveItemIndex ( ) )
812+ . withContext ( 'active item index, after full debounce interval' )
813+ . toBe ( 0 ) ;
814+ } ) ) ;
815+
816+ it ( 'should focus the first item that starts with a letter' , fakeAsync ( ( ) => {
817+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 84 , 't' ) ) ; // types "t"
818+
819+ tick ( debounceInterval ) ;
820+
821+ expect ( keyManager . getActiveItemIndex ( ) ) . withContext ( 'active item index' ) . toBe ( 1 ) ;
822+ } ) ) ;
823+
824+ it ( 'should focus the first item that starts with sequence of letters' , fakeAsync ( ( ) => {
825+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 84 , 't' ) ) ; // types "t"
826+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 72 , 'h' ) ) ; // types "h"
827+
828+ tick ( debounceInterval ) ;
829+
830+ expect ( keyManager . getActiveItemIndex ( ) ) . withContext ( 'active item index' ) . toBe ( 2 ) ;
831+ } ) ) ;
832+
833+ it ( 'should cancel any pending timers if a navigation key is pressed' , fakeAsync ( ( ) => {
834+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 84 , 't' ) ) ; // types "t"
835+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 72 , 'h' ) ) ; // types "h"
836+ keyManager . onKeydown ( fakeKeyEvents . downArrow ) ;
837+
838+ tick ( debounceInterval ) ;
839+
840+ expect ( keyManager . getActiveItemIndex ( ) ) . withContext ( 'active item index' ) . toBe ( 0 ) ;
841+ } ) ) ;
842+
843+ it ( 'should handle non-English input' , fakeAsync ( ( ) => {
844+ itemList . reset ( [
845+ new itemParam . constructor ( 'едно' ) ,
846+ new itemParam . constructor ( 'две' ) ,
847+ new itemParam . constructor ( 'три' ) ,
848+ ] ) ;
849+ itemList . notifyOnChanges ( ) ;
850+
851+ const keyboardEvent = createKeyboardEvent ( 'keydown' , 68 , 'д' ) ;
852+
853+ keyManager . onKeydown ( keyboardEvent ) ; // types "д"
854+ tick ( debounceInterval ) ;
855+
856+ expect ( keyManager . getActiveItemIndex ( ) ) . withContext ( 'active item index' ) . toBe ( 1 ) ;
857+ } ) ) ;
858+
859+ it ( 'should handle non-letter characters' , fakeAsync ( ( ) => {
860+ itemList . reset ( [
861+ new itemParam . constructor ( '[]' ) ,
862+ new itemParam . constructor ( '321' ) ,
863+ new itemParam . constructor ( '`!?' ) ,
864+ ] ) ;
865+ itemList . notifyOnChanges ( ) ;
866+
867+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 192 , '`' ) ) ; // types "`"
868+ tick ( debounceInterval ) ;
869+ expect ( keyManager . getActiveItemIndex ( ) ) . withContext ( 'active item index' ) . toBe ( 2 ) ;
870+
871+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 51 , '3' ) ) ; // types "3"
872+ tick ( debounceInterval ) ;
873+ expect ( keyManager . getActiveItemIndex ( ) ) . withContext ( 'active item index' ) . toBe ( 1 ) ;
874+
875+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 219 , '[' ) ) ; // types "["
876+ tick ( debounceInterval ) ;
877+ expect ( keyManager . getActiveItemIndex ( ) ) . withContext ( 'active item index' ) . toBe ( 0 ) ;
878+ } ) ) ;
879+
880+ it ( 'should not focus disabled items' , fakeAsync ( ( ) => {
881+ expect ( keyManager . getActiveItemIndex ( ) ) . withContext ( 'initial active item index' ) . toBe ( - 1 ) ;
882+
883+ parentItem . isDisabled = true ;
884+
885+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 79 , 'o' ) ) ; // types "o"
886+ tick ( debounceInterval ) ;
887+
888+ expect ( keyManager . getActiveItemIndex ( ) ) . withContext ( 'initial active item index' ) . toBe ( - 1 ) ;
889+ } ) ) ;
890+
891+ it ( 'should start looking for matches after the active item' , fakeAsync ( ( ) => {
892+ const frodo = new itemParam . constructor ( 'Frodo' ) ;
893+ itemList . reset ( [
894+ new itemParam . constructor ( 'Bilbo' ) ,
895+ frodo ,
896+ new itemParam . constructor ( 'Pippin' ) ,
897+ new itemParam . constructor ( 'Boromir' ) ,
898+ new itemParam . constructor ( 'Aragorn' ) ,
899+ ] ) ;
900+ itemList . notifyOnChanges ( ) ;
901+
902+ keyManager . onClick ( frodo ) ;
903+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 66 , 'b' ) ) ;
904+ tick ( debounceInterval ) ;
905+
906+ expect ( keyManager . getActiveItemIndex ( ) ) . withContext ( 'active item index' ) . toBe ( 3 ) ;
907+ } ) ) ;
908+
909+ it ( 'should wrap back around if there were no matches after the active item' , fakeAsync ( ( ) => {
910+ const boromir = new itemParam . constructor ( 'Boromir' ) ;
911+ itemList . reset ( [
912+ new itemParam . constructor ( 'Bilbo' ) ,
913+ new itemParam . constructor ( 'Frodo' ) ,
914+ new itemParam . constructor ( 'Pippin' ) ,
915+ boromir ,
916+ new itemParam . constructor ( 'Aragorn' ) ,
917+ ] ) ;
918+ itemList . notifyOnChanges ( ) ;
919+
920+ keyManager . onClick ( boromir ) ;
921+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 66 , 'b' ) ) ;
922+ tick ( debounceInterval ) ;
923+
924+ expect ( keyManager . getActiveItemIndex ( ) ) . withContext ( 'active item index' ) . toBe ( 0 ) ;
925+ } ) ) ;
926+
927+ it ( 'should wrap back around if the last item is active' , fakeAsync ( ( ) => {
928+ keyManager . onClick ( lastItem ) ;
929+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 79 , 'o' ) ) ;
930+ tick ( debounceInterval ) ;
931+
932+ expect ( keyManager . getActiveItemIndex ( ) ) . withContext ( 'active item index' ) . toBe ( 0 ) ;
933+ } ) ) ;
934+
935+ it ( 'should be able to select the first item' , fakeAsync ( ( ) => {
936+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 79 , 'o' ) ) ;
937+ tick ( debounceInterval ) ;
938+
939+ expect ( keyManager . getActiveItemIndex ( ) ) . withContext ( 'active item index' ) . toBe ( 0 ) ;
940+ } ) ) ;
941+
942+ it ( 'should not do anything if there is no match' , fakeAsync ( ( ) => {
943+ keyManager . onKeydown ( createKeyboardEvent ( 'keydown' , 87 , 'w' ) ) ;
944+ tick ( debounceInterval ) ;
945+
946+ expect ( keyManager . getActiveItemIndex ( ) ) . withContext ( 'active item index' ) . toBe ( - 1 ) ;
947+ } ) ) ;
948+ } ) ;
731949 } ) ;
732950 }
733951} ) ;
0 commit comments