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.
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.
@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?
It would be great if it could support dynamic menus with a variable number of submenus, like what was suggested in https://github.com/angular/material2/issues/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.
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.
@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
@jelbourn any progress on this ?
@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...
Hey @jelbourn, has there been any progress on integrating this into Angular Material?
Nope- context menu isn't super high on our priority list
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 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 https://github.com/angular/components/issues/5007#issuecomment-508992078 and https://github.com/angular/components/issues/5007#issuecomment-554124365)
@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
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 https://github.com/angular/components/issues/5007#issuecomment-508992078 and https://github.com/angular/components/issues/5007#issuecomment-554124365)
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!
I've created a temporary contextmenu of
mdMenu
with smallcss
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 ofmdMenu
is dynamically created.For instance.
@irowbin do you have a stackblitz example of this? This is really great!
I've created a temporary contextmenu of
mdMenu
with smallcss
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 ofmdMenu
is dynamically created.For instance.
@irowbin This is awesome! Can you share it on stackblitz ?
@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.
@irowbin Thx XD
Hi there, I came across the need of implementing a contextual menu with angular material today and the simplest solution I could figure out has been a component extending the MatMenuTrigger directive as per the following:
@Component({
selector: 'wm-context-menu',
template: '<ng-content></ng-content>',
styles: ['']
})
export class ContextMenuComponent extends MatMenuTrigger {
@HostBinding('style.position') private position = 'fixed';
@HostBinding('style.left') private x: string;
@HostBinding('style.top') private y: string;
// Intercepts the global context menu event
@HostListener('document:contextmenu', ['$event']) menuContext(ev: MouseEvent) {
// Closes the menu when already opened
if(this.menuOpen) {
this.closeMenu();
}
else {
// Adjust the menu anchor position
this.x = ev.clientX + 'px';
this.y = ev.clientY + 'px';
// Opens the menu
this.openMenu();
}
// prevents default
return false;
}
}
There's a working demo on stackblitz here: https://stackblitz.com/edit/wizdm-contextmenu
Hope this helps,
Cheers,
@s2-abdo can you give us an example about how to use the new implementation?
Thanks!
Amanzing. Can't wait to see matMenuTrigger taking advantage from it.
@wizdmio Thanks! Your solution works as a charm aside to be very clean.
@simonbland Why put the trigger inside *ngFor and have it duplicated?
<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:
visibility: hidden
to make sure it doesn't render;[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>
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!
@simonbland Very nice, you could also change the code in your original comment, so it matches the stackblitz
@philip-firstorder Done, thanks!
@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.
Hi @camargo,
Thank you for the improvement and for the explanations why the first item is highlighted :+1:
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:
@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).
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.
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 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).
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.
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!
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/contextmenuEdit: 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:
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>
Most helpful comment
Hi there, I came across the need of implementing a contextual menu with angular material today and the simplest solution I could figure out has been a component extending the MatMenuTrigger directive as per the following:
There's a working demo on stackblitz here: https://stackblitz.com/edit/wizdm-contextmenu
Hope this helps,
Cheers,