diff --git a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.html b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.html
index 00517a74cf0..b21ff83961d 100644
--- a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.html
+++ b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.html
@@ -10,10 +10,11 @@
(click)="toggleSection($event)"
(keydown.space)="$event.preventDefault()"
(keydown.tab)="deactivateSection($event, false)"
+ (keydown.arrowDown)="navigateDropdown($event)"
aria-haspopup="menu"
data-test="navbar-section-toggler"
[attr.aria-expanded]="(active$ | async).valueOf()"
- [attr.aria-controls]="expandableNavbarSectionId(section.id)"
+ [attr.aria-controls]="expandableNavbarSectionId()"
class="d-flex flex-row flex-nowrap align-items-center gapx-1 ds-menu-toggler-wrapper"
[class.disabled]="section.model?.disabled"
id="browseDropdown">
@@ -24,7 +25,7 @@
diff --git a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.ts b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.ts
index 9526b0e5d7f..c1839cc1f55 100644
--- a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.ts
+++ b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.ts
@@ -5,6 +5,8 @@ import {
NgIf,
} from '@angular/common';
import {
+ AfterViewChecked,
+ ChangeDetectorRef,
Component,
HostListener,
Inject,
@@ -18,6 +20,7 @@ import { first } from 'rxjs/operators';
import { HostWindowService } from '../../shared/host-window.service';
import { MenuService } from '../../shared/menu/menu.service';
import { MenuID } from '../../shared/menu/menu-id.model';
+import { HoverOutsideDirective } from '../../shared/utils/hover-outside.directive';
import { VarDirective } from '../../shared/utils/var.directive';
import { NavbarSectionComponent } from '../navbar-section/navbar-section.component';
@@ -29,9 +32,17 @@ import { NavbarSectionComponent } from '../navbar-section/navbar-section.compone
templateUrl: './expandable-navbar-section.component.html',
styleUrls: ['./expandable-navbar-section.component.scss'],
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 {
/**
* This section resides in the Public Navbar
*/
@@ -52,6 +63,13 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
*/
isMobile$: Observable;
+ /**
+ * 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(
@@ -69,7 +87,8 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
constructor(@Inject('sectionDataProvider') menuSection,
protected menuService: MenuService,
protected injector: Injector,
- private windowService: HostWindowService,
+ protected windowService: HostWindowService,
+ protected cdr: ChangeDetectorRef,
) {
super(menuSection, menuService, injector);
this.isMobile$ = this.windowService.isMobile();
@@ -77,6 +96,22 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
ngOnInit() {
super.ngOnInit();
+ this.subs.push(this.active$.subscribe((active: boolean) => {
+ if (active === true) {
+ this.addArrowEventListeners = true;
+ this.cdr.detectChanges();
+ }
+ }));
+ }
+
+ 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;
+ }
}
/**
@@ -111,9 +146,29 @@ 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`;
+ }
+
+ navigateDropdown(event: KeyboardEvent): void {
+ if (event.key === 'Tab') {
+ this.deactivateSection(event, false);
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+
+ const items = document.querySelectorAll(`#${this.expandableNavbarSectionId()} *[role="menuitem"]`);
+ if (items.length === 0) {
+ return;
+ }
+ const currentIndex: number = Array.from(items).findIndex(item => 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();
+ }
}
}
diff --git a/src/app/shared/utils/hover-outside.directive.ts b/src/app/shared/utils/hover-outside.directive.ts
new file mode 100644
index 00000000000..85232e6bea4
--- /dev/null
+++ b/src/app/shared/utils/hover-outside.directive.ts
@@ -0,0 +1,38 @@
+import {
+ Directive,
+ ElementRef,
+ EventEmitter,
+ HostListener,
+ Output,
+} from '@angular/core';
+
+/**
+ * Directive to detect when the user hovers outside of the element the directive was put on
+ *
+ * BEWARE: it's probably not good for performance to use this excessively (on {@link ExpandableNavbarSectionComponent}
+ * for example, a workaround for this problem was to add an `*ngIf` to prevent this Directive from always being active)
+ */
+@Directive({
+ selector: '[dsHoverOutside]',
+ standalone: true,
+})
+export class HoverOutsideDirective {
+ /**
+ * Emits null when the user hovers outside of the element
+ */
+ @Output()
+ public dsHoverOutside = new EventEmitter();
+
+ constructor(private _elementRef: ElementRef) {}
+
+ @HostListener('document:mouseover', ['$event'])
+ public onMouseOver(event: MouseEvent) {
+ const hostElement = this._elementRef.nativeElement;
+ const targetElement = event.target as HTMLElement;
+ const hoveredInside = hostElement.contains(targetElement);
+
+ if (!hoveredInside) {
+ this.dsHoverOutside.emit(null);
+ }
+ }
+}
diff --git a/src/themes/custom/app/navbar/expandable-navbar-section/expandable-navbar-section.component.ts b/src/themes/custom/app/navbar/expandable-navbar-section/expandable-navbar-section.component.ts
index f0e2ebd5029..167b805fd58 100644
--- a/src/themes/custom/app/navbar/expandable-navbar-section/expandable-navbar-section.component.ts
+++ b/src/themes/custom/app/navbar/expandable-navbar-section/expandable-navbar-section.component.ts
@@ -9,11 +9,9 @@ import { RouterLinkActive } from '@angular/router';
import { ExpandableNavbarSectionComponent as BaseComponent } from '../../../../../app/navbar/expandable-navbar-section/expandable-navbar-section.component';
import { slide } from '../../../../../app/shared/animations/slide';
+import { HoverOutsideDirective } from '../../../../../app/shared/utils/hover-outside.directive';
import { VarDirective } from '../../../../../app/shared/utils/var.directive';
-/**
- * Represents an expandable section in the navbar
- */
@Component({
selector: 'ds-themed-expandable-navbar-section',
// templateUrl: './expandable-navbar-section.component.html',
@@ -22,7 +20,15 @@ import { VarDirective } from '../../../../../app/shared/utils/var.directive';
styleUrls: ['../../../../../app/navbar/expandable-navbar-section/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 BaseComponent {
}