@@ -27,6 +27,7 @@ import {
2727 dispatchKeyboardEvent ,
2828 createKeyboardEvent ,
2929 dispatchEvent ,
30+ dispatchMouseEvent ,
3031} from '@angular/cdk/testing/private' ;
3132import { CdkMenuBar } from './menu-bar' ;
3233import { CdkMenuModule } from './menu-module' ;
@@ -837,6 +838,195 @@ describe('MenuBar', () => {
837838 . toBe ( 1 ) ;
838839 } ) ;
839840 } ) ;
841+
842+ describe ( 'Mouse handling' , ( ) => {
843+ let fixture : ComponentFixture < MultiMenuWithSubmenu > ;
844+ let nativeMenus : HTMLElement [ ] ;
845+ let menuBarNativeItems : HTMLButtonElement [ ] ;
846+ let fileMenuNativeItems : HTMLButtonElement [ ] ;
847+ let shareMenuNativeItems : HTMLButtonElement [ ] ;
848+
849+ /** Get menus and items used for tests. */
850+ function grabElementsForTesting ( ) {
851+ nativeMenus = fixture . componentInstance . nativeMenus . map ( e => e . nativeElement ) ;
852+
853+ menuBarNativeItems = fixture . componentInstance . nativeItems
854+ . map ( e => e . nativeElement )
855+ . slice ( 0 , 2 ) ; // menu bar has the first 2 menu items
856+
857+ fileMenuNativeItems = fixture . componentInstance . nativeItems
858+ . map ( e => e . nativeElement )
859+ . slice ( 2 , 5 ) ; // file menu has the next 3 menu items
860+
861+ shareMenuNativeItems = fixture . componentInstance . nativeItems
862+ . map ( e => e . nativeElement )
863+ . slice ( 5 , 7 ) ; // share menu has the next 2 menu items
864+ }
865+
866+ /** Run change detection and extract then set the rendered elements. */
867+ function detectChanges ( ) {
868+ fixture . detectChanges ( ) ;
869+ grabElementsForTesting ( ) ;
870+ }
871+
872+ /** Mock mouse events required to open the file menu. */
873+ function openFileMenu ( ) {
874+ dispatchMouseEvent ( menuBarNativeItems [ 0 ] , 'mouseenter' ) ;
875+ dispatchMouseEvent ( menuBarNativeItems [ 0 ] , 'click' ) ;
876+ detectChanges ( ) ;
877+ }
878+
879+ /** Mock mouse events required to open the share menu. */
880+ function openShareMenu ( ) {
881+ dispatchMouseEvent ( fileMenuNativeItems [ 1 ] , 'mouseenter' ) ;
882+ detectChanges ( ) ;
883+ }
884+
885+ beforeEach ( async ( ( ) => {
886+ TestBed . configureTestingModule ( {
887+ imports : [ CdkMenuModule ] ,
888+ declarations : [ MultiMenuWithSubmenu ] ,
889+ } ) . compileComponents ( ) ;
890+ } ) ) ;
891+
892+ beforeEach ( ( ) => {
893+ fixture = TestBed . createComponent ( MultiMenuWithSubmenu ) ;
894+ detectChanges ( ) ;
895+ } ) ;
896+
897+ it ( 'should toggle menu from menu bar when clicked' , ( ) => {
898+ openFileMenu ( ) ;
899+
900+ expect ( nativeMenus . length ) . toBe ( 1 ) ;
901+ expect ( nativeMenus [ 0 ] . id ) . toBe ( 'file_menu' ) ;
902+
903+ dispatchMouseEvent ( menuBarNativeItems [ 0 ] , 'click' ) ;
904+ detectChanges ( ) ;
905+
906+ expect ( nativeMenus . length ) . toBe ( 0 ) ;
907+ } ) ;
908+
909+ it ( 'should not open menu when hovering over trigger in menu bar with no open siblings' , ( ) => {
910+ dispatchMouseEvent ( menuBarNativeItems [ 0 ] , 'mouseenter' ) ;
911+ detectChanges ( ) ;
912+
913+ expect ( nativeMenus . length ) . toBe ( 0 ) ;
914+ } ) ;
915+
916+ it (
917+ 'should not change focused items when hovering over trigger in menu bar with no open ' +
918+ 'siblings' ,
919+ ( ) => {
920+ dispatchMouseEvent ( menuBarNativeItems [ 0 ] , 'mouseenter' ) ;
921+ detectChanges ( ) ;
922+
923+ expect ( document . querySelector ( ':focus' ) ) . not . toEqual ( menuBarNativeItems [ 0 ] ) ;
924+ expect ( document . querySelector ( ':focus' ) ) . not . toEqual ( menuBarNativeItems [ 1 ] ) ;
925+ }
926+ ) ;
927+
928+ it (
929+ 'should toggle open menus in menu bar if sibling is open when mouse moves from one item ' +
930+ 'to the other' ,
931+ ( ) => {
932+ openFileMenu ( ) ;
933+
934+ dispatchMouseEvent ( menuBarNativeItems [ 1 ] , 'mouseenter' ) ;
935+ detectChanges ( ) ;
936+
937+ expect ( nativeMenus . length ) . toBe ( 1 ) ;
938+ expect ( nativeMenus [ 0 ] . id ) . toBe ( 'edit_menu' ) ;
939+
940+ dispatchMouseEvent ( menuBarNativeItems [ 0 ] , 'mouseenter' ) ;
941+ detectChanges ( ) ;
942+
943+ expect ( nativeMenus . length ) . toBe ( 1 ) ;
944+ expect ( nativeMenus [ 0 ] . id ) . toBe ( 'file_menu' ) ;
945+
946+ dispatchMouseEvent ( menuBarNativeItems [ 1 ] , 'mouseenter' ) ;
947+ detectChanges ( ) ;
948+
949+ expect ( nativeMenus . length ) . toBe ( 1 ) ;
950+ expect ( nativeMenus [ 0 ] . id ) . toBe ( 'edit_menu' ) ;
951+ }
952+ ) ;
953+
954+ it ( 'should not close the menu when re-hovering the trigger' , ( ) => {
955+ openFileMenu ( ) ;
956+
957+ dispatchMouseEvent ( menuBarNativeItems [ 0 ] , 'mouseenter' ) ;
958+
959+ expect ( nativeMenus . length ) . toBe ( 1 ) ;
960+ expect ( nativeMenus [ 0 ] . id ) . toBe ( 'file_menu' ) ;
961+ } ) ;
962+
963+ it ( 'should open a submenu when hovering over a trigger in a menu with no siblings open' , ( ) => {
964+ openFileMenu ( ) ;
965+
966+ openShareMenu ( ) ;
967+
968+ expect ( nativeMenus . length ) . toBe ( 2 ) ;
969+ expect ( nativeMenus [ 0 ] . id ) . toBe ( 'file_menu' ) ;
970+ expect ( nativeMenus [ 1 ] . id ) . toBe ( 'share_menu' ) ;
971+ } ) ;
972+
973+ it ( 'should close menu when hovering over non-triggering sibling menu item' , ( ) => {
974+ openFileMenu ( ) ;
975+ openShareMenu ( ) ;
976+
977+ dispatchMouseEvent ( fileMenuNativeItems [ 0 ] , 'mouseenter' ) ;
978+ detectChanges ( ) ;
979+
980+ expect ( nativeMenus . length ) . toBe ( 1 ) ;
981+ expect ( nativeMenus [ 0 ] . id ) . toBe ( 'file_menu' ) ;
982+ } ) ;
983+
984+ it ( 'should retain open menus when hovering over root level trigger which opened them' , ( ) => {
985+ openFileMenu ( ) ;
986+ openShareMenu ( ) ;
987+
988+ dispatchMouseEvent ( menuBarNativeItems [ 0 ] , 'mouseenter' ) ;
989+ detectChanges ( ) ;
990+
991+ expect ( nativeMenus . length ) . toBe ( 2 ) ;
992+ } ) ;
993+
994+ it ( 'should close out the menu tree when hovering over sibling item in menu bar' , ( ) => {
995+ openFileMenu ( ) ;
996+ openShareMenu ( ) ;
997+
998+ dispatchMouseEvent ( menuBarNativeItems [ 1 ] , 'mouseenter' ) ;
999+ detectChanges ( ) ;
1000+
1001+ expect ( nativeMenus . length ) . toBe ( 1 ) ;
1002+ expect ( nativeMenus [ 0 ] . id ) . toBe ( 'edit_menu' ) ;
1003+ } ) ;
1004+
1005+ it ( 'should close out the menu tree when clicking a non-triggering menu item' , ( ) => {
1006+ openFileMenu ( ) ;
1007+ openShareMenu ( ) ;
1008+
1009+ dispatchMouseEvent ( shareMenuNativeItems [ 0 ] , 'mouseenter' ) ;
1010+ dispatchMouseEvent ( shareMenuNativeItems [ 0 ] , 'click' ) ;
1011+ detectChanges ( ) ;
1012+
1013+ expect ( nativeMenus . length ) . toBe ( 0 ) ;
1014+ } ) ;
1015+
1016+ it (
1017+ 'should allow keyboard down arrow to focus next item after mouse sets focus to' +
1018+ ' initial item' ,
1019+ ( ) => {
1020+ openFileMenu ( ) ;
1021+ dispatchMouseEvent ( fileMenuNativeItems [ 0 ] , 'mouseenter' ) ;
1022+ detectChanges ( ) ;
1023+
1024+ dispatchKeyboardEvent ( nativeMenus [ 0 ] , 'keydown' , DOWN_ARROW ) ;
1025+
1026+ expect ( document . querySelector ( ':focus' ) ) . toEqual ( fileMenuNativeItems [ 1 ] ) ;
1027+ }
1028+ ) ;
1029+ } ) ;
8401030} ) ;
8411031
8421032@Component ( {
0 commit comments