Skip to content

Using `fyzMatMenuTrigger`

@flyze/lib-core-angular 1.0.0-alpha.34· latest

How to open a Material menu with a gap, a viewport margin, or from an arbitrary element or coordinate.

Behavior and limitations: features/mat-menu-trigger.

FyzMenuTrigger is a standalone directive, so import the class — there is no module:

@Component({
standalone: true,
imports: [MatMenuModule, FyzMenuTrigger],
})

In an NgModule-based component, add FyzMenuTrigger to the module’s imports (not declarations). MatMenuModule is still required — the menu itself is Material’s.

<button [fyzMatMenuTriggerFor]="menu" [gap]="8" [viewportMargin]="16">Actions</button>
<mat-menu #menu="matMenu" yPosition="below">
<button mat-menu-item>Rename</button>
<button mat-menu-item>Delete</button>
</mat-menu>

That is the whole difference from matMenuTriggerFor. gap defaults to 0, viewportMargin to 8. xPosition / yPosition on the <mat-menu> work as before, and so do menuOpened, menuClosed, closeMenu() and menuOpen.

The gap is applied along the vertical axis — yPosition="below" pushes the menu down by gap, "above" pushes it up. It holds in every position the menu can take, including the fallback the CDK picks when your preferred side does not fit.

Open from an element that is not the trigger

Section titled “Open from an element that is not the trigger”

Useful when many buttons share one menu, or when the visual anchor is not the element that was clicked. Declare a trigger with no for, grab it, and point it at an element:

<div class="invisible" #trigger="fyzMatMenuTrigger" fyzMatMenuTrigger></div>
<button (click)="open($event, menu)">Row 1</button>
<button (click)="open($event, menu)">Row 2</button>
<mat-menu #menu="matMenu"><button mat-menu-item>Rename</button></mat-menu>
@ViewChild('trigger') private trigger: FyzMenuTrigger;
open(event: MouseEvent, menu: MatMenu) {
if (!(event.target instanceof HTMLElement)) return;
this.trigger.menu = menu;
this.trigger.openMenuAtElement(event.target);
}

Assign menu before opening — that is what lets one trigger serve several menus. The element is used for that one open only; calling openMenu() afterwards falls back to the host element of the directive.

Keep the host element in the layout (visibility: hidden, or a zero-size box) rather than display: none, so Material still has an element to attach to.

onContextMenu(event: MouseEvent) {
event.preventDefault();
this.trigger.menu = this.contextMenu;
this.trigger.openMenuAt(event.clientX, event.clientY);
}

Viewport coordinates. A menu opened this way stays pinned to the point rather than following anything — which is what a context menu should do.

Anchor the menu to a wrapper instead of the trigger

Section titled “Anchor the menu to a wrapper instead of the trigger”

When the trigger is not the box the menu should line up with — a search input inside a chips container, for example — point anchor at the wrapper. The menu then attaches to the wrapper on every open, while the trigger, its click handling and its ARIA attributes stay on the input:

<div #wrapper class="chips-field">
<span class="chip">Kitchen</span>
<input [fyzMatMenuTriggerFor]="menu" [anchor]="wrapper" />
</div>

A template ref (#wrapper) or an ElementRef from @ViewChild both work. Without it the menu would line up with the input, drifting further right as each chip pushes the input along.

Do not try to do this with openMenuAtElement() from (menuOpened). That event fires from inside openMenu(), so the nested openMenu() early-returns and the element is applied to the next open instead of the current one — the first open looks wrong and the second looks right. Use anchor.

openMenuAtElement() still wins for the one open it is called for, so the two compose: a persistent anchor for normal opens, an explicit element for a one-off.

By default a click on the trigger toggles the menu, exactly as Material does. For a combobox that is usually wrong — toggleMenu() means clicking the input to move the caret closes the list. Turn the click handling off and drive it from your own handlers:

<input
#trigger="fyzMatMenuTrigger"
[fyzMatMenuTriggerFor]="menu"
[openOnClick]="false"
(focus)="trigger.openMenu()"
(keydown)="onKeydown($event, trigger)"
/>

The menu stays linked, so aria-haspopup, aria-expanded and aria-controls keep working — which is why this is better than the tempting [fyzMatMenuTriggerFor]="disabled ? null : menu". Nulling the menu to stop it opening also strips those attributes; [openOnClick]="!disabled" does not.

openOnClick only covers the click. A submenu trigger still opens on hover and RIGHT_ARROW.

Close the menu when the trigger scrolls away

Section titled “Close the menu when the trigger scrolls away”

An open menu follows its trigger, including inside a nested overflow: auto container you have not marked cdkScrollable. Once the trigger scrolls out of sight the menu keeps following it and is clamped to the viewport edge — Material’s behavior. If you would rather have the menu go away:

<button [fyzMatMenuTriggerFor]="menu" closeOnScrollOutOfView>Actions</button>

It works as a bare attribute or as a binding, and it checks the trigger against the viewport as well as against every scroll container it sits in.

Menus opened with openMenuAt(x, y) are the exception: they stay pinned to the point, so there is nothing to follow.

  • Nothing behind an open menu is interactive. Material’s backdrop swallows clicks and wheel events, so you cannot scroll the page underneath → proposals/menu-trigger-scroll-pass-through. This is worth knowing when you try to test the scroll behavior by hand: nothing outside the menu can be wheeled while it is open.

The /fyz-mat-menu-trigger route of the test app has all three usages — per-button triggers, one shared trigger opened at the clicked element, and one trigger with one menu repositioned per button — plus a window-scroll row, two scroll boxes, and a chips field using anchor with [openOnClick]="false".

The scroll boxes have a “start auto scroll” button because a backdrop-covered page cannot be wheeled: start the scroll, then open the menu inside the box.