116404: Added navigation with arrow keys in navbar & collapsed the expandable menu when hovering outside of it

(cherry picked from commit 05232cdf2b)
This commit is contained in:
Alexandre Vryghem
2024-07-05 13:30:01 +02:00
committed by github-actions[bot]
parent 17e03330d0
commit b9b1d3fd8d
4 changed files with 185 additions and 43 deletions

View File

@@ -5,11 +5,14 @@ import {
NgIf,
} from '@angular/common';
import {
AfterViewChecked,
Component,
ElementRef,
HostListener,
Inject,
Injector,
OnInit,
ViewChild,
} from '@angular/core';
import { RouterLinkActive } from '@angular/router';
import { Observable } from 'rxjs';
@@ -19,6 +22,8 @@ import { slide } from '../../shared/animations/slide';
import { HostWindowService } from '../../shared/host-window.service';
import { MenuService } from '../../shared/menu/menu.service';
import { MenuID } from '../../shared/menu/menu-id.model';
import { MenuSection } from '../../shared/menu/menu-section.model';
import { HoverOutsideDirective } from '../../shared/utils/hover-outside.directive';
import { VarDirective } from '../../shared/utils/var.directive';
import { NavbarSectionComponent } from '../navbar-section/navbar-section.component';
@@ -31,9 +36,20 @@ import { NavbarSectionComponent } from '../navbar-section/navbar-section.compone
styleUrls: ['./expandable-navbar-section.component.scss'],
animations: [slide],
standalone: true,
imports: [VarDirective, RouterLinkActive, NgComponentOutlet, NgIf, NgFor, AsyncPipe],
imports: [
AsyncPipe,
HoverOutsideDirective,
NgComponentOutlet,
NgFor,
NgIf,
RouterLinkActive,
VarDirective,
],
})
export class ExpandableNavbarSectionComponent extends NavbarSectionComponent implements OnInit {
export class ExpandableNavbarSectionComponent extends NavbarSectionComponent implements AfterViewChecked, OnInit {
@ViewChild('expandableNavbarSectionContainer') expandableNavbarSection: ElementRef;
/**
* This section resides in the Public Navbar
*/
@@ -54,6 +70,13 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
*/
isMobile$: Observable<boolean>;
/**
* Boolean used to add the event listeners to the items in the expandable menu when expanded. This is done for
* performance reasons, there is currently an *ngIf on the menu to prevent the {@link HoverOutsideDirective} to tank
* performance when not expanded.
*/
addArrowEventListeners = false;
@HostListener('window:resize', ['$event'])
onResize() {
this.isMobile$.pipe(
@@ -68,17 +91,33 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
});
}
constructor(@Inject('sectionDataProvider') menuSection,
protected menuService: MenuService,
protected injector: Injector,
private windowService: HostWindowService,
constructor(
@Inject('sectionDataProvider') public section: MenuSection,
protected menuService: MenuService,
protected injector: Injector,
protected windowService: HostWindowService,
) {
super(menuSection, menuService, injector);
super(section, menuService, injector);
this.isMobile$ = this.windowService.isMobile();
}
ngOnInit() {
super.ngOnInit();
this.subs.push(this.active$.subscribe((active: boolean) => {
if (active === true) {
this.addArrowEventListeners = true;
}
}));
}
ngAfterViewChecked(): void {
if (this.addArrowEventListeners) {
const dropdownItems = document.querySelectorAll(`#${this.expandableNavbarSectionId()} *[role="menuitem"]`);
dropdownItems.forEach(item => {
item.addEventListener('keydown', this.navigateDropdown.bind(this));
});
this.addArrowEventListeners = false;
}
}
/**
@@ -113,9 +152,54 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
/**
* returns the ID of the DOM element representing the navbar section
* @param sectionId
*/
expandableNavbarSectionId(sectionId: string) {
return `expandable-navbar-section-${sectionId}-dropdown`;
expandableNavbarSectionId(): string {
return `expandable-navbar-section-${this.section.id}-dropdown`;
}
/**
* Handles the navigation between the menu items
*
* @param event
*/
navigateDropdown(event: KeyboardEvent): void {
if (event.key === 'Tab') {
this.deactivateSection(event, false);
return;
}
event.preventDefault();
event.stopPropagation();
const items: NodeListOf<Element> = document.querySelectorAll(`#${this.expandableNavbarSectionId()} *[role="menuitem"]`);
if (items.length === 0) {
return;
}
const currentIndex: number = Array.from(items).findIndex((item: Element) => item === event.target);
if (event.key === 'ArrowDown') {
(items[(currentIndex + 1) % items.length] as HTMLElement).focus();
} else if (event.key === 'ArrowUp') {
(items[(currentIndex - 1 + items.length) % items.length] as HTMLElement).focus();
}
}
/**
* Handles all the keydown events on the dropdown toggle
*
* @param event
*/
keyDown(event: KeyboardEvent): void {
switch (event.code) {
// Works for both Tab & Shift Tab
case 'Tab':
this.deactivateSection(event, false);
break;
case 'ArrowDown':
this.navigateDropdown(event);
break;
case 'Space':
event.preventDefault();
break;
}
}
}