Skip to content

Commit

Permalink
116404: Collapse the expandable menu when hovering outside of it
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandrevryghem committed Jul 5, 2024
1 parent d494a84 commit c6caf7e
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand All @@ -24,7 +25,7 @@
<i class="fas fa-caret-down fa-xs toggle-menu-icon" aria-hidden="true"></i>
</a>
<div *ngIf="(active$ | async).valueOf() === true" (click)="deactivateSection($event)"
[id]="expandableNavbarSectionId(section.id)"
[id]="expandableNavbarSectionId()" (dsHoverOutside)="deactivateSection($event, false)"
role="menu"
class="dropdown-menu show nav-dropdown-menu m-0 shadow-none border-top-0 px-3 px-md-0 pt-0 pt-md-1">
<div *ngFor="let subSection of (subSections$ | async)" class="text-nowrap" role="presentation">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
NgIf,
} from '@angular/common';
import {
AfterViewChecked,
ChangeDetectorRef,
Component,
HostListener,
Inject,
Expand All @@ -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';

Expand All @@ -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
*/
Expand All @@ -52,6 +63,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(
Expand All @@ -69,14 +87,31 @@ 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();
}

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;
}
}

/**
Expand Down Expand Up @@ -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();
}
}
}
38 changes: 38 additions & 0 deletions src/app/shared/utils/hover-outside.directive.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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 {
}

0 comments on commit c6caf7e

Please sign in to comment.