angularangular-material

Open Material menu at mouse position in Angular


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.


Solution

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