Using mat-menu as context menu: How to close opened mat-menu, and open it in another location

Viewed 8528

Angular: v8.2.8

I manage to use mat-menu (angular material) to emulate the context menu. Right-clicking anyway on my page, the menu appears at my mouse location.

What I want to achieve: when I move my mouse to another location on the page, I right-click again. I was expecting the menu at the original location to close and reopen at the new location.

But In reality: When I right-click, nothing happens and the menu at the original location remains open.

In fact, I have to left-click to close the opened menu at original location, followed by right click to open menu at the new mouse location.

Question: Any idea how to achieve what I want?

@Component({
  selector: 'fl-home',
  template:`
 <mat-menu #contextmenu>
      <div >
           <button mat-menu-item>Clear all Paths</button>
       </div>
  </mat-menu>
  <div [matMenuTriggerFor]="contextmenu" [style.position]="'absolute'" [style.left.px]="menuX" [style.top.px]="menuY" ></div>
  
  <div class="container" (contextmenu)="onTriggerContextMenu($event);"> ....</div>
`})
export class HomeComponent  {

    menuX:number=0
    menuY:number=0

    @ViewChild(MatMenuTrigger,{static:false}) menu: MatMenuTrigger; 
    
   
    onTriggerContextMenu(event){
        
      event.preventDefault();
      this.menuX = event.x - 10;
      this.menuY = event.y - 10;
      this.menu.closeMenu() // close existing menu first.
      this.menu.openMenu()

    }
}
5 Answers

Just add hasBackdrop=false as in <mat-menu ... [hasBackdrop]="false">. What happens is that when the menu is opened after I right click, an invisible backdrop overlay appears covering the entire page. No matter what I try to right click again, the contextmenu event won't trigger and hence onTriggerContextMenu() does not get called. By setting hasBackgrop to false, this overlay will not appear.

My solution

  ngOnInit(): void {
    this.windowClickSubscription = fromEvent(window, 'click').subscribe((_) => {
      if (this.contextMenu.menuOpen) {
        this.contextMenu.closeMenu();
      }
    });
  }
  ngOnDestroy(): void {
    this.windowClickSubscription && this.windowClickSubscription.unsubscribe();
    this.menuSubscription && this.menuSubscription.unsubscribe();
  }
  onContextMenu(event: MouseEvent){
    event.preventDefault();
    this.menuSubscription && this.menuSubscription.unsubscribe();
    this.menuSubscription = of(1)
      .pipe(
        tap(() => {
          if (this.contextMenu.menuOpen) {
            this.contextMenu.closeMenu();
          }
          this.contextMenuPosition.x = event.clientX;
          this.contextMenuPosition.y = event.clientY;
        }),
        // delay(this.contextMenu.menuOpen ? 200 : 0),
        delayWhen((_) => (this.contextMenu.menuOpen ? interval(200) : of(undefined))),
        tap(async() => {
          this.contextMenu.openMenu();
          let backdrop: HTMLElement = null;
          do {
            await this.delay(100);
            backdrop = document.querySelector(
              'div.cdk-overlay-backdrop.cdk-overlay-transparent-backdrop.cdk-overlay-backdrop-showing'
            ) as HTMLElement;
          } while (backdrop === null);
          backdrop.style.pointerEvents = 'none';
        })
      )
      .subscribe();
  }
  delay(delayInms: number) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(2);
      }, delayInms);
    });
  }

https://stackblitz.com/edit/angular-ivy-udzyez

Maybe you solved it, but for others here is my approach to this problem: surround menu items in div, and when mouse leaves menu (that div) I am closing it, so you can immediately rightclick again on another place to show context menu again....

<div (mouseleave)="mouseLeaveMenu()">
  ...menu items...
</div>
...
mouseLeaveMenu() {
  if (this.contextMenu.menuOpened) this.contextMenu.closeMenu();
}
  1. add the attribute [hasBackdrop]="false" to mat-menu
  2. update onTriggerContextMenu() function to look like below
  3. Finally; to close the menu with a normal click on the same element where you have (contextmenu)="..." trigger also add (click)="themenu.closeMenu();"
    onTriggerContextMenu(event: MouseEvent) {
        this.themenu.closeMenu();
        event.preventDefault();
        this.menuX = event.clientX;
        this.menuY = event.clientY;
        this.themenu.menu.focusFirstItem('mouse');
        this.themenu.openMenu();
    }

If you don't want to close the menu first like in most other answers you could just set the position of the menu manually:

// Assume you have the position in variables x and y
// Additionally you need to have the MatMenuTrigger (in this example in variable matMenuTrigger)

// this.cd.markForCheck() is important here if you just opened the menu with matMenuTrigger.openMenu()

const panelID = matMenuTrigger.menu.panelId;
const panelElement: HTMLElement = document.getElementById(panelID);

panelElement.style.position = "absolute";
panelElement.style.left = x + "px";
panelElement.style.top = y + "px";
Related