Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Right-click context menu #5007

Open
jelbourn opened this issue Jun 7, 2017 · 41 comments
Open

Right-click context menu #5007

jelbourn opened this issue Jun 7, 2017 · 41 comments
Labels
area: material/menu feature This issue represents a new feature or feature request rather than a bug or bug fix P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent

Comments

@jelbourn
Copy link
Member

jelbourn commented Jun 7, 2017

This would effectively be md-menu but triggered by right-click instead of a specific element on the page.

Would need some investigation for a11y.

@jelbourn jelbourn added feature This issue represents a new feature or feature request rather than a bug or bug fix P5 The team acknowledges the request but does not plan to address it, it remains open for discussion post-beta labels Jun 7, 2017
@fxck fxck mentioned this issue Jun 20, 2017
@dschnelldavis
Copy link

As a temporary workaround, until material2 adds this feature, it's currently possible to simulate a context menu by putting a hidden menu trigger next to the item you want to right-click, like so:

import { Component, ViewChild } from '@angular/core';
import { MdMenuTrigger } from '@angular/material';

@Component({
  selector: 'contextmenu-example',
  template: `
    <span [mdMenuTriggerFor]="contextMenu"></span>
    <button md-button (contextmenu)="openContextMenu($event)">Context Menu</button>
    <md-menu #contextMenu="mdMenu">
      <button md-menu-item>Item 1</button>
      <button md-menu-item>Item 2</button>
    </md-menu>
  `,
})
export class ContextMenuExample {
  @ViewChild(MdMenuTrigger) contextMenu: MdMenuTrigger;

  openContextMenu(event) {
    event.preventDefault(); // Suppress the browser's context menu
    this.contextMenu.openMenu(); // Open your custom context menu instead
  }
}

This workaround is functional, but not perfect—so I'm looking forward to when material2 adds built-in support for context menus.

@heyanctil
Copy link

@dschnelldavis we did something similar to use mdMenu as a contextual menu. But have you encountered problems with the overlay backdrop? I explain my case. We have a map with different markers and we show mdMenu on right clicking these markers. But, between each right click, if you don't close the menu, the backdrop intercept the right click and display the browser contextual menu instead. Have you manage this situation?

@jraadt
Copy link

jraadt commented Sep 2, 2017

It would be great if it could support dynamic menus with a variable number of submenus, like what was suggested in #4995.

Different elements I right click on may produce slightly different menu options and submenus. I'm not sure how to create dynamic submenus since I think I would need dynamic template reference variables on those submenus.

@irowbin
Copy link

irowbin commented Sep 3, 2017

I've created a temporary contextmenu of mdMenu with small css changes. This is just a temp until material release. I am having hard time when each element have different logic. Somehow i made it work with all component by creating re-usable module and all the items of mdMenu is dynamically created.

For instance.

capture

@abdel-ships-it
Copy link

@jelbourn Would it be acceptable to have the menu items become navigable through keyboard arrow keys? That's how context menus work in chrome macOS, or would we rather have it work with the tab key.

I am currently making a context menu by utilizing the Overlay package in the cdk

@ghost
Copy link

ghost commented Jan 18, 2018

@jelbourn any progress on this ?

@DennisSmolek
Copy link

@heyanctil Until they expose the overlay I added a littlebit of a hack that has been working well:

this.trigger.openMenu();
document.getElementsByClassName('cdk-overlay-backdrop')[0].addEventListener('contextmenu', (offEvent: any) => {
    console.log('Context menu triggered!');
     offEvent.preventDefault();
    this.trigger.closeMenu();
});

The CDK destroys the element when it's closed which destroys the listener...

@MikeAgostino
Copy link

Hey @jelbourn, has there been any progress on integrating this into Angular Material?

@jelbourn
Copy link
Member Author

Nope- context menu isn't super high on our priority list

@sssalib42
Copy link

Ideally, when a context menu is up, and the user right-clicks on a different element which has a context menu, the expected behavior is that the first context menu will be closed and the second context menu, triggered by the right-click, be opened. Please consider.

@simonbland
Copy link

simonbland commented Aug 29, 2018

I needed this too.

Here is how I've implemented it, inspired by the solution of @dschnelldavis. I've added precise positioning of the context menu and reference to the contextual data:

<mat-list>
  <mat-list-item *ngFor="let item of items" (contextmenu)="onContextMenu($event, item)">
    {{ item.name }}
  </mat-list-item>
</mat-list>
<div style="visibility: hidden; position: fixed"
    [style.left]="contextMenuPosition.x"
    [style.top]="contextMenuPosition.y"
    [matMenuTriggerFor]="contextMenu">
</div>
<mat-menu #contextMenu="matMenu">
  <ng-template matMenuContent let-item="item">
    <button mat-menu-item (click)="onContextMenuAction1(item)">Action 1</button>
    <button mat-menu-item (click)="onContextMenuAction2(item)">Action 2</button>
  </ng-template>
</mat-menu>
import { Component, ViewChild } from '@angular/core';
import { MatMenuTrigger } from '@angular/material';

@Component({
  selector: 'context-menu-example',
  templateUrl: 'context-menu-example.html'
})
export class ContextMenuExample {

  items = [
    {id: 1, name: 'Item 1'},
    {id: 2, name: 'Item 2'},
    {id: 3, name: 'Item 3'}
  ];

  @ViewChild(MatMenuTrigger)
  contextMenu: MatMenuTrigger;

  contextMenuPosition = { x: '0px', y: '0px' };

  onContextMenu(event: MouseEvent, item: Item) {
    event.preventDefault();
    this.contextMenuPosition.x = event.clientX + 'px';
    this.contextMenuPosition.y = event.clientY + 'px';
    this.contextMenu.menuData = { 'item': item };
    this.contextMenu.menu.focusFirstItem('mouse');
    this.contextMenu.openMenu();
  }

  onContextMenuAction1(item: Item) {
    alert(`Click on Action 1 for ${item.name}`);
  }

  onContextMenuAction2(item: Item) {
    alert(`Click on Action 2 for ${item.name}`);
  }
}

export interface Item {
  id: number;
  name: string;
}

Here is a working example on StackBlitz.

(code edited based on #5007 (comment) and #5007 (comment))

@hgndgn
Copy link

hgndgn commented Sep 9, 2018

@simonbland Do you know why your solution does not work properly with a material-table ?
I can not manipulate the x/y position of the context menu. It shows up only on left top or right top of the table element. If you could help, it would be nice

@simonbland
Copy link

simonbland commented Sep 14, 2018

Hi @hgndgn,

Here is another working example, but with a table instead of list, also on StackBlitz.

This is the same implementation, except that the table was replaced with a list and this is working fine.

(code edited based on #5007 (comment) and #5007 (comment))

@hgndgn
Copy link

hgndgn commented Sep 14, 2018

Thank you @simonbland it works now.
I had before this part

<td>
   <div style="position: absolute"
          [style.left]="contextMenuPosition.x"
          [style.top]="contextMenuPosition.y"
          [matMenuTriggerFor]="contextMenu"
          [matMenuTriggerData]="{item: item}">
   </div>
 </td>

inside the last <tr> tag (displayedColumns) of the table. But now, it does not matter in which column I insert this, it works correct.

Thank you again!

@codestitch
Copy link

I've created a temporary contextmenu of mdMenu with small css changes. This is just a temp until material release. I am having hard time when each element have different logic. Somehow i made it work with all component by creating re-usable module and all the items of mdMenu is dynamically created.

For instance.

capture

@irowbin do you have a stackblitz example of this? This is really great!

@TauanMatos
Copy link

I've created a temporary contextmenu of mdMenu with small css changes. This is just a temp until material release. I am having hard time when each element have different logic. Somehow i made it work with all component by creating re-usable module and all the items of mdMenu is dynamically created.

For instance.

capture

@irowbin This is awesome! Can you share it on stackblitz ?

@irowbin
Copy link

irowbin commented Nov 21, 2018

@codestitch @TauanMatos sorry that the source code from the image above is not available at the moment.😢 To popup the context-menu you write few css rules for the mat-menu, few js code to adjust position dynamically based on the event target wrapper and that's it.😉 I did the same thing

Take a look at these links to get an idea which is written in vanilla js. Not the Angular or Material Design.
Its is easy to implement as needed on angular.

codepen link 1 & codepen link 2

@TauanMatos
Copy link

@irowbin Thx XD

s2-abdo pushed a commit to s2-abdo/material2 that referenced this issue Jan 18, 2019
…to a point (angular#14616)

Allows for the connected overlay's origin to be set to a point on the page, rather than a DOM element. This allows people to easily implement right click context menus.

Relates to angular#5007.
s2-abdo pushed a commit to s2-abdo/material2 that referenced this issue Jan 18, 2019
…to a point (angular#14616)

Allows for the connected overlay's origin to be set to a point on the page, rather than a DOM element. This allows people to easily implement right click context menus.

Relates to angular#5007.
@ghost
Copy link

ghost commented Jan 29, 2019

@s2-abdo can you give us an example about how to use the new implementation?
Thanks!

@NetanelBasal
Copy link

@eusaro https://netbasal.com/context-menus-made-easy-with-angular-cdk-963797e679fc

@lasfrancisco
Copy link

Amanzing. Can't wait to see matMenuTrigger taking advantage from it.

@diosney
Copy link

diosney commented Mar 12, 2019

@wizdmio Thanks! Your solution works as a charm aside to be very clean.

@philip-firstorder
Copy link

philip-firstorder commented Jul 7, 2019

@simonbland Why put the trigger inside *ngFor and have it duplicated?

  • Since it's position: fixed it will just create a multitude of empty divs at the top-left page corner at start-up.
  • It triggers a more intensive angular rendering as all of them will have to update the same contextMenuPosition values
  • It might interfere with other css styling in mat-list-item
<mat-list>
  <mat-list-item *ngFor="let item of items" (contextmenu)="onContextMenu($event, item)">
    {{ item.name }}
    <div style="position: fixed"
        [style.left]="contextMenuPosition.x"
        [style.top]="contextMenuPosition.y"
        [matMenuTriggerFor]="contextMenu"
        [matMenuTriggerData]="{item: item}">
    </div>
  </mat-list-item>
</mat-list>
<mat-menu #contextMenu="matMenu">
  <ng-template matMenuContent let-item="item">
    <button mat-menu-item (click)="onContextMenuAction1(item)">Action 1</button>
    <button mat-menu-item (click)="onContextMenuAction2(item)">Action 2</button>
  </ng-template>
</mat-menu>

Putting it only once like this accomplishes the same result in a more efficient way:

  • I put an extra visibility: hidden to make sure it doesn't render;
  • I removed [matMenuTriggerData] since you set it dynamically in .ts anyways.
<mat-list>
  <mat-list-item *ngFor="let item of items" (contextmenu)="onContextMenu($event, item)">
    {{ item.name }}
  </mat-list-item>
</mat-list>

<div style="visibility: hidden; position: fixed;"
    [style.left]="contextMenuPosition.x"
    [style.top]="contextMenuPosition.y"
    [matMenuTriggerFor]="contextMenu">
</div>
<mat-menu #contextMenu="matMenu">
  <ng-template matMenuContent let-item="item">
    <button mat-menu-item (click)="onContextMenuAction1(item)">Action 1</button>
    <button mat-menu-item (click)="onContextMenuAction2(item)">Action 2</button>
  </ng-template>
</mat-menu>

@simonbland
Copy link

Thank you @philip-firstorder!

I agree with all your points.

I remember I was not totally happy with putting the trigger inside *ngFor, but simply didn't realise at the time I wrote this code that [matMenuTriggerData] was not necessary and hence the trigger could be moved outside the loop.

I've update the original example on StackBlitz with your enhancement.

Cheers!

@philip-firstorder
Copy link

@simonbland Very nice, you could also change the code in your original comment, so it matches the stackblitz

@simonbland
Copy link

@philip-firstorder Done, thanks!

@camargo
Copy link

camargo commented Nov 14, 2019

@simonbland Great and simple solution, thanks for posting it.
One small issue I'm seeing is that when the mat-menu contextmenu opens, the first mat-menu-item is always highlighted.
Do you or anyone here know whats going on with that?

EDIT: I found a solution. I had to update onContextMenu as follows:

  onContextMenu(event: MouseEvent, item: Item) {
    event.preventDefault();
    this.contextMenuPosition.x = event.clientX + 'px';
    this.contextMenuPosition.y = event.clientY + 'px';
    this.contextMenu.menuData = { item };
    this.contextMenu._openedBy = 'mouse';
    this.contextMenu.openMenu();
  }

You need to tell the context menu trigger that it's opened by a mouse or it highlights the first item for keyboard selection (defaults to 'program' instead of 'mouse').

Note you could also create a ViewChild to the context menu itself, and call focusFirstItem('mouse'); on it if you don't want to overwrite the _openedBy private variable.

@simonbland
Copy link

Hi @camargo,

Thank you for the improvement and for the explanations why the first item is highlighted 👍

To fix this, I've found that we can merge the two alternative solutions you proposed, and instead of:

this.contextMenu._openedBy = 'mouse';

We can write this:

this.contextMenu.menu.focusFirstItem('mouse');

This doesn't involve calling the private _openedBy field, and also doesn't requires that we create a new ViewChild to the context menu itself.

I've updated the examples on StackBlitz:

@kreinerjm
Copy link

@simonbland Is this sort of thing possible with your implementation? In your examples right clicking anywhere while a context menu is open shows the browser context menu.

Ideally, when a context menu is up, and the user right-clicks on a different element which has a context menu, the expected behavior is that the first context menu will be closed and the second context menu, triggered by the right-click, be opened. Please consider.

I found an example that has this functionality, but it doesn't use the material context menu (I would prefer material over cdk).

https://stackblitz.com/edit/angular-yd6ay3

@SimonGAndrews
Copy link

SimonGAndrews commented Jan 21, 2020

Thank you , @simonbland and @camargo, Very useful indeed.
Also I can confirm from a quick test, that the technique works in the Angular Material Tree control, within and below the 'mat-tree-node' tags as per the 'mat-list-item' in your code example.

@SimonGAndrews
Copy link

SimonGAndrews commented Jan 21, 2020

ah …. @simonbland and @camargo, …. is it correct that this technique is relying on the HTML contextmenu attribute that is no longer supported on browsers ??
https://developer.mozilla.org/en-US/docs/web/html/global_attributes/contextmenu

Edit: Ok Looked into this more and I got this wrong. I now understand, this is Not correct. It Is the HTML attribute 'contextmenu' that is being made obsolete. And In the example code here;

                         (contextmenu)="onContextMenu($event, item)

(contextmenu) is the HTML oncontextmenu Event (not the attribute) and the key learning for a newbie like myself is: Angular not using the 'on' in event names has to be considered when looking up the mechanics of code examples like these.

So … Back to where I started... Thanks for the great solution for a context menu in Angular material.

reference: https://www.w3schools.com/jsref/event_oncontextmenu.asp

@simonbland
Copy link

@simonbland Is this sort of thing possible with your implementation? In your examples right clicking anywhere while a context menu is open shows the browser context menu.

Ideally, when a context menu is up, and the user right-clicks on a different element which has a context menu, the expected behavior is that the first context menu will be closed and the second context menu, triggered by the right-click, be opened. Please consider.

I found an example that has this functionality, but it doesn't use the material context menu (I would prefer material over cdk).

https://stackblitz.com/edit/angular-yd6ay3

Hi @kreinerjm,

You are right. Thank you for pointing this out. I've quickly tried to workaround this issue, but didn't find a solution using the CDK. If someone finds a nice solution for this, I will update the code examples.

I've used this context menu implementation for an Electron application, where the browser context menu is disabled, so this problem don't appear.

@simonbland
Copy link

Thank you , @simonbland and @camargo, Very useful indeed.
Also I can confirm from a quick test, that the technique works in the Angular Material Tree control, within and below the 'mat-tree-node' tags as per the 'mat-list-item' in your code example.

Hi @SimonGAndrews,

Good to know!

@simonbland
Copy link

ah …. @simonbland and @camargo, …. is it correct that this technique is relying on the HTML contextmenu attribute that is no longer supported on browsers ??
https://developer.mozilla.org/en-US/docs/web/html/global_attributes/contextmenu

Edit: Ok Looked into this more and I got this wrong. I now understand, this is Not correct. It Is the HTML attribute 'contextmenu' that is being made obsolete. And In the example code here;

                         (contextmenu)="onContextMenu($event, item)

(contextmenu) is the HTML oncontextmenu Event (not the attribute) and the key learning for a newbie like myself is: Angular not using the 'on' in event names has to be considered when looking up the mechanics of code examples like these.

So … Back to where I started... Thanks for the great solution for a context menu in Angular material.

reference: https://www.w3schools.com/jsref/event_oncontextmenu.asp

Thank for sharing your reasoning. It looks correct to me.

However, I would say that (contextmenu) is the equivalent for the HTML oncontextmenu Event. Angular does not necessarily implement it like that. In fact, I don't see DOM onevent handlers in Angular generated code.

Here is some more documentation on this topic, for those who are interested:

@camargo
Copy link

camargo commented Feb 12, 2020

FWIW I created a version of this that does not require adding a contextMenuPosition or a ViewChild to the host component. It requires accessing a MatMenuTrigger private _element property. If anyone has a better way to access aMatMenuTrigger native element let me know.

export function onContextMenu(
  event: MouseEvent,
  trigger: MatMenuTrigger,
  data: any,
) {
  event.preventDefault();
  // @ts-ignore
  const triggerElement: HTMLElement = trigger._element.nativeElement;
  triggerElement.style.setProperty('left', `${event.clientX}px`);
  triggerElement.style.setProperty('position', 'fixed');
  triggerElement.style.setProperty('top', `${event.clientY}px`);
  triggerElement.style.setProperty('visibility', 'hidden');
  trigger.menuData = { data };
  trigger.menu.focusFirstItem('mouse');
  trigger.openMenu();
}
<button (click)="onContextMenu($event, contextMenuTrigger, {})">
  Open Context Menu
</button>

<div #contextMenuTrigger="matMenuTrigger" [matMenuTriggerFor]="contextMenu">
  <mat-menu #contextMenu="matMenu">
    <ng-template matMenuContent let-data="data">
      <button mat-menu-item>
        <mat-icon>delete_forever</mat-icon>
        <span>Delete</span>
      </button>
    </ng-template>
  </mat-menu>
</div>

@mmalerba
Copy link
Contributor

Context menus are now supported by the CdkMenu (https://material.angular.io/cdk/menu/overview#context-menus). We should re-implement the MatMenu to be based on the CdkMenu and it will get this feature for free.

@mmalerba mmalerba added P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent and removed P5 The team acknowledges the request but does not plan to address it, it remains open for discussion labels Jun 29, 2022
@mmalerba mmalerba self-assigned this Jun 29, 2022
@jneuhaus20
Copy link

@mmalerba CdkMenu doesn't support passing a context to the template like MatMenu does, fyi. I just ran into that trying to replace a discontinued context menu library, so I'm stuck with working around MatMenu's shortcomings in the meantime. Luckily I don't need positioning, just a different triggering event.

@jelbourn
Copy link
Member Author

@jneuhaus20 could you open a feature request for cdk menu for this? That will make it easier to track (and mark as a good community contribution)

@jneuhaus20
Copy link

Feature request made

@Eugeny
Copy link

Eugeny commented Feb 16, 2023

This is actually now possible with FlexibleConnectedPositionStrategy.setOrigin: https://gist.github.com/Eugeny/8935314c874c9fd784c942ebcf0679f9 (relevant code in ngAfterContentInit)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: material/menu feature This issue represents a new feature or feature request rather than a bug or bug fix P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent
Projects
None yet
Development

No branches or pull requests