'How to open and close Angular mat menu on hover
This question is in reference to this Github issue, with mat-menu
which can't be toggled using mouse hover, I am basically trying to replace a bootstrap based horizontal navigation menu with angular material's menu. The only thing keeping me from replicating bootstrap based menu is opening and closing mat-menu
on hover.
As mentioned in the above Github issue there are some workarounds to achieve what I want, like using, mouseEnter
(mouseenter)="menuTrigger.openMenu()"
or adding a span inside Mat-menu in order to bind mat-menu
close,
<mat-menu #menu="matMenu" overlapTrigger="false">
<span (mouseleave)="menuTrigger.closeMenu()">
<button mat-menu-item>Item 1</button>
<button mat-menu-item>Item 2</button>
</span>
</mat-menu>
but none of the solutions seems to cover every little scenario,
e.g.
As mentioned in the above Github issue, there are following issues in the first SO solution.
- Hover the mouse cursor on the button and the menu will pop up. But if you click on the button, it will hide and show the menu. IMHO it's a bug.
- To hide the menu, the user needs to click outside of the menu. Ideally, the menu would become hidden if the mouse cursor is outside
of the area (which includes the button, the menu, and sub-menus)
longer than 400ms.
And in the span solution which tries to solve one of the above issues, but doesn't work properly, e.g.
hovering over MatMenuTrigger
does open the mat-menu
as expected but if a user moves the mouse away without entering mat-menu
, then it doesn't close automatically which is wrong.
Also moving to one of the levels two sub-menu also closes the level one menu which is not what I want,
P.S moving mouse from one opened menu to the next sibling one doesn't open the next one. I guess this might be difficult to achieve as mentioned here, But I think some of these might be achievable right?
Here is a basic stackBlitz which reproduces what I am experiencing, any help is appreciated.
Solution 1:[1]
The first challenge is that mat-menu
steals the focus from the button when the CDK overlay is generated due to the z-index
of the overlay... to solve this you need to set the z-index in a style for the button...
- This will stop the recursive loop when you add a
(mouseleave)
to the button.style="z-index:1050"
Next you need to track the state of all enter and leave events for the levelone
and levelTwo
menu's and store that state in two component variables.
enteredButton = false;
isMatMenuOpen = false;
isMatMenu2Open = false;
Next create menu enter and menuLeave methods for both menu levels.. notice menuLeave(trigger)
checks if level2 is accessed and does nothing if true.
Please Note: menu2Leave()
has logic to allow navigation back to level one but close both if exit the other side... also removing button focus upon leave of levels.
menuenter() {
this.isMatMenuOpen = true;
if (this.isMatMenu2Open) {
this.isMatMenu2Open = false;
}
}
menuLeave(trigger, button) {
setTimeout(() => {
if (!this.isMatMenu2Open && !this.enteredButton) {
this.isMatMenuOpen = false;
trigger.closeMenu();
this.ren.removeClass(button['_elementRef'].nativeElement, 'cdk-focused');
this.ren.removeClass(button['_elementRef'].nativeElement, 'cdk-program-focused');
} else {
this.isMatMenuOpen = false;
}
}, 80)
}
menu2enter() {
this.isMatMenu2Open = true;
}
menu2Leave(trigger1, trigger2, button) {
setTimeout(() => {
if (this.isMatMenu2Open) {
trigger1.closeMenu();
this.isMatMenuOpen = false;
this.isMatMenu2Open = false;
this.enteredButton = false;
this.ren.removeClass(button['_elementRef'].nativeElement, 'cdk-focused');
this.ren.removeClass(button['_elementRef'].nativeElement, 'cdk-program-focused');
} else {
this.isMatMenu2Open = false;
trigger2.closeMenu();
}
}, 100)
}
buttonEnter(trigger) {
setTimeout(() => {
if(this.prevButtonTrigger && this.prevButtonTrigger != trigger){
this.prevButtonTrigger.closeMenu();
this.prevButtonTrigger = trigger;
trigger.openMenu();
}
else if (!this.isMatMenuOpen) {
this.enteredButton = true;
this.prevButtonTrigger = trigger
trigger.openMenu()
}
else {
this.enteredButton = true;
this.prevButtonTrigger = trigger
}
})
}
buttonLeave(trigger, button) {
setTimeout(() => {
if (this.enteredButton && !this.isMatMenuOpen) {
trigger.closeMenu();
this.ren.removeClass(button['_elementRef'].nativeElement, 'cdk-focused');
this.ren.removeClass(button['_elementRef'].nativeElement, 'cdk-program-focused');
} if (!this.isMatMenuOpen) {
trigger.closeMenu();
this.ren.removeClass(button['_elementRef'].nativeElement, 'cdk-focused');
this.ren.removeClass(button['_elementRef'].nativeElement, 'cdk-program-focused');
} else {
this.enteredButton = false;
}
}, 100)
}
HTML
below is how to wire it all up.
<ng-container *ngFor="let menuItem of modulesList">
<ng-container *ngIf="!menuItem.children">
<a class="nav-link">
<span class="icon fa" [ngClass]="menuItem.icon"></span>
<span class="text-holder">{{menuItem.label}}</span>
</a>
</ng-container>
<ng-container *ngIf="menuItem.children.length > 0">
<button #button mat-button [matMenuTriggerFor]="levelOne" #levelOneTrigger="matMenuTrigger" (mouseenter)="levelOneTrigger.openMenu()" (mouseleave)="buttonLeave(levelOneTrigger, button)" style="z-index:1050">
<span class="icon fa" [ngClass]="menuItem.icon"></span>
<span>{{menuItem.label}}
<i class="fa fa-chevron-down"></i>
</span>
</button>
<mat-menu #levelOne="matMenu" direction="down" yPosition="below">
<span (mouseenter)="menuenter()" (mouseleave)="menuLeave(levelOneTrigger, button)">
<ng-container *ngFor="let childL1 of menuItem.children">
<li class="p-0" *ngIf="!childL1.children" mat-menu-item>
<a class="nav-link">{{childL1.label}}
<i *ngIf="childL1.icon" [ngClass]="childL1.icon"></i>
</a>
</li>
<ng-container *ngIf="childL1.children && childL1.children.length > 0">
<li mat-menu-item #levelTwoTrigger="matMenuTrigger" [matMenuTriggerFor]="levelTwo">
<span class="icon fa" [ngClass]="childL1.icon"></span>
<span>{{childL1.label}}</span>
</li>
<mat-menu #levelTwo="matMenu">
<span (mouseenter)="menu2enter()" (mouseleave)="menu2Leave(levelOneTrigger,levelTwoTrigger, button)">
<ng-container *ngFor="let childL2 of childL1.children">
<li class="p-0" mat-menu-item>
<a class="nav-link">{{childL2.label}}
<i *ngIf="childL2.icon" [ngClass]="childL2.icon"></i>
</a>
</li>
</ng-container>
</span>
</mat-menu>
</ng-container>
</ng-container>
</span>
</mat-menu>
</ng-container>
</ng-container>
Stackblitz
https://stackblitz.com/edit/mat-nested-menu-yclrmd?embed=1&file=app/nested-menu-example.html
Solution 2:[2]
here a component I wrote for handling auto open/close mat-menu:
import { Component } from '@angular/core';
@Component({
selector: 'app-auto-open-menu',
template: `
<div class="app-nav-item" [matMenuTriggerFor]="menu" #menuTrigger="matMenuTrigger"
(mouseenter)="mouseEnter(menuTrigger)" (mouseleave)="mouseLeave(menuTrigger)">
<ng-content select="[trigger]"></ng-content>
</div>
<mat-menu #menu="matMenu" [hasBackdrop]="false">
<div (mouseenter)="mouseEnter(menuTrigger)" (mouseleave)="mouseLeave(menuTrigger)">
<ng-content select="[content]"></ng-content>
</div>
</mat-menu>
`
})
export class AutoOpenMenuComponent {
timedOutCloser;
constructor() { }
mouseEnter(trigger) {
if (this.timedOutCloser) {
clearTimeout(this.timedOutCloser);
}
trigger.openMenu();
}
mouseLeave(trigger) {
this.timedOutCloser = setTimeout(() => {
trigger.closeMenu();
}, 50);
}
}
Then you can use it in your app:
<app-auto-open-menu>
<div trigger>Auto-open</div>
<div content>
<span mat-menu-item>Foo</span>
<span mat-menu-item>Bar</span>
</div>
</app-auto-open-menu>
Solution 3:[3]
Simplest solution that worked for me add [hasBackdrop]="false"
:
<mat-menu [hasBackdrop]="false">
</mat-menu>
Solution 4:[4]
This solution can be used as an alternative to setting z-index:1050 as suggested by Marshal. For other fixes you should check Marshal's answer.
You can use
<button [matMenuTriggerFor]="menu" #trigger="matMenuTrigger" (mouseenter)="trigger.openMenu()" (mouseleave)="trigger.closeMenu()"></button>
Using this will create continuous flicker loop, but there is a simple fix.
Only one thing needs to be taken care of i.e. :
when menu opens
<div class="cdk-overlay-container"></div>
this div covers the whole screen, typically added at the end of whole html just before the /body tag. All your menus are generated inside this container. (class name may differ in different versions).
Just add this in your css/scss styles file :
.cdk-overlay-container{
left:200px;
top:200px;
}
.cdk-overlay-connected-position-bounding-box{
top:0 !important;
}
or anything which stops this element from overlapping your button.
I have tried this myself, hope my answer was clear and precise.
Here is stackblitz demo of the same, i have edited the stackblitz code in the question.
Solution 5:[5]
you can do it the following way (is the best way so far):
Add "#locationMenuTrigger="matMenuTrigger" trigger to your button and add "(mouseenter)" event on this button:
<button [matMenuTriggerFor]="locationMenu" #locationMenuTrigger="matMenuTrigger" (mouseover)="locationMenuTrigger.openMenu()" >info</button>
Put your "(mouseleave)" event on the mat-menu div/span that u have put inside mat-menu, like following:
<mat-menu #locationMenu="matMenu" class="location-menu"> <span class="locations" (mouseleave)="closeMenu()"> {{ row | notAvailable: '-' }} </span>
Solution 6:[6]
Whoever like me would like understand and avoid that z-index: 1050
, here's a perfect solution - https://stackoverflow.com/a/54630251/1122524 .
Solution 7:[7]
Here is another good and simple solution that creates Hover Menu in angular Material it does not require any TS but you will have to add the reference of various Relevant Modules in your root component.
<button mat-flat-button class="nav-item" (mouseover)="createPlanmenuTrigger.openMenu()">
<a class="nav-link" #createPlanmenuTrigger="matMenuTrigger" [matMenuTriggerFor]="createPlan">Create Plan</a>
<mat-menu #createPlan="matMenu">
<button mat-menu-item>Manual Plan</button>
<button mat-menu-item>Upload Plan</button>
<button mat-menu-item>Pending Plans</button>
</mat-menu>
</button>
Solution 8:[8]
Simplest solution using the in-built methods of mat menu component.
<ul class="navbar-nav mr-auto">
<li class="nav-item" routerLinkActive="active" (mouseover)="createPlanmenuTrigger.openMenu()" (mouseout)="$event.stopPropagation();createPlanmenuTrigger.openMenu()">
<a class="nav-link" #createPlanmenuTrigger="matMenuTrigger" [matMenuTriggerFor]="createPlan">Create Plan</a>
<mat-menu #createPlan="matMenu">
<button mat-menu-item [routerLink]="['/plan/manual-plan']">Manual Plan</button>
<button mat-menu-item [routerLink]="['/plan/create-plan']">Upload Plan</button>
<button mat-menu-item [routerLink]="['/plan/pending-plans']">Pending Plans</button>
</mat-menu>
</li>
</ul>
Solution 9:[9]
If you are fine without using angular material, use the below code.
.dropbtn {
background-color: #4CAF50;
color: white;
padding: 16px;
font-size: 16px;
border: none;
}
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #f1f1f1;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
}
.dropdown-content a {
color: black;
padding: 12px 16px;
text-decoration: none;
display: block;
}
.dropdown-content a:hover {background-color: #ddd;}
.dropdown:hover .dropdown-content {display: block;}
.dropdown:hover .dropbtn {background-color: #3e8e41;}
<div class="dropdown">
<button class="dropbtn">Dropdown</button>
<div class="dropdown-content">
<a href="#">Link 1</a>
<a href="#">Link 2</a>
<a href="#">Link 3</a>
</div>
</div>
Solution 10:[10]
I have a POC for a client and it only has one top level menu. Was able to make this solution work wihtout z index and renderer.
My trigger button is not even a button nor a matbutton, it's a div:
added these attributes to the div with the matMenuTriggerFor attribute. (menuOpened)="isMatMenuOpen = true;" (menuClosed)="isMatMenuOpen= false;"
Solution 11:[11]
If you have a dynamic menu structure based off of this solution: https://stackblitz.com/edit/dynamic-nested-topnav-menu), the existing code can be modified with the timeout solution mentioned above. An example below:
app.component.ts (snippet):
menuTrigger!: MatMenuTrigger;
@ViewChildren(MatMenuTrigger) public menuTriggers!: QueryList<MatMenuTrigger>;
timedOutCloser: any;
timeOut: number = 100;
mouseEnter(trigger: MatMenuTrigger) {
if (this.timedOutCloser) {
clearTimeout(this.timedOutCloser);
}
this.closeMenus();
trigger.openMenu();
this.menuTrigger = trigger;
}
mouseLeave(trigger: MatMenuTrigger) {
this.timedOutCloser = setTimeout(() => {
trigger.closeMenu();
}, this.timeOut);
}
onNotify(isOpen: boolean): void {
if (isOpen) {
if (this.timedOutCloser) {
clearTimeout(this.timedOutCloser);
}
} else {
this.timedOutCloser = setTimeout(() => {
this.menuTrigger.closeMenu();
}, this.timeOut);
}
}
closeMenus() {
for (let i = 0; i < this.menuTriggers?.length; i++) {
this.menuTriggers?.get(i)?.closeMenu();
}
}
app.component.html (snippet):
<span class="button-span" *ngIf="item.children && item.children.length > 0">
<button mat-flat-button #menuTrigger="matMenuTrigger" [matMenuTriggerFor]="menu.childMenu" (mouseenter)="mouseEnter(menuTrigger)" (mouseleave)="mouseLeave(menuTrigger)"
[disabled]="item.disabled">
{{item.displayName}}
</button>
<app-menu-item #menu [items]="item.children" [timedOutCloser]="timedOutCloser" (notify)='onNotify($event)'></app-menu-item>
</span>
menu-item.component.ts (snippet):
timeOut: number = 100;
@Input() timedOutCloser: any;
@ViewChild('childMenu', { static: true }) public childMenu: any;
@ViewChildren(MatMenuTrigger) public menuTriggers!: QueryList<MatMenuTrigger>;
@Output() notify: EventEmitter<boolean> = new EventEmitter<boolean>();
mouseEnter(trigger: MatMenuTrigger) {
if (this.timedOutCloser) {
clearTimeout(this.timedOutCloser);
this.notify.emit(true);
}
trigger.openMenu();
}
mouseLeave() {
this.notify.emit(false);
}
childMouseEnter() {
if (this.timedOutCloser) {
clearTimeout(this.timedOutCloser);
this.notify.emit(true);
}
}
childMouseLeave() {
this.notify.emit(false);
}
onNotify(isOpen: boolean): void {
if (isOpen) {
if (this.timedOutCloser) {
clearTimeout(this.timedOutCloser);
this.notify.emit(true);
}
} else {
this.timedOutCloser = setTimeout(() => {
this.notify.emit(false);
}, this.timeOut);
}
}
menu-item.component.html (snippet):
<mat-menu #childMenu="matMenu" [overlapTrigger]="false" [hasBackdrop]="false">
<span *ngFor="let child of items; index as index">
<!-- Handle branch node menu items -->
<span *ngIf="child.children && child.children.length > 0">
<button mat-menu-item #menuTrigger="matMenuTrigger" [matMenuTriggerFor]="menu.childMenu" (mouseenter)="mouseEnter(menuTrigger)" (mouseleave)="mouseLeave()">
<span>{{child.displayName}}</span>
</button>
<app-menu-item #menu [items]="child.children" [timedOutCloser]="timedOutCloser" (notify)='onNotify($event)'></app-menu-item>
</span>
<!-- Handle leaf node menu items -->
<span *ngIf="!child.children || child.children.length === 0">
<button mat-menu-item [routerLink]="child.route" (mouseenter)="childMouseEnter()" (mouseleave)="childMouseLeave()">
<span>{{child.displayName}}</span>
</button>
</span>
</span>
</mat-menu>
Some items to note:
[hasBackdrop]="false" to take out the menu flickering.
(mouseenter) and (mouseleave) functions.
[timedOutCloser] to pass the timedOutCloser from parent to child menu items.
(notify)='onNotify($event)' to notify the parent on child menu enter and leave events.
Solution 12:[12]
This code worked for
<mat-icon [matMenuTriggerFor]="login" #loginTrigger="matMenuTrigger" (mouseenter)="loginTrigger.openMenu()">account_circle</mat-icon>
<!-- yPosition="above" -->
<mat-menu #login="matMenu">
<button mat-menu-item>
<!-- <mat-icon>dialpad</mat-icon> -->
<span>Login / Register</span>
</button>
</mat-menu>
Solution 13:[13]
I use this way in my project, but even I apply style="z-index:1050
" after I implement (mouseleave) it does not stop the recursive loop. I got so confused. furthermore, my menu components are recursive menu component, I do not know if the sub-menu have same trigger name, it will work properly.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow