Per Material Issue 9631 centering the mat-menu
to the button is as follows.
the issue is that supporting it deviates from the spec and may end up looking weird.
I have a need for this functionality... Because writing my own CDK overlay would be more time intensive than overriding the mat-menu
component... I am not interested in re-creating the mat-menu
, and simply need the menu to be centered... I am also not interested in any other library's to accomplish this, I want to use Material mat-menu
, so my question is as follows.
Question:
Utilizing an angular directive
, how can I override the private variables and methods of the MatMenuTrigger
to center the CDK overlay to the button?
I chose a Directive because MatMenuTrigger
is a Directive
, it made sense to replicate the logic used by the Material source.
So far this is the approach I have come up with, I am not sure if reaching into the class like this is "acceptable" per se, or if there are any negative ramifications for doing it this way.
I am open to constructive discussion on this approach for any recommendations or improvements.
Stackblitz
https://stackblitz.com/edit/angular-jpgjdc-nqmyht?embed=1&file=app/center-matmenu.directive.ts
Essentially I am decoupling the matMenuTrigger
from the button by placing it on a div
wrapping the mat-menu
... this is so I can programmatically open the menu from the directive and not the button.
menuTrigger
to a templateRef
on the div
and passing it as an input to my center-mat-menu
selector<button mat-button [center-mat-menu]="menuTrigger">Menu</button> <div #menuTrigger="matMenuTrigger" [matMenuTriggerFor]="menu"> <mat-menu #menu="matMenu">
From there I am creating a listener via @HostListener
@HostListener('click', ['$event'])
And then replicating the logic of the MatMenuTrigger
directive to manipulation the placement, initialize the menu, and fire the open animation on click.
This is basically a replication of the openMenu()
method in the menu-trigger.ts
source except I am manipulating the left and top styles after the menu is initialized, and before I call this.menuTrigger.menu['_startAnimation']();
to open the menu.
I store the dimension of the source button into variables and use that information to calculate the center point of the button, I then use that with the width of the initialized menu to calculate
left
@Directive({
selector: "[center-mat-menu]"
})
export class CenterMatmenuDirective {
overlayRef: OverlayRef;
overlayConf: OverlayConfig;
dropDown: HTMLElement;
overlayPositionBox: HTMLElement;
menu: MatMenuPanel;
button: HTMLElement;
buttonWidth: number;
buttonLeft: number;
buttonBottom: number;
@Input("center-mat-menu") private menuTrigger: MatMenuTrigger;
constructor(private _menuButton: ElementRef, private _renderer: Renderer2) {}
@HostListener("click", ["$event"])
onclick(e) {
this._setVariables();
//menu not opened by keyboard down arrow, have to set this so MatMenuTrigger knows the menu was opened with a mouse click
this.menuTrigger["_openedBy"] = e.button === 0 ? "mouse" : null;
this._overrideMatMenu();
this.dropDown = this.overlayRef.overlayElement.children[0].children[0] as HTMLElement;
this.overlayPositionBox = this.overlayRef.hostElement;
setTimeout(() => {
this._styleDropDown(this.dropDown);
this._setOverlayPosition(this.dropDown, this.overlayPositionBox);
this._openMenu();
});
}
private _setVariables() {
const config = this.menuTrigger["_getOverlayConfig"]();
this.menuTrigger["_overlayRef"] = this.menuTrigger["_overlay"].create(config);
this.overlayRef = this.menuTrigger["_overlayRef"];
this.overlayConf = this.overlayRef.getConfig();
this.overlayRef.keydownEvents().subscribe();
this.menu = this.menuTrigger.menu;
this._setButtonVars();
}
private _setButtonVars() {
this.button = this._menuButton.nativeElement;
this.buttonWidth = this.button.getBoundingClientRect().width;
this.buttonLeft = this.button.getBoundingClientRect().left;
this.buttonBottom = this.button.getBoundingClientRect().bottom;
}
private _overrideMatMenu() {
let strat = this.overlayConf.positionStrategy as FlexibleConnectedPositionStrategy;
this.menuTrigger["_setPosition"](strat);
strat.positionChanges.subscribe(() => {
this._setButtonVars();
this._setOverlayPosition(this.dropDown, this.overlayPositionBox);
});
this.overlayConf.hasBackdrop =
this.menu.hasBackdrop == null ? !this.menuTrigger.triggersSubmenu() : this.menu.hasBackdrop;
this.overlayRef.attach(this.menuTrigger["_getPortal"]());
if (this.menu.lazyContent) {
this.menu.lazyContent.attach();
}
this.menuTrigger["_closeSubscription"] = this.menuTrigger["_menuClosingActions"]().subscribe(() => {
this.menuTrigger.closeMenu();
});
this.menuTrigger["_initMenu"]();
}
private _styleDropDown(dropDown: HTMLElement) {
this._renderer.setStyle(this._renderer.parentNode(dropDown), "transform-origin", "center top 0px");
}
private _setOverlayPosition(dropDown: HTMLElement, overlayPositionBox: HTMLElement) {
let dropDownleft = this.buttonWidth / 2 + this.buttonLeft - dropDown.offsetWidth / 2;
this._renderer.setStyle(overlayPositionBox, "top", this.buttonBottom + 5 + "px");
this._renderer.setStyle(overlayPositionBox, "left", dropDownleft + "px");
this._renderer.setStyle(overlayPositionBox, "height", "100%");
}
private _openMenu() {
this.menuTrigger.menu["_startAnimation"]();
}
}