@@ -5,11 +5,14 @@ import {
55 NgIf ,
66} from '@angular/common' ;
77import {
8+ AfterViewChecked ,
89 Component ,
10+ ElementRef ,
911 HostListener ,
1012 Inject ,
1113 Injector ,
1214 OnInit ,
15+ ViewChild ,
1316} from '@angular/core' ;
1417import { RouterLinkActive } from '@angular/router' ;
1518import { Observable } from 'rxjs' ;
@@ -19,6 +22,8 @@ import { slide } from '../../shared/animations/slide';
1922import { HostWindowService } from '../../shared/host-window.service' ;
2023import { MenuService } from '../../shared/menu/menu.service' ;
2124import { MenuID } from '../../shared/menu/menu-id.model' ;
25+ import { MenuSection } from '../../shared/menu/menu-section.model' ;
26+ import { HoverOutsideDirective } from '../../shared/utils/hover-outside.directive' ;
2227import { VarDirective } from '../../shared/utils/var.directive' ;
2328import { NavbarSectionComponent } from '../navbar-section/navbar-section.component' ;
2429
@@ -31,9 +36,20 @@ import { NavbarSectionComponent } from '../navbar-section/navbar-section.compone
3136 styleUrls : [ './expandable-navbar-section.component.scss' ] ,
3237 animations : [ slide ] ,
3338 standalone : true ,
34- imports : [ VarDirective , RouterLinkActive , NgComponentOutlet , NgIf , NgFor , AsyncPipe ] ,
39+ imports : [
40+ AsyncPipe ,
41+ HoverOutsideDirective ,
42+ NgComponentOutlet ,
43+ NgFor ,
44+ NgIf ,
45+ RouterLinkActive ,
46+ VarDirective ,
47+ ] ,
3548} )
36- export class ExpandableNavbarSectionComponent extends NavbarSectionComponent implements OnInit {
49+ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent implements AfterViewChecked , OnInit {
50+
51+ @ViewChild ( 'expandableNavbarSectionContainer' ) expandableNavbarSection : ElementRef ;
52+
3753 /**
3854 * This section resides in the Public Navbar
3955 */
@@ -54,6 +70,13 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
5470 */
5571 isMobile$ : Observable < boolean > ;
5672
73+ /**
74+ * Boolean used to add the event listeners to the items in the expandable menu when expanded. This is done for
75+ * performance reasons, there is currently an *ngIf on the menu to prevent the {@link HoverOutsideDirective} to tank
76+ * performance when not expanded.
77+ */
78+ addArrowEventListeners = false ;
79+
5780 @HostListener ( 'window:resize' , [ '$event' ] )
5881 onResize ( ) {
5982 this . isMobile$ . pipe (
@@ -68,17 +91,33 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
6891 } ) ;
6992 }
7093
71- constructor ( @Inject ( 'sectionDataProvider' ) menuSection ,
72- protected menuService : MenuService ,
73- protected injector : Injector ,
74- private windowService : HostWindowService ,
94+ constructor (
95+ @Inject ( 'sectionDataProvider' ) public section : MenuSection ,
96+ protected menuService : MenuService ,
97+ protected injector : Injector ,
98+ protected windowService : HostWindowService ,
7599 ) {
76- super ( menuSection , menuService , injector ) ;
100+ super ( section , menuService , injector ) ;
77101 this . isMobile$ = this . windowService . isMobile ( ) ;
78102 }
79103
80104 ngOnInit ( ) {
81105 super . ngOnInit ( ) ;
106+ this . subs . push ( this . active$ . subscribe ( ( active : boolean ) => {
107+ if ( active === true ) {
108+ this . addArrowEventListeners = true ;
109+ }
110+ } ) ) ;
111+ }
112+
113+ ngAfterViewChecked ( ) : void {
114+ if ( this . addArrowEventListeners ) {
115+ const dropdownItems = document . querySelectorAll ( `#${ this . expandableNavbarSectionId ( ) } *[role="menuitem"]` ) ;
116+ dropdownItems . forEach ( item => {
117+ item . addEventListener ( 'keydown' , this . navigateDropdown . bind ( this ) ) ;
118+ } ) ;
119+ this . addArrowEventListeners = false ;
120+ }
82121 }
83122
84123 /**
@@ -113,9 +152,54 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
113152
114153 /**
115154 * returns the ID of the DOM element representing the navbar section
116- * @param sectionId
117155 */
118- expandableNavbarSectionId ( sectionId : string ) {
119- return `expandable-navbar-section-${ sectionId } -dropdown` ;
156+ expandableNavbarSectionId ( ) : string {
157+ return `expandable-navbar-section-${ this . section . id } -dropdown` ;
158+ }
159+
160+ /**
161+ * Handles the navigation between the menu items
162+ *
163+ * @param event
164+ */
165+ navigateDropdown ( event : KeyboardEvent ) : void {
166+ if ( event . key === 'Tab' ) {
167+ this . deactivateSection ( event , false ) ;
168+ return ;
169+ }
170+ event . preventDefault ( ) ;
171+ event . stopPropagation ( ) ;
172+
173+ const items : NodeListOf < Element > = document . querySelectorAll ( `#${ this . expandableNavbarSectionId ( ) } *[role="menuitem"]` ) ;
174+ if ( items . length === 0 ) {
175+ return ;
176+ }
177+ const currentIndex : number = Array . from ( items ) . findIndex ( ( item : Element ) => item === event . target ) ;
178+
179+ if ( event . key === 'ArrowDown' ) {
180+ ( items [ ( currentIndex + 1 ) % items . length ] as HTMLElement ) . focus ( ) ;
181+ } else if ( event . key === 'ArrowUp' ) {
182+ ( items [ ( currentIndex - 1 + items . length ) % items . length ] as HTMLElement ) . focus ( ) ;
183+ }
184+ }
185+
186+ /**
187+ * Handles all the keydown events on the dropdown toggle
188+ *
189+ * @param event
190+ */
191+ keyDown ( event : KeyboardEvent ) : void {
192+ switch ( event . code ) {
193+ // Works for both Tab & Shift Tab
194+ case 'Tab' :
195+ this . deactivateSection ( event , false ) ;
196+ break ;
197+ case 'ArrowDown' :
198+ this . navigateDropdown ( event ) ;
199+ break ;
200+ case 'Space' :
201+ event . preventDefault ( ) ;
202+ break ;
203+ }
120204 }
121205}
0 commit comments