mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
253 lines
7.2 KiB
TypeScript
253 lines
7.2 KiB
TypeScript
import {
|
|
AsyncPipe,
|
|
NgComponentOutlet,
|
|
NgFor,
|
|
NgIf,
|
|
} from '@angular/common';
|
|
import {
|
|
AfterViewChecked,
|
|
Component,
|
|
HostListener,
|
|
Inject,
|
|
Injector,
|
|
OnDestroy,
|
|
OnInit,
|
|
} from '@angular/core';
|
|
import { RouterLinkActive } from '@angular/router';
|
|
import { Observable } from 'rxjs';
|
|
import { first } from 'rxjs/operators';
|
|
|
|
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 { NavbarSectionComponent } from '../navbar-section/navbar-section.component';
|
|
|
|
/**
|
|
* Represents an expandable section in the navbar
|
|
*/
|
|
@Component({
|
|
selector: 'ds-base-expandable-navbar-section',
|
|
templateUrl: './expandable-navbar-section.component.html',
|
|
styleUrls: ['./expandable-navbar-section.component.scss'],
|
|
animations: [slide],
|
|
standalone: true,
|
|
imports: [
|
|
AsyncPipe,
|
|
HoverOutsideDirective,
|
|
NgComponentOutlet,
|
|
NgFor,
|
|
NgIf,
|
|
RouterLinkActive,
|
|
],
|
|
})
|
|
export class ExpandableNavbarSectionComponent extends NavbarSectionComponent implements AfterViewChecked, OnInit, OnDestroy {
|
|
|
|
/**
|
|
* This section resides in the Public Navbar
|
|
*/
|
|
menuID = MenuID.PUBLIC;
|
|
|
|
/**
|
|
* True if mouse has entered the menu section toggler
|
|
*/
|
|
mouseEntered = false;
|
|
|
|
/**
|
|
* Whether the section was expanded
|
|
*/
|
|
focusOnFirstChildSection = false;
|
|
|
|
/**
|
|
* True if screen size was small before a resize event
|
|
*/
|
|
wasMobile = undefined;
|
|
|
|
/**
|
|
* Observable that emits true if the screen is small, false otherwise
|
|
*/
|
|
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;
|
|
|
|
/**
|
|
* List of current dropdown items who have event listeners
|
|
*/
|
|
private dropdownItems: NodeListOf<HTMLElement>;
|
|
|
|
@HostListener('window:resize', ['$event'])
|
|
onResize() {
|
|
this.isMobile$.pipe(
|
|
first(),
|
|
).subscribe((isMobile) => {
|
|
// When switching between desktop and mobile active sections should be deactivated
|
|
if (isMobile !== this.wasMobile) {
|
|
this.wasMobile = isMobile;
|
|
this.menuService.deactivateSection(this.menuID, this.section.id);
|
|
this.mouseEntered = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
constructor(
|
|
@Inject('sectionDataProvider') public section: MenuSection,
|
|
protected menuService: MenuService,
|
|
protected injector: Injector,
|
|
protected windowService: HostWindowService,
|
|
) {
|
|
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;
|
|
} else {
|
|
this.focusOnFirstChildSection = undefined;
|
|
this.unsubscribeFromEventListeners();
|
|
}
|
|
}));
|
|
}
|
|
|
|
ngAfterViewChecked(): void {
|
|
if (this.addArrowEventListeners) {
|
|
this.dropdownItems = document.querySelectorAll(`#${this.expandableNavbarSectionId()} *[role="menuitem"]`);
|
|
this.dropdownItems.forEach((item: HTMLElement) => {
|
|
item.addEventListener('keydown', this.navigateDropdown.bind(this));
|
|
});
|
|
if (this.focusOnFirstChildSection && this.dropdownItems.length > 0) {
|
|
this.dropdownItems.item(0).focus();
|
|
}
|
|
this.addArrowEventListeners = false;
|
|
}
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
super.ngOnDestroy();
|
|
this.unsubscribeFromEventListeners();
|
|
}
|
|
|
|
/**
|
|
* Activate this section if it's currently inactive, deactivate it when it's currently active.
|
|
* Also saves whether this toggle was performed by a keyboard event (non-click event) in order to know if thi first
|
|
* item should be focussed when activating a section.
|
|
*
|
|
* @param {Event} event The user event that triggered this method
|
|
*/
|
|
override toggleSection(event: Event): void {
|
|
this.focusOnFirstChildSection = event.type !== 'click';
|
|
super.toggleSection(event);
|
|
}
|
|
|
|
/**
|
|
* Removes all the current event listeners on the dropdown items (called when the menu is closed & on component
|
|
* destruction)
|
|
*/
|
|
unsubscribeFromEventListeners(): void {
|
|
if (this.dropdownItems) {
|
|
this.dropdownItems.forEach((item: HTMLElement) => {
|
|
item.removeEventListener('keydown', this.navigateDropdown.bind(this));
|
|
});
|
|
this.dropdownItems = undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* When the mouse enters the section toggler activate the menu section
|
|
* @param $event
|
|
*/
|
|
onMouseEnter($event: Event): void {
|
|
this.isMobile$.pipe(
|
|
first(),
|
|
).subscribe((isMobile) => {
|
|
if (!isMobile && !this.active$.value && !this.mouseEntered) {
|
|
this.activateSection($event);
|
|
}
|
|
this.mouseEntered = true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* When the mouse leaves the section toggler deactivate the menu section
|
|
* @param $event
|
|
*/
|
|
onMouseLeave($event: Event): void {
|
|
this.isMobile$.pipe(
|
|
first(),
|
|
).subscribe((isMobile) => {
|
|
if (!isMobile && this.active$.value && this.mouseEntered) {
|
|
this.deactivateSection($event);
|
|
}
|
|
this.mouseEntered = false;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* returns the ID of the DOM element representing the navbar section
|
|
*/
|
|
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.code === 'Tab') {
|
|
this.deactivateSection(event, false);
|
|
return;
|
|
} else if (event.code === 'Escape') {
|
|
this.deactivateSection(event, false);
|
|
(document.querySelector(`a[aria-controls="${this.expandableNavbarSectionId()}"]`) as HTMLElement)?.focus();
|
|
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.focusOnFirstChildSection = true;
|
|
this.activateSection(event);
|
|
break;
|
|
case 'Space':
|
|
case 'Enter':
|
|
event.preventDefault();
|
|
break;
|
|
}
|
|
}
|
|
}
|