How to open and close Angular mat menu on hover
Asked Answered
B

16

48

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.

Bathyscaphe answered 4/12, 2018 at 17:19 Comment(2)
mat menu sucks at this!Blackfish
evidence of incapacity for a menu at all :/Otter
H
38

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

Hopple answered 4/12, 2018 at 18:3 Comment(18)
Thanks @Hopple this seems to solve most of the issues I was facing. The only thing annoying is some minor issues while transition of mouse between trigger and mat-menu, but i guess i can live with that.Bathyscaphe
I just updated the stackblitz link by increasing the setTimeout to 100ms on buttonLeave... this should allow more time for the enter menu variable to be set on the transition between trigger and mat-menuHopple
I also added button state, that should address all remaining minor issues. Any items beyond that, please note the solution is fully customizable, you will need to make adjustments to the timing etc to make the solution efficient in your application.Hopple
A bug with clicking on the menu (essential for the touch screens) - on the 1st click the menu opens and collapses straight away, on the 2nd - it stays open.Moneywort
@AlexKlaus this has bee resolved, please see revised stackblitzHopple
Confirm, that touch screens are good now. Only see two minor issues: 1) On clicking the menu again it gets hidden (see gph.is/2SBxCnL); 2) Clicking on the menu for the 3rd-4th time it may appear and disappear in a second.Moneywort
@AlexKlaus that is interesting, managed to get it to replicate once in the chrome dev tools mobile emulation... but tough to do it again, I really had to spam it the first time and think I had to access level 2 once to do it. The only thing I can think of is maybe the level 2 state is not getting reset properly after accessing level 2 and going back to button... so I went ahead and made sure those are set to false under the one if condition in the buttonEnter that would apply... unfortunately can't reliably replicate to go any further than to take a guess at this issue.Hopple
@Hopple can you share how exactly you fixed the first bug reported by Alex? I think I have the same setup, but It is appearing at my end but not in the above stackblitz. "A bug with clicking on the menu (essential for the touch screens) - on the 1st click the menu opens and collapses straight away, on the 2nd - it stays open"Bathyscaphe
I lengthened the setTimout duration to allow time for the state variables to be set, but I also included the enteredButton variable to track the button states.Hopple
@Hopple I guess I have the same configurations at my end, but I'll check again or provide an updated stackBlitz, thanks.Bathyscaphe
@SaifUllah check your version of buttonEnter() and make sure it matches the three conditions if, else if, else. Just noticed I also started passing a copy of the trigger to store in a local variable for comparison as well, this was to address when hovering over a new button while prior button menu was open it will close prior menu before opening new menu.Hopple
This is working fine in my chrome but not safari (if you move the mouse down to item 2 in the submenu the whole thing closes prematurely).Madalene
dynamic-nested-menus.stackblitz.io Can you make this with on hover?Caplan
On menu scrollbar it doesn't work. IS there any way to include scrollbar ?Caplan
Just FYI - there's a perfect solution regarding avoiding that z-index: 1050 - https://mcmap.net/q/357065/-mat-menu-pierces-through-quot-cdk-overlay-fog-quot.Leibowitz
I noticed a minor issue in your example. When you hover over User, User 1 is highlighted. As you move over the other users, User 1 unhighlights and each other user highlights. When you hover over Admin, Admin 1 is highlighted. As you move over the other admins, each other admin highlights, but Admin 1 remains highlighted. I assume this functionality wasn't intentional? Any idea as to why this happens, because I am facing a similar issue?Indetermination
@Indetermination there are issues on github discussing this for the material library. https://github.com/angular/material/issues/3707 I revised the stackblitz to use the renderer and remove the focus class of the first element on menu open.Hopple
This works well in browsers like Chrome, but in Safari the menu closes immediately when hovering over an item in the menu. Is there a fix for this?Meanie
P
49

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>
Prouty answered 3/1, 2020 at 10:23 Comment(11)
This should be the selected answer, no CSS and a simple complete solution :)Muffle
This is 100 times better than any answer here. It uses native material API. Works very well. A few minor tweaks needed. mat-menu has 8px padding top and bottom which when hovered causes close to fire when it should not. To remove this padding add this to styles.css / styles.scss : div.cdk-overlay-container .mat-menu-content { padding-top: 0; padding-bottom: 0; } ... also <div class="app-nav-item" ...> should be <span class="app-nav-item" ...> to get rid of an unwanted block elementConfine
note if you want the top and bottom padding (removed in advice on previous comment) then you can add it to you <div content> ... just add a class to this div and style it as usual - this will not cause a problem - you may also want to pass additional props like this <app-auto-open-menu xPosition="before" yPosition="below"> which requires you to add an @Input for each in the usual mannerConfine
Upvoted but this doesn't work on mobile unless you add something like <div trigger><div (click)="$event.stopPropagation();">Auto-open</div></div>Yet
Works perfectly, but the first item always stays highlighted until I click somewhere. Any tips on how to fix this ?Helfand
@TomasLukac I am also facing same issue. You got a solution for first item always getting highlighted ?Pham
a work around for MOBILE view you can copy paste the same code without any mouseenter or mouseleave functions and give different ids to mat-menu. Then using css @media hide it in desktop view and hide the actual code in mobile view.Juni
I completely agree with @danday74's comment! This solution is truly fantastic and surpasses all other answers here. It cleverly utilizes the native Material API, resulting in a seamless and high-performing hover menu feature. I did make some minor adjustments to address specific requirements. Just know that with a few tweaks, this solution worked flawlessly for my use case.Brutus
Perfect! This is exactly what I needed. Take my upvote :DBaryton
Wow! This should definitely be the selected answer. Thank you!Gaur
It's important to mention that menu item focus isn't working when using content projection, so the solution with "mat-menu-item" elements outside "mat-menu" can break accessibility. SourceBrittni
H
38

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

Hopple answered 4/12, 2018 at 18:3 Comment(18)
Thanks @Hopple this seems to solve most of the issues I was facing. The only thing annoying is some minor issues while transition of mouse between trigger and mat-menu, but i guess i can live with that.Bathyscaphe
I just updated the stackblitz link by increasing the setTimeout to 100ms on buttonLeave... this should allow more time for the enter menu variable to be set on the transition between trigger and mat-menuHopple
I also added button state, that should address all remaining minor issues. Any items beyond that, please note the solution is fully customizable, you will need to make adjustments to the timing etc to make the solution efficient in your application.Hopple
A bug with clicking on the menu (essential for the touch screens) - on the 1st click the menu opens and collapses straight away, on the 2nd - it stays open.Moneywort
@AlexKlaus this has bee resolved, please see revised stackblitzHopple
Confirm, that touch screens are good now. Only see two minor issues: 1) On clicking the menu again it gets hidden (see gph.is/2SBxCnL); 2) Clicking on the menu for the 3rd-4th time it may appear and disappear in a second.Moneywort
@AlexKlaus that is interesting, managed to get it to replicate once in the chrome dev tools mobile emulation... but tough to do it again, I really had to spam it the first time and think I had to access level 2 once to do it. The only thing I can think of is maybe the level 2 state is not getting reset properly after accessing level 2 and going back to button... so I went ahead and made sure those are set to false under the one if condition in the buttonEnter that would apply... unfortunately can't reliably replicate to go any further than to take a guess at this issue.Hopple
@Hopple can you share how exactly you fixed the first bug reported by Alex? I think I have the same setup, but It is appearing at my end but not in the above stackblitz. "A bug with clicking on the menu (essential for the touch screens) - on the 1st click the menu opens and collapses straight away, on the 2nd - it stays open"Bathyscaphe
I lengthened the setTimout duration to allow time for the state variables to be set, but I also included the enteredButton variable to track the button states.Hopple
@Hopple I guess I have the same configurations at my end, but I'll check again or provide an updated stackBlitz, thanks.Bathyscaphe
@SaifUllah check your version of buttonEnter() and make sure it matches the three conditions if, else if, else. Just noticed I also started passing a copy of the trigger to store in a local variable for comparison as well, this was to address when hovering over a new button while prior button menu was open it will close prior menu before opening new menu.Hopple
This is working fine in my chrome but not safari (if you move the mouse down to item 2 in the submenu the whole thing closes prematurely).Madalene
dynamic-nested-menus.stackblitz.io Can you make this with on hover?Caplan
On menu scrollbar it doesn't work. IS there any way to include scrollbar ?Caplan
Just FYI - there's a perfect solution regarding avoiding that z-index: 1050 - https://mcmap.net/q/357065/-mat-menu-pierces-through-quot-cdk-overlay-fog-quot.Leibowitz
I noticed a minor issue in your example. When you hover over User, User 1 is highlighted. As you move over the other users, User 1 unhighlights and each other user highlights. When you hover over Admin, Admin 1 is highlighted. As you move over the other admins, each other admin highlights, but Admin 1 remains highlighted. I assume this functionality wasn't intentional? Any idea as to why this happens, because I am facing a similar issue?Indetermination
@Indetermination there are issues on github discussing this for the material library. https://github.com/angular/material/issues/3707 I revised the stackblitz to use the renderer and remove the focus class of the first element on menu open.Hopple
This works well in browsers like Chrome, but in Safari the menu closes immediately when hovering over an item in the menu. Is there a fix for this?Meanie
A
24

Simplest solution that worked for me add [hasBackdrop]="false" :

<mat-menu [hasBackdrop]="false">

</mat-menu>
Arkose answered 11/12, 2019 at 15:40 Comment(3)
Ahhhh, this is perfect! I just wanted the menu to close when the mouse left and the stupid loop problem happened.Pyrochemical
Worked great for me, thanks! I did have an error of "Can't bind to 'hasBackdrop' since it isn't a known property of 'mat-menu'" but taking the square brackets off the hasBackdrop property solved it.Y
doesn't seem to work for me. Could you please provide an example on stackblitz?Quiberon
B
11

you can do it the following way (is the best way so far):

  1. Add "#locationMenuTrigger="matMenuTrigger" trigger to your button and add "(mouseenter)" event on this button:

    <button 
       [matMenuTriggerFor]="locationMenu"
        #locationMenuTrigger="matMenuTrigger" 
        (mouseover)="locationMenuTrigger.openMenu()"
     >info</button>
    
  2. 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>
    
Bul answered 20/5, 2021 at 8:12 Comment(1)
By far the easiest solution.Juni
B
10

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.

Blackfish answered 10/12, 2018 at 14:40 Comment(6)
can you provide a working stackBlitz or update the one provided in the question, though I have found one solution, but may be I'll learn some thing new in the process. Thanks,Bathyscaphe
@SaifUllah Done.Blackfish
I'm in safari and this isn't working. When I move the mouse down from the initial trigger the triggered menu disappears.Madalene
@Madalene I know, "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." because z-index trick did not work for me.Blackfish
Am getting this error everytime when I hover on menus ERROR TypeError: Cannot read property 'nativeElement' of undefinedSungod
Am getting "core.js:7187 ERROR TypeError: Cannot read property 'nativeElement' of undefined" errorEmbezzle
L
2

Whoever like me would like understand and avoid that z-index: 1050, here's a perfect solution - https://mcmap.net/q/357065/-mat-menu-pierces-through-quot-cdk-overlay-fog-quot .

Leibowitz answered 25/9, 2019 at 2:40 Comment(0)
L
1

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>
Labefaction answered 20/8, 2020 at 8:48 Comment(1)
nice but doesn't close on mouseout +1 anywayConfine
C
1

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>

STACKBLITZ DEMO

Counterglow answered 25/8, 2020 at 13:15 Comment(1)
The problem with your solution is it doesn't close when mouseout event.Finespun
E
1

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>
Embezzle answered 10/9, 2020 at 5:45 Comment(0)
M
1

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.

Mesarch answered 19/9, 2021 at 14:33 Comment(0)
F
0

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>
Ferroconcrete answered 4/5, 2022 at 15:20 Comment(0)
M
0

maybe that can help :)

 <mat-icon #menuTrigger="matMenuTrigger"
            (mouseover)="menuTrigger.openMenu()"
            (mouseleave)="menuTrigger.closeMenu()"
            [matMenuTriggerFor]="afterMenu">keyboard_arrow_down
  </mat-icon>
  <mat-menu #afterMenu="matMenu" xPosition="after" [hasBackdrop]="false">
    <button mat-menu-item>Item 1</button>
    <button mat-menu-item>Item 2</button>
  </mat-menu>
Magenta answered 24/6, 2022 at 14:21 Comment(0)
M
0

With the help of Sunil Kumar's answer, I found the solution for my issue. To remove the continuous flicker loop of mat-menu when mouse hovering, in the style.scss file, I had to add the following code:

.cdk-overlay-container .cdk-overlay-connected-position-bounding-box:has(.my-mat-menu-class) {
  margin-top: 10px; // or how many pixels is need it in your case
  // outline: 1px solid red //to help you to find exactly how many pixels you need;
}

<mat-menu class="my-mat-menu-class"></mat-menu>
Messenger answered 6/2 at 15:0 Comment(0)
C
-1

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

Correy answered 10/5, 2019 at 22:54 Comment(0)
P
-1
(mouseenter)="
      targetMenuTrigger = createPlanmenuTrigger;
      mouseEnter(createPlanmenuTrigger)
    "
    (mouseleave)="mouseLeave(createPlanmenuTrigger)"

targetMenuTrigger is assigned when hovering so we can use it if mat-menu is out of the loop.

[hasBackdrop]="false"
[overlapTrigger]="false"

config to mat-menu

code example :

**------------HTML----------------**
<mat-icon
  class="nav-link"
  #createPlanmenuTrigger="matMenuTrigger"
  [matMenuTriggerFor]="createPlan" (mouseenter)="
  targetMenuTrigger = createPlanmenuTrigger;
  mouseEnter(createPlanmenuTrigger)"
  (mouseleave)="mouseLeave(createPlanmenuTrigger)"
  >Create Plan</mat-icon>

<mat-menu
  #createPlan="matMenu"
  [hasBackdrop]="false"
  [overlapTrigger]="false">
  <div (mouseenter)="mouseEnter(targetMenuTrigger)"
    (mouseleave)="mouseLeave(targetMenuTrigger)">
    <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>
  </div>
</mat-menu>

------------ts--------------

public timedOutCloser;
  public targetMenuTrigger;
  mouseEnter(trigger) {
    if (this.timedOutCloser) {
      clearTimeout(this.timedOutCloser);
    }
    trigger.openMenu();
  }

  mouseLeave(trigger) {
    this.timedOutCloser = setTimeout(() => {
      trigger.closeMenu();
    }, 50);
  }

Example: https://stackblitz.com/edit/angular-material-mat-menu-zenp65

Proclivity answered 6/11, 2023 at 11:16 Comment(0)
L
-2

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.

Lorenelorens answered 1/8, 2019 at 17:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.