mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
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:

committed by
github-actions[bot]
![github-actions[bot]](/assets/img/avatar_default.png)
parent
17e03330d0
commit
b9b1d3fd8d
@@ -1,36 +1,37 @@
|
|||||||
<div class="ds-menu-item-wrapper text-md-center"
|
<div #expandableNavbarSectionContainer class="ds-menu-item-wrapper text-md-center"
|
||||||
[id]="'expandable-navbar-section-' + section.id"
|
[id]="'expandable-navbar-section-' + section.id"
|
||||||
(mouseenter)="onMouseEnter($event)"
|
(mouseenter)="onMouseEnter($event)"
|
||||||
(mouseleave)="onMouseLeave($event)"
|
(mouseleave)="onMouseLeave($event)"
|
||||||
data-test="navbar-section-wrapper">
|
data-test="navbar-section-wrapper">
|
||||||
<a href="javascript:void(0);" routerLinkActive="active"
|
<a href="javascript:void(0);" routerLinkActive="active"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
(keyup.enter)="toggleSection($event)"
|
(keyup.enter)="toggleSection($event)"
|
||||||
(keyup.space)="toggleSection($event)"
|
(keyup.space)="toggleSection($event)"
|
||||||
(click)="toggleSection($event)"
|
(click)="toggleSection($event)"
|
||||||
(keydown.space)="$event.preventDefault()"
|
(keydown)="keyDown($event)"
|
||||||
(keydown.tab)="deactivateSection($event, false)"
|
aria-haspopup="menu"
|
||||||
aria-haspopup="menu"
|
data-test="navbar-section-toggler"
|
||||||
data-test="navbar-section-toggler"
|
[attr.aria-expanded]="(active$ | async).valueOf()"
|
||||||
[attr.aria-expanded]="(active$ | async).valueOf()"
|
[attr.aria-controls]="expandableNavbarSectionId()"
|
||||||
[attr.aria-controls]="expandableNavbarSectionId(section.id)"
|
class="d-flex flex-row flex-nowrap align-items-center gapx-1 ds-menu-toggler-wrapper"
|
||||||
class="d-flex flex-row flex-nowrap align-items-center gapx-1 ds-menu-toggler-wrapper"
|
[class.disabled]="section.model?.disabled">
|
||||||
[class.disabled]="section.model?.disabled">
|
|
||||||
<span class="flex-fill">
|
<span class="flex-fill">
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||||
</span>
|
</span>
|
||||||
<i class="fas fa-caret-down fa-xs toggle-menu-icon" aria-hidden="true"></i>
|
<i class="fas fa-caret-down fa-xs toggle-menu-icon" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
<div *ngIf="(active$ | async).valueOf() === true" (click)="deactivateSection($event)"
|
<div *ngIf="(active$ | async).valueOf() === true" (click)="deactivateSection($event)"
|
||||||
[id]="expandableNavbarSectionId(section.id)"
|
[id]="expandableNavbarSectionId()"
|
||||||
role="menu"
|
[dsHoverOutsideOfElement]="expandableNavbarSection"
|
||||||
class="dropdown-menu show nav-dropdown-menu m-0 shadow-none border-top-0 px-3 px-md-0 pt-0 pt-md-1">
|
(dsHoverOutside)="deactivateSection($event, false)"
|
||||||
<div @slide role="presentation">
|
role="menu"
|
||||||
<div *ngFor="let subSection of (subSections$ | async)" class="text-nowrap" role="presentation">
|
class="dropdown-menu show nav-dropdown-menu m-0 shadow-none border-top-0 px-3 px-md-0 pt-0 pt-md-1">
|
||||||
<ng-container
|
<div @slide role="presentation">
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
|
<div *ngFor="let subSection of (subSections$ | async)" class="text-nowrap" role="presentation">
|
||||||
</div>
|
<ng-container
|
||||||
|
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -5,11 +5,14 @@ import {
|
|||||||
NgIf,
|
NgIf,
|
||||||
} from '@angular/common';
|
} from '@angular/common';
|
||||||
import {
|
import {
|
||||||
|
AfterViewChecked,
|
||||||
Component,
|
Component,
|
||||||
|
ElementRef,
|
||||||
HostListener,
|
HostListener,
|
||||||
Inject,
|
Inject,
|
||||||
Injector,
|
Injector,
|
||||||
OnInit,
|
OnInit,
|
||||||
|
ViewChild,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { RouterLinkActive } from '@angular/router';
|
import { RouterLinkActive } from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
@@ -19,6 +22,8 @@ import { slide } from '../../shared/animations/slide';
|
|||||||
import { HostWindowService } from '../../shared/host-window.service';
|
import { HostWindowService } from '../../shared/host-window.service';
|
||||||
import { MenuService } from '../../shared/menu/menu.service';
|
import { MenuService } from '../../shared/menu/menu.service';
|
||||||
import { MenuID } from '../../shared/menu/menu-id.model';
|
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 { VarDirective } from '../../shared/utils/var.directive';
|
||||||
import { NavbarSectionComponent } from '../navbar-section/navbar-section.component';
|
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'],
|
styleUrls: ['./expandable-navbar-section.component.scss'],
|
||||||
animations: [slide],
|
animations: [slide],
|
||||||
standalone: true,
|
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
|
* This section resides in the Public Navbar
|
||||||
*/
|
*/
|
||||||
@@ -54,6 +70,13 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
|
|||||||
*/
|
*/
|
||||||
isMobile$: Observable<boolean>;
|
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'])
|
@HostListener('window:resize', ['$event'])
|
||||||
onResize() {
|
onResize() {
|
||||||
this.isMobile$.pipe(
|
this.isMobile$.pipe(
|
||||||
@@ -68,17 +91,33 @@ export class ExpandableNavbarSectionComponent extends NavbarSectionComponent imp
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(@Inject('sectionDataProvider') menuSection,
|
constructor(
|
||||||
protected menuService: MenuService,
|
@Inject('sectionDataProvider') public section: MenuSection,
|
||||||
protected injector: Injector,
|
protected menuService: MenuService,
|
||||||
private windowService: HostWindowService,
|
protected injector: Injector,
|
||||||
|
protected windowService: HostWindowService,
|
||||||
) {
|
) {
|
||||||
super(menuSection, menuService, injector);
|
super(section, menuService, injector);
|
||||||
this.isMobile$ = this.windowService.isMobile();
|
this.isMobile$ = this.windowService.isMobile();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
super.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
|
* returns the ID of the DOM element representing the navbar section
|
||||||
* @param sectionId
|
|
||||||
*/
|
*/
|
||||||
expandableNavbarSectionId(sectionId: string) {
|
expandableNavbarSectionId(): string {
|
||||||
return `expandable-navbar-section-${sectionId}-dropdown`;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
51
src/app/shared/utils/hover-outside.directive.ts
Normal file
51
src/app/shared/utils/hover-outside.directive.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
Directive,
|
||||||
|
ElementRef,
|
||||||
|
EventEmitter,
|
||||||
|
HostListener,
|
||||||
|
Input,
|
||||||
|
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();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link ElementRef} for which this directive should emit when the mouse leaves it. By default this will be the
|
||||||
|
* element the directive was put on.
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
public dsHoverOutsideOfElement: ElementRef;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private elementRef: ElementRef,
|
||||||
|
) {
|
||||||
|
this.dsHoverOutsideOfElement = this.elementRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('document:mouseover', ['$event'])
|
||||||
|
public onMouseOver(event: MouseEvent): void {
|
||||||
|
const targetElement: HTMLElement = event.target as HTMLElement;
|
||||||
|
const hoveredInside = this.dsHoverOutsideOfElement.nativeElement.contains(targetElement);
|
||||||
|
|
||||||
|
if (!hoveredInside) {
|
||||||
|
this.dsHoverOutside.emit(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -9,11 +9,9 @@ import { RouterLinkActive } from '@angular/router';
|
|||||||
|
|
||||||
import { ExpandableNavbarSectionComponent as BaseComponent } from '../../../../../app/navbar/expandable-navbar-section/expandable-navbar-section.component';
|
import { ExpandableNavbarSectionComponent as BaseComponent } from '../../../../../app/navbar/expandable-navbar-section/expandable-navbar-section.component';
|
||||||
import { slide } from '../../../../../app/shared/animations/slide';
|
import { slide } from '../../../../../app/shared/animations/slide';
|
||||||
|
import { HoverOutsideDirective } from '../../../../../app/shared/utils/hover-outside.directive';
|
||||||
import { VarDirective } from '../../../../../app/shared/utils/var.directive';
|
import { VarDirective } from '../../../../../app/shared/utils/var.directive';
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents an expandable section in the navbar
|
|
||||||
*/
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-themed-expandable-navbar-section',
|
selector: 'ds-themed-expandable-navbar-section',
|
||||||
// templateUrl: './expandable-navbar-section.component.html',
|
// 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'],
|
styleUrls: ['../../../../../app/navbar/expandable-navbar-section/expandable-navbar-section.component.scss'],
|
||||||
animations: [slide],
|
animations: [slide],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [VarDirective, RouterLinkActive, NgComponentOutlet, NgIf, NgFor, AsyncPipe],
|
imports: [
|
||||||
|
AsyncPipe,
|
||||||
|
HoverOutsideDirective,
|
||||||
|
NgComponentOutlet,
|
||||||
|
NgFor,
|
||||||
|
NgIf,
|
||||||
|
RouterLinkActive,
|
||||||
|
VarDirective,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class ExpandableNavbarSectionComponent extends BaseComponent {
|
export class ExpandableNavbarSectionComponent extends BaseComponent {
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user