When triggering a Material menu (mat-menu) it opens relative to the clicked object (above, below, before, after). My object is a very large container so I need the menu to open at the mouse position when clicking for a better user experience.
How can I do this?
I am using Angular 14.
I solved it using a custom directive that extends the material menu.
import { ConnectedPosition, FlexibleConnectedPositionStrategy } from '@angular/cdk/overlay';
import { Directive, Input } from '@angular/core';
import { MatMenuPanel, _MatMenuTriggerBase } from '@angular/material/menu';
// @Directive declaration styled same as matMenuTriggerFor
// with different selector and exportAs.
@Directive({
selector: `[matContextMenuTriggerFor]`,
host: {
class: 'mat-menu-trigger',
},
exportAs: 'matContextMenuTriggerDirective',
})
export class MatContextMenuTriggerDirective extends _MatMenuTriggerBase {
private lastMouseEvent: MouseEvent | null = null;
// Duplicate the code for the matMenuTriggerFor binding
// using a new property and the public menu accessors.
@Input('matContextMenuTriggerFor')
get menu_again() {
return this.menu!;
}
set menu_again(menu: MatMenuPanel) {
this.menu = menu;
}
// Override _handleMousedown, and call super._handleMousedown
_handleMousedown(event: MouseEvent): void {
this.lastMouseEvent = event;
super._handleMousedown(new MouseEvent(event.type));
}
ngAfterContentInit() {
// Can't just override a private method due to how the lib is compiled
this['_setPosition'] = (menu: any, positionStrategy: FlexibleConnectedPositionStrategy) => {
let positions: ConnectedPosition[] = [];
super['_setPosition'](menu, {
withPositions: (p: any) => {
positions = p;
},
});
positionStrategy.withPositions(positions);
if (this.lastMouseEvent) {
positionStrategy.setOrigin({ x: this.lastMouseEvent.clientX, y: this.lastMouseEvent.clientY });
}
positionStrategy.withViewportMargin(10);
};
}
ngOnDestroy() {
super.ngOnDestroy();
}
}
Use it likes this:
<div [matContextMenuTriggerFor]="nameOfMenu">Click to open menu</div>