src/menu-button/menu-button.component.ts
OnChanges
AfterViewInit
OnDestroy
changeDetection | ChangeDetectionStrategy.OnPush |
selector | cds-menu-button |
template |
|
Properties |
|
Methods |
Inputs |
HostBindings |
Accessors |
constructor(ngZone: NgZone, renderer: Renderer2, hostElement: ElementRef, viewContainerRef: ViewContainerRef, changeDetectorRef: ChangeDetectorRef)
|
||||||||||||||||||
Defined in src/menu-button/menu-button.component.ts:109
|
||||||||||||||||||
Parameters :
|
buttonTabIndex | |
Type : "0" | "1" | "-1" | string
|
|
Default value : "0"
|
|
Defined in src/menu-button/menu-button.component.ts:96
|
disabled | |
Type : boolean
|
|
Default value : false
|
|
Defined in src/menu-button/menu-button.component.ts:97
|
kind | |
Type : "primary" | "tertiary" | "ghost"
|
|
Default value : "primary"
|
|
Defined in src/menu-button/menu-button.component.ts:93
|
label | |
Type : string
|
|
Defined in src/menu-button/menu-button.component.ts:99
|
menuAlignment | |
Type : MenuButtonPlacement
|
|
Default value : "bottom"
|
|
Defined in src/menu-button/menu-button.component.ts:95
|
menuId | |
Type : string
|
|
Default value : `menu-button-${MenuButtonComponent.menuButtonCounter++}`
|
|
Defined in src/menu-button/menu-button.component.ts:77
|
open | |
Type : boolean
|
|
Default value : false
|
|
Defined in src/menu-button/menu-button.component.ts:98
|
size | |
Type : "sm" | "md" | "lg"
|
|
Default value : "lg"
|
|
Defined in src/menu-button/menu-button.component.ts:94
|
class.cds--menu-button__container |
Type : boolean
|
Default value : true
|
Defined in src/menu-button/menu-button.component.ts:91
|
cleanUp |
cleanUp()
|
Defined in src/menu-button/menu-button.component.ts:180
|
Clean up
Returns :
void
|
handleFocusOut | ||||
handleFocusOut(event)
|
||||
Defined in src/menu-button/menu-button.component.ts:169
|
||||
On body click, close the menu
Parameters :
Returns :
void
|
handleMenuItemClick | ||||||
handleMenuItemClick(event: ItemClickEvent)
|
||||||
Defined in src/menu-button/menu-button.component.ts:156
|
||||||
As of now, menu button does not support nexted menu, on button click it should close
Parameters :
Returns :
void
|
ngAfterViewInit |
ngAfterViewInit()
|
Defined in src/menu-button/menu-button.component.ts:135
|
If user has passed in true for open, we dynamically open the menu
Returns :
void
|
ngOnChanges | ||||||
ngOnChanges(changes: SimpleChanges)
|
||||||
Defined in src/menu-button/menu-button.component.ts:124
|
||||||
In case user updates alignment, store initial value. This allows us to test user passed alignment on each open
Parameters :
Returns :
void
|
ngOnDestroy |
ngOnDestroy()
|
Defined in src/menu-button/menu-button.component.ts:147
|
Clean up Floating-ui & subscriptions
Returns :
void
|
recomputePosition |
recomputePosition()
|
Defined in src/menu-button/menu-button.component.ts:238
|
Compute position of menu
Returns :
void
|
roundByDPR | ||||
roundByDPR(value)
|
||||
Defined in src/menu-button/menu-button.component.ts:228
|
||||
Parameters :
Returns :
number
|
toggleMenu |
toggleMenu()
|
Defined in src/menu-button/menu-button.component.ts:197
|
Handles emitting open/close event
Returns :
void
|
Private _alignment |
Type : MenuButtonPlacement
|
Default value : "bottom"
|
Defined in src/menu-button/menu-button.component.ts:108
|
containerClass |
Default value : true
|
Decorators :
@HostBinding('class.cds--menu-button__container')
|
Defined in src/menu-button/menu-button.component.ts:91
|
Protected documentClick |
Default value : this.handleFocusOut.bind(this)
|
Defined in src/menu-button/menu-button.component.ts:104
|
Static menuButtonCounter |
Type : number
|
Default value : 0
|
Defined in src/menu-button/menu-button.component.ts:76
|
Private menuRef |
Type : HTMLElement
|
Defined in src/menu-button/menu-button.component.ts:109
|
menuTemplate |
Type : TemplateRef<any>
|
Decorators :
@ViewChild('menuTemplate')
|
Defined in src/menu-button/menu-button.component.ts:102
|
referenceElement |
Type : ElementRef<HTMLButtonElement>
|
Decorators :
@ViewChild('reference', {static: true})
|
Defined in src/menu-button/menu-button.component.ts:101
|
Private subscriptions |
Type : Subscription[]
|
Default value : []
|
Defined in src/menu-button/menu-button.component.ts:107
|
Protected unmountFloatingElement |
Type : Function
|
Defined in src/menu-button/menu-button.component.ts:105
|
projectedMenuItems | ||||||
setprojectedMenuItems(itemList: QueryList<ContextMenuItemComponent>)
|
||||||
Defined in src/menu-button/menu-button.component.ts:80
|
||||||
Parameters :
Returns :
void
|
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChildren,
ElementRef,
forwardRef,
HostBinding,
Input,
NgZone,
OnChanges,
OnDestroy,
QueryList,
Renderer2,
SimpleChanges,
TemplateRef,
ViewChild,
ViewContainerRef
} from "@angular/core";
import {
autoUpdate,
computePosition,
flip
} from "@floating-ui/dom";
import { ContextMenuItemComponent, ItemClickEvent } from "carbon-components-angular/context-menu";
import { Subscription } from "rxjs";
type MenuButtonPlacement = "top" | "top-start" | "top-end" | "bottom" | "bottom-start" | "bottom-end";
@Component({
selector: "cds-menu-button",
template: `
<button
#reference
class="cds--menu-button__trigger"
[ngClass]="{'cds--menu-button__trigger--open': open}"
[cdsButton]="kind"
[size]="size"
[attr.tabindex]="buttonTabIndex"
[disabled]="disabled"
type="button"
[attr.aria-haspopup]="true"
[attr.aria-expanded]="open"
[attr.aria-controls]="open ? menuId : undefined"
(click)="toggleMenu()">
{{label}}
<svg
cdsIcon="chevron--down"
size="16"
class="cds--btn__icon">
</svg>
</button>
<ng-template #menuTemplate>
<cds-menu
mode="basic"
[size]="size"
[open]="open"
[attr.id]="menuId"
[ngClass]="{
'cds--menu-button__bottom': this.menuAlignment === 'bottom',
'cds--menu-button__bottom-start': this.menuAlignment === 'bottom-start',
'cds--menu-button__bottom-end': this.menuAlignment === 'bottom-end',
'cds--menu-top': this.menuAlignment === 'top',
'cds--menu-top-start': this.menuAlignment === 'top-start',
'cds--menu-top-end': this.menuAlignment === 'top-end'
}">
<ng-content select="cds-menu-item, cds-menu-divider"></ng-content>
</cds-menu>
</ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MenuButtonComponent implements OnChanges, AfterViewInit, OnDestroy {
static menuButtonCounter = 0;
@Input() menuId = `menu-button-${MenuButtonComponent.menuButtonCounter++}`;
// Listen for click & determine if menu should close
@ContentChildren(ContextMenuItemComponent) set projectedMenuItems(itemList: QueryList<ContextMenuItemComponent>) {
// Reset in case user dynamically updates menu item
this.subscriptions.forEach((sub) => sub?.unsubscribe());
this.subscriptions = [];
itemList.forEach((item) => {
this.subscriptions.push(
item.itemClick.subscribe((clickEvent) => this.handleMenuItemClick(clickEvent))
);
});
}
@HostBinding("class.cds--menu-button__container") containerClass = true;
@Input() kind: "primary" | "tertiary" | "ghost" = "primary";
@Input() size: "sm" | "md" | "lg" = "lg";
@Input() menuAlignment: MenuButtonPlacement = "bottom";
@Input() buttonTabIndex: "0" | "1" | "-1" | string = "0";
@Input() disabled = false;
@Input() open = false;
@Input() label: string;
@ViewChild("reference", { static: true }) referenceElement: ElementRef<HTMLButtonElement>;
@ViewChild("menuTemplate") menuTemplate: TemplateRef<any>;
protected documentClick = this.handleFocusOut.bind(this);
protected unmountFloatingElement: Function;
private subscriptions: Subscription[] = [];
private _alignment: MenuButtonPlacement = "bottom";
private menuRef: HTMLElement;
constructor(
protected ngZone: NgZone,
protected renderer: Renderer2,
protected hostElement: ElementRef,
protected viewContainerRef: ViewContainerRef,
protected changeDetectorRef: ChangeDetectorRef
) { }
/**
* In case user updates alignment, store initial value.
* This allows us to test user passed alignment on each open
*/
ngOnChanges(changes: SimpleChanges): void {
if (changes.menuAlignment) {
this._alignment = changes.menuAlignment.currentValue;
}
}
/**
* If user has passed in true for open, we dynamically open the menu
*/
ngAfterViewInit(): void {
if (this.open) {
this.open = !this.open;
this.toggleMenu();
}
}
/**
* Clean up Floating-ui & subscriptions
*/
ngOnDestroy(): void {
this.cleanUp();
this.subscriptions.forEach((sub) => sub.unsubscribe());
}
/**
* As of now, menu button does not support nexted menu, on button click it should close
*/
handleMenuItemClick(event: ItemClickEvent) {
// If event is not type radio/checkbox, we close the menu
if (!event.type) {
this.toggleMenu();
}
}
/**
* On body click, close the menu
* @param event
*/
handleFocusOut(event) {
if (!this.hostElement.nativeElement.contains(event.target)) {
this.toggleMenu();
}
}
/**
* Clean up `autoUpdate` if auto alignment is enabled
*/
cleanUp() {
document.removeEventListener("click", this.documentClick);
if (this.unmountFloatingElement) {
this.menuRef.remove();
this.viewContainerRef.clear();
this.unmountFloatingElement();
}
this.unmountFloatingElement = undefined;
// On all instances of menu closing, make sure icon direction is correct
this.changeDetectorRef.markForCheck();
}
/**
* Handles emitting open/close event
*/
toggleMenu() {
this.open = !this.open;
if (this.open) {
// Render the template & append to view
const view = this.viewContainerRef.createEmbeddedView(this.menuTemplate);
this.menuRef = document.body.appendChild(view.rootNodes[0] as HTMLElement);
// Assign button width to the menu ref to align with button
Object.assign(this.menuRef.style, {
width: `${this.referenceElement.nativeElement.clientWidth}px`,
top: "0",
left: "0"
});
// Reset & test alignment every open
this.menuAlignment = this._alignment;
document.addEventListener("click", this.documentClick);
// Listen for events such as scrolling to keep menu aligned
this.unmountFloatingElement = autoUpdate(
this.referenceElement.nativeElement,
this.menuRef,
this.recomputePosition.bind(this)
);
} else {
this.cleanUp();
}
}
roundByDPR(value) {
const dpr = window.devicePixelRatio || 1;
return Math.round(value * dpr) / dpr;
}
/**
* Compute position of menu
*/
recomputePosition() {
if (this.menuTemplate && this.referenceElement) {
// Run outside of angular zone to avoid unnecessary change detection and rely on floating-ui
this.ngZone.runOutsideAngular(async () => {
const { x, y, placement } = await computePosition(
this.referenceElement.nativeElement,
this.menuRef,
{
placement: this.menuAlignment,
strategy: "fixed",
middleware: [
flip({ crossAxis: false })
]
});
this.menuAlignment = placement as MenuButtonPlacement;
// Using CSSOM to manipulate CSS to avoid content security policy inline-src
// https://github.com/w3c/webappsec-csp/issues/212
Object.assign(this.menuRef.style, {
position: "fixed",
// Using transform instead of top/left position to improve performance
transform: `translate(${this.roundByDPR(x)}px,${this.roundByDPR(y)}px)`
});
this.changeDetectorRef.markForCheck();
});
}
}
}