【问题标题】:How to open and close Angular mat menu on hover如何在悬停时打开和关闭 Angular mat 菜单
【发布时间】:2019-05-06 04:45:06
【问题描述】:

这个问题参考了this Github 问题,mat-menu 无法使用鼠标悬停切换,我基本上是在尝试用 Angular Material 的菜单替换基于引导的水平导航菜单。唯一让我无法复制基于引导程序的菜单的是在悬停时打开和关闭mat-menu。 正如上述 Github 问题中提到的,有一些解决方法可以实现我想要的,比如使用 mouseEnter

(mouseenter)="menuTrigger.openMenu()"

或在 Mat-menu 中添加 span 以绑定 mat-menu 关闭,

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

但似乎没有一个解决方案涵盖所有小场景,

例如

在上面的Github issue中提到,第一个SO解决方案有以下问题。

  • 将鼠标光标悬停在按钮上,将弹出菜单。但是,如果您单击该按钮,它将隐藏并显示菜单。恕我直言 这是一个错误。
  • 要隐藏菜单,用户需要在菜单外单击。理想情况下,如果鼠标光标在外面,菜单会隐藏
    区域(包括按钮、菜单和子菜单)
    超过 400 毫秒。

在尝试解决上述问题之一但无法正常工作的跨度解决方案中,例如

将鼠标悬停在MatMenuTrigger 上确实会按预期打开mat-menu,但如果用户在未输入mat-menu 的情况下将鼠标移开,则它不会自动关闭,这是错误的。

同时移动到二级子菜单之一也会关闭一级菜单,这不是我想要的,

P.S 将鼠标从一个打开的菜单移动到下一个同级菜单不会打开下一个菜单。正如here 所提到的,我想这可能很难实现,但我认为其中一些可能是可以实现的,对吧?

这是一个基本的stackBlitz,它重现了我的经历,感谢任何帮助。

【问题讨论】:

  • mat 菜单太烂了!
  • 完全无法使用菜单的证据:/

标签: angular angular-material


【解决方案1】:

第一个挑战是 mat-menu 在生成 CDK 覆盖时由于覆盖的 z-index 而从按钮中窃取焦点...要解决这个问题,您需要将 z-index 设置为按钮...

  • 当您将(mouseleave) 添加到按钮时,这将停止递归循环。 style="z-index:1050"

接下来,您需要跟踪 levelonelevelTwo 菜单的所有进入和离开事件的状态,并将该状态存储在两个组件变量中。

enteredButton = false;
isMatMenuOpen = false;
isMatMenu2Open = false;

接下来为两个菜单级别创建 menu enter 和 menuLeave 方法。注意 menuLeave(trigger) 检查是否访问了 level2,如果为 true,则不执行任何操作。

请注意:menu2Leave() 具有允许导航返回第一层但如果退出另一侧则关闭两者的逻辑...同时在离开层时移除按钮焦点。

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

下面是如何将它们全部连接起来。

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

堆栈闪电战

https://stackblitz.com/edit/mat-nested-menu-yclrmd?embed=1&file=app/nested-menu-example.html

【讨论】:

  • 我刚刚更新了 stackblitz 链接,将 buttonLeave 上的 setTimeout 增加到 100 毫秒...这应该允许在触发器和 mat-menu 之间的转换时设置 enter menu 变量的更多时间
  • 我还添加了按钮状态,这应该可以解决所有剩余的小问题。除此之外的任何项目,请注意该解决方案是完全可定制的,您需要调整时间等,以使解决方案在您的应用程序中高效。
  • 点击菜单的错误(触摸屏必不可少) - 第一次点击菜单会立即打开并折叠,第二次点击 - 它保持打开状态。
  • @AlexKlaus 这个问题已经解决了,请查看修改后的stackblitz
  • 确认,现在触摸屏很好。只看到两个小问题:1)再次单击菜单时它会被隐藏(参见gph.is/2SBxCnL); 2) 第 3-4 次点击菜单,它可能会在一秒钟内出现和消失。
【解决方案2】:

这是我为处理自动打开/关闭 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);
  }
}

然后你就可以在你的应用中使用它了:

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

【讨论】:

  • 这应该是选择的答案,没有 CSS 和一个简单的完整解决方案:)
  • 这比这里的任何答案都要好 100 倍。它使用原生材质 API。效果很好。需要进行一些小的调整。 mat-menu 有 8px 的顶部和底部填充,当它悬停时会导致接近火,而不是。要删除此填充,请将其添加到 styles.css / styles.scss : div.cdk-overlay-container .mat-menu-content { padding-top: 0;填充底部:0; } ...也
    应该是 以摆脱不需要的块元素
  • 请注意,如果您想要顶部和底部填充(在先前评论的建议中删除),那么您可以将其添加到您
    ...只需向该 div 添加一个类并将其样式设置为通常 - 这不会导致问题 - 你可能还想传递像这样的额外道具 这需要你为每个在平常的方式
  • 赞成,但这不适用于移动设备,除非您添加类似&lt;div trigger&gt;&lt;div (click)="$event.stopPropagation();"&gt;Auto-open&lt;/div&gt;&lt;/div&gt;
【解决方案3】:

对我有用的最简单的解决方案添加[hasBackdrop]="false"

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

</mat-menu>

【讨论】:

  • 啊啊啊,这太完美了!我只是希望在鼠标离开并发生愚蠢的循环问题时关闭菜单。
  • 非常适合我,谢谢!我确实有一个错误“无法绑定到'hasBackdrop',因为它不是'mat-menu'的已知属性”,但是从hasBackdrop属性中去掉方括号就解决了。
【解决方案4】:

此解决方案可用作 Marshal 建议的设置 z-index:1050 的替代方法。对于其他修复,您应该查看 Marshal 的答案。

你可以使用

<button [matMenuTriggerFor]="menu" #trigger="matMenuTrigger" (mouseenter)="trigger.openMenu()" (mouseleave)="trigger.closeMenu()"></button>

使用它会产生连续的闪烁循环,但有一个简单的修复方法。

只需要注意一件事,即:

菜单打开时

<div class="cdk-overlay-container"></div>

这个 div 覆盖整个屏幕,通常添加在整个 html 的末尾,就在 /body 标记之前。您的所有菜单都在此容器内生成。 (不同版本的类名可能不同)。

只需将其添加到您的 css/scss 样式文件中即可:

.cdk-overlay-container{
    left:200px;
    top:200px;
}
.cdk-overlay-connected-position-bounding-box{
    top:0 !important;

}

或任何阻止此元素与您的按钮重叠的东西。

我自己试过这个,希望我的回答清晰准确。

这是stackblitz 的演示,我已经编辑了问题中的stackblitz 代码。

【讨论】:

  • 您能否提供一个有效的 stackBlitz 或更新问题中提供的那个,虽然我已经找到了一种解决方案,但我可能会在此过程中学到一些新东西。谢谢,
  • @SaifUllah 完成。
  • 我在 safari 中,但这不起作用。当我从初始触发器向下移动鼠标时,触发的菜单消失了。
  • @Magnus 我知道,“此解决方案可用作 Marshal 建议的设置 z-index:1050 的替代方法。对于其他修复,您应该查看 Marshal 的答案。”因为 z-index 技巧对我不起作用。
  • 每次我将鼠标悬停在菜单上时都会收到此错误 ERROR TypeError: Cannot read property 'nativeElement' of undefined
【解决方案5】:

像我这样想了解并避免 z-index: 1050 的人,这里有一个完美的解决方案 - https://stackoverflow.com/a/54630251/1122524

【讨论】:

    【解决方案6】:

    您可以通过以下方式进行操作(目前为止最好的方式):

    1. 将“#locationMenuTrigger="matMenuTrigger”触发器添加到您的按钮并在此按钮上添加“(mouseenter)”事件:

      <button 
         [matMenuTriggerFor]="locationMenu"
          #locationMenuTrigger="matMenuTrigger" 
          (mouseover)="locationMenuTrigger.openMenu()"
       >info</button>
      
    2. 将您的 "(mouseleave)" 事件放在您已放入 mat-menu 的 mat-menu div/span 上,如下所示:

      <mat-menu #locationMenu="matMenu" class="location-menu">
       <span class="locations" (mouseleave)="closeMenu()">
         {{ row | notAvailable: '-' }}
       </span>
      

    【讨论】:

      【解决方案7】:

      这是另一个很好且简单的解决方案,它在 Angular Material 中创建 Hover Menu,它不需要任何 TS,但您必须在根组件中添加各种相关模块的引用。

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

      【讨论】:

      • 不错,但无论如何都不会在 mouseout +1 时关闭
      【解决方案8】:

      使用mat菜单组件的内置方法的最简单的解决方案。

      <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

      【讨论】:

      • 你的解决方案的问题是当mouseout事件时它没有关闭。
      【解决方案9】:

      如果不使用角度材料也可以,请使用以下代码。

      .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>

      【讨论】:

        【解决方案10】:

        我有一个客户 POC,它只有一个顶级菜单。 能够使这个解决方案在没有 z 索引和渲染器的情况下工作。

        我的触发按钮既不是按钮也不是matbutton,它是一个div:

        使用 matMenuTriggerFor 属性将这些属性添加到 div。 (menuOpened)="isMatMenuOpen = true;" (menuClosed)="isMatMenuOpen=false;"

        【讨论】:

          【解决方案11】:

          如果您有一个基于此解决方案的动态菜单结构:https://stackblitz.com/edit/dynamic-nested-topnav-menu),则可以使用上述超时解决方案修改现有代码。下面是一个例子:

          app.component.ts (sn-p):

            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 (sn-p):

            <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 (sn-p):

            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 (sn-p):

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

          注意事项:

          [hasBackdrop]="false" 取出菜单闪烁。

          (mouseenter) 和 (mouseleave) 函数。

          [timedOutCloser] 将 timedOutCloser 从父菜单项传递给子菜单项。

          (notify)='onNotify($event)' 通知父子菜单进入和离开事件。

          【讨论】:

            【解决方案12】:

            我在我的项目中使用这种方式,但即使我在实现 (mouseleave) 后应用 style="z-index:1050" 它也不会停止递归循环。我很困惑。此外,我的菜单组件是递归菜单组件,我这样做不知道子菜单是否有相同的触发器名称,它会正常工作。

            【讨论】:

              猜你喜欢
              相关资源
              最近更新 更多
              热门标签