UNPKG

carbon-components-angular

Version:
1,178 lines (1,168 loc) 58.6 kB
import * as i0 from '@angular/core'; import { Injectable, EventEmitter, Component, Optional, Output, Input, ViewChild, HostListener, Directive, HostBinding, ViewEncapsulation, ContentChild, NgModule } from '@angular/core'; import { tabbableSelector, getFocusElementList, cycleTabs, isFocusInFirstItem, isFocusInLastItem } from 'carbon-components-angular/common'; import * as i1 from 'carbon-components-angular/placeholder'; import { PlaceholderModule } from 'carbon-components-angular/placeholder'; import { Subscription } from 'rxjs'; import Position, { position } from '@carbon/utils-position'; import * as i2 from 'carbon-components-angular/utils'; import { closestAttr, UtilsModule } from 'carbon-components-angular/utils'; import * as i1$1 from 'carbon-components-angular/i18n'; import { I18nModule } from 'carbon-components-angular/i18n'; import * as i2$1 from 'carbon-components-angular/experimental'; import { ExperimentalModule } from 'carbon-components-angular/experimental'; import * as i2$2 from '@angular/common'; import { CommonModule } from '@angular/common'; import * as i3 from 'carbon-components-angular/icon'; import { IconModule } from 'carbon-components-angular/icon'; /** * An enum of the various reasons a dialog may close. For use with `CloseMeta` and `shouldClose` * * It's expected that `interaction` will be a common closure reason. */ var CloseReasons; (function (CloseReasons) { /** * For when the component is closed by being destroyed */ CloseReasons[CloseReasons["destroyed"] = 0] = "destroyed"; /** * For use in cases where the dialog closes for programmatic reasons other than destruction */ CloseReasons[CloseReasons["programmatic"] = 1] = "programmatic"; /** * interaction reasons will also provide a target for the interaction */ CloseReasons[CloseReasons["interaction"] = 2] = "interaction"; /** * For use in cases where the dialog closes due to being hidden */ CloseReasons[CloseReasons["hidden"] = 3] = "hidden"; })(CloseReasons || (CloseReasons = {})); /** * `Dialog` object to be injected into other components. */ class DialogService { /** * Creates an instance of `DialogService`. */ constructor(injector, placeholderService) { this.injector = injector; this.placeholderService = placeholderService; } /** * Closes all known `Dialog`s. Does not focus any previous elements, since we can't know which would be correct */ static closeAll() { DialogService.dialogRefs.forEach(ref => ref.instance.doClose({ reason: CloseReasons.programmatic })); DialogService.dialogRefs.clear(); } /** * If `dialogRef` is defined, the Dialog is already open. If * `dialogRef` is undefined, we create the `Dialog` component and reference to it. * A subscription is created to track if the `Dialog` should close. * * @param viewContainer a `ViewContainerRef` to instantiate the component against. * May be `null` if an `cds-placeholder` exists and `dialogConfig.appendInline` is false * @param dialogConfig the `DialogConfig` for the component */ open(viewContainer, dialogConfig, component) { if (!component) { return; } let dialogRef; if (dialogConfig.appendInline) { // add our component to the view dialogRef = viewContainer.createComponent(component, { index: 0, injector: this.injector }); } else if (!this.placeholderService.hasPlaceholderRef()) { dialogRef = viewContainer.createComponent(component, { index: 0, injector: this.injector }); if (dialogRef) { setTimeout(() => { window.document.querySelector("body").appendChild(dialogRef.location.nativeElement); }); } } else { dialogRef = this.placeholderService.createComponent(component, this.injector); } // keep track of all initialized dialogs DialogService.dialogRefs.add(dialogRef); // initialize some extra options dialogConfig["previouslyFocusedElement"] = document.activeElement; dialogRef.instance.dialogConfig = dialogConfig; dialogRef.instance.elementRef.nativeElement.focus(); return dialogRef; } /** * On close of `Dialog` item, sets focus back to previous item, unsets * the current `dialogRef` item. Unsubscribes to the event of `Dialog` close. * * @param dialogRef the dialogRef to close */ close(dialogRef) { // to handle the case where we have a null `this.dialogRef` if (!dialogRef) { return; } const elementToFocus = dialogRef.instance.dialogConfig["previouslyFocusedElement"]; dialogRef.destroy(); // update the globally tracked dialogRefs if (DialogService.dialogRefs.has(dialogRef)) { DialogService.dialogRefs.delete(dialogRef); } // Keeps the focus on the dialog trigger if there are no focusable elements. Change focus to previously focused element // if there are focusable elements in the dialog. if (!dialogRef.location.nativeElement.querySelectorAll(tabbableSelector)) { elementToFocus.focus(); } } /** * Fix for safari hijacking clicks. * * Runs on `ngOnInit` of every dialog. Ensures we don't have multiple listeners * because having many of them could degrade performance in certain cases (and is * not necessary for our use case) * * This is an internally used function, can change at any point (even get removed) * and changes to it won't be considered a breaking change. Use at your own risk. */ singletonClickListen() { if (!DialogService.listeningForBodyClicks) { document.body.firstElementChild.addEventListener("click", () => null, true); DialogService.listeningForBodyClicks = true; } } } /** * Used in `singletonClickListen`, don't count on its existence and values. */ DialogService.listeningForBodyClicks = false; /** * A set of all known dialog components */ DialogService.dialogRefs = new Set(); DialogService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: DialogService, deps: [{ token: i0.Injector }, { token: i1.PlaceholderService }], target: i0.ɵɵFactoryTarget.Injectable }); DialogService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: DialogService }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: DialogService, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: i0.Injector }, { type: i1.PlaceholderService }]; } }); /** * Implements a `Dialog` that can be positioned anywhere on the page. * Used to implement a popover or tooltip. */ class Dialog { /** * Creates an instance of `Dialog`. * @param elementRef * @param elementService */ constructor(elementRef, elementService, animationFrameService = null) { this.elementRef = elementRef; this.elementService = elementService; this.animationFrameService = animationFrameService; /** * Emits event that handles the closing of a `Dialog` object. */ this.close = new EventEmitter(); /** * Stores the data received from `dialogConfig`. */ this.data = {}; this.visibilitySubscription = new Subscription(); this.animationFrameSubscription = new Subscription(); /** * Handles offsetting the `Dialog` item based on the defined position * to not obscure the content beneath. */ this.addGap = { "left": pos => position.addOffset(pos, 0, -this.dialogConfig.gap), "right": pos => position.addOffset(pos, 0, this.dialogConfig.gap), "top": pos => position.addOffset(pos, -this.dialogConfig.gap), "bottom": pos => position.addOffset(pos, this.dialogConfig.gap), "left-bottom": pos => position.addOffset(pos, 0, -this.dialogConfig.gap), "right-bottom": pos => position.addOffset(pos, 0, this.dialogConfig.gap) }; /** * Extra placements. Child classes can add to this for use in `placeDialog`. */ this.placements = {}; } /** * Initialize the `Dialog`, set the placement and gap, and add a `Subscription` to resize events. */ ngOnInit() { this.placement = this.dialogConfig.placement.split(",")[0]; this.data = this.dialogConfig.data; // run any additional initialization code that consuming classes may have this.onDialogInit(); } /** * After the DOM is ready, focus is set and dialog is placed * in respect to the parent element. */ ngAfterViewInit() { const dialogElement = this.dialog.nativeElement; // split the wrapper class list and apply separately to avoid IE // 1. throwing an error due to assigning a readonly property (classList) // 2. throwing a SyntaxError due to passing an empty string to `add` if (this.dialogConfig.wrapperClass) { for (const extraClass of this.dialogConfig.wrapperClass.split(" ")) { dialogElement.classList.add(extraClass); } } // only focus the dialog if there are focusable elements within the dialog if (getFocusElementList(this.dialog.nativeElement).length > 0) { dialogElement.focus(); } const parentElement = this.dialogConfig.parentRef.nativeElement; if (this.animationFrameService) { this.animationFrameSubscription = this.animationFrameService.tick.subscribe(() => { this.placeDialog(); }); } if (this.dialogConfig.closeWhenHidden) { this.visibilitySubscription = this.elementService .visibility(parentElement, parentElement) .subscribe(value => { this.placeDialog(); if (!value.visible) { this.doClose({ reason: CloseReasons.hidden }); } }); } this.placeDialog(); // run afterDialogViewInit on the next tick setTimeout(() => this.afterDialogViewInit()); } /** * Empty method to be overridden by consuming classes to run any additional initialization code. */ onDialogInit() { } /** * Empty method to be overridden by consuming classes to run any additional initialization code after the view is available. * NOTE: this does _not_ guarantee the dialog will be positioned, simply that it will exist in the DOM */ afterDialogViewInit() { } /** * Uses the position service to position the `Dialog` in screen space */ placeDialog() { const positionService = new Position(this.placements); // helper to find the position based on the current/given environment const findPosition = (reference, target, placement) => { let pos; if (this.dialogConfig.appendInline) { pos = this.addGap[placement](positionService.findRelative(reference, target, placement)); } else { pos = this.addGap[placement](positionService.findAbsolute(reference, target, placement)); } if (this.dialogConfig.offset) { // Apply vertical and horizontal offsets given through the dialogConfig pos.top = pos.top + this.dialogConfig.offset.y; pos.left = pos.left + this.dialogConfig.offset.x; } return pos; }; let parentEl = this.dialogConfig.parentRef.nativeElement; let el = this.dialog.nativeElement; let dialogPlacement = this.placement; // split always returns an array, so we can just use the auto position logic // for single positions too const placements = this.dialogConfig.placement.split(","); // find the best placement dialogPlacement = positionService.findBestPlacement(parentEl, el, placements); // calculate the final position const pos = findPosition(parentEl, el, dialogPlacement); // update the element positionService.setElement(el, pos); setTimeout(() => { this.placement = dialogPlacement; }); } /** * Sets up a KeyboardEvent to close `Dialog` with Escape key. * @param event */ escapeClose(event) { switch (event.key) { case "Escape": { event.stopImmediatePropagation(); this.doClose({ reason: CloseReasons.interaction, target: event.target }); break; } case "Tab": { cycleTabs(event, this.elementRef.nativeElement); break; } } } /** * Sets up a event Listener to close `Dialog` if click event occurs outside * `Dialog` object. * @param event */ clickClose(event) { if (!this.elementRef.nativeElement.contains(event.target) && !this.dialogConfig.parentRef.nativeElement.contains(event.target)) { this.doClose({ reason: CloseReasons.interaction, target: event.target }); } } /** * Closes `Dialog` object by emitting the close event upwards to parents. */ doClose(meta = { reason: CloseReasons.interaction }) { this.close.emit(meta); } /** * At destruction of component, `Dialog` unsubscribes from all the subscriptions. */ ngOnDestroy() { this.visibilitySubscription.unsubscribe(); if (this.animationFrameSubscription) { this.animationFrameSubscription.unsubscribe(); } } } Dialog.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: Dialog, deps: [{ token: i0.ElementRef }, { token: i2.ElementService }, { token: i2.AnimationFrameService, optional: true }], target: i0.ɵɵFactoryTarget.Component }); Dialog.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: Dialog, selector: "cds-dialog, ibm-dialog", inputs: { dialogConfig: "dialogConfig" }, outputs: { close: "close" }, host: { listeners: { "keydown": "escapeClose($event)", "document:click": "clickClose($event)" } }, viewQueries: [{ propertyName: "dialog", first: true, predicate: ["dialog"], descendants: true }], ngImport: i0, template: "", isInline: true }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: Dialog, decorators: [{ type: Component, args: [{ selector: "cds-dialog, ibm-dialog", template: "" }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i2.ElementService }, { type: i2.AnimationFrameService, decorators: [{ type: Optional }] }]; }, propDecorators: { close: [{ type: Output }], dialogConfig: [{ type: Input }], dialog: [{ type: ViewChild, args: ["dialog"] }], escapeClose: [{ type: HostListener, args: ["keydown", ["$event"]] }], clickClose: [{ type: HostListener, args: ["document:click", ["$event"]] }] } }); /** * A generic directive that can be inherited from to create dialogs (for example, a tooltip or popover) * * This class contains the relevant initialization code, specific templates, options, and additional inputs * should be specified in the derived class. * * NOTE: All child classes should add `DialogService` as a provider, otherwise they will lose context that * the service relies on. */ class DialogDirective { /** * Creates an instance of DialogDirective. * @param elementRef * @param viewContainerRef * @param dialogService * @param eventService */ constructor(elementRef, viewContainerRef, dialogService, eventService) { this.elementRef = elementRef; this.viewContainerRef = viewContainerRef; this.dialogService = dialogService; this.eventService = eventService; /** * Title for the dialog */ this.title = ""; /** * Defines how the Dialog is triggered.(Hover and click behave the same on mobile - both respond to a single tap). * Do not add focusable elements if trigger is `hover` or `mouseenter`. */ this.trigger = "click"; /** * Defines how the Dialog close event is triggered. * * [See here](https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseleave_event) * for more on the difference between `mouseleave` and `mouseout`. * * Defaults to `click` when `trigger` is set to `click`. */ this.closeTrigger = "mouseleave"; /** * Placement of the dialog, usually relative to the element the directive is on. */ this.placement = "left"; /** * Spacing between the dialog and it's triggering element */ this.gap = 0; /** * Set to `true` to open the dialog next to the triggering component */ this.appendInline = false; /** * Optional data for templates */ this.data = {}; this.isOpen = false; /** * This prevents the dialog from being toggled */ this.disabled = false; /** * Emits an event when the dialog is closed */ this.onClose = new EventEmitter(); /** * Emits an event when the dialog is opened */ this.onOpen = new EventEmitter(); /** * Emits an event when the state of `isOpen` changes. Allows `isOpen` to be double bound */ this.isOpenChange = new EventEmitter(); this.role = "button"; this.hasPopup = true; } /** * @deprecated as of v5, use `cdsDialog` instead * Dialog body content. */ set ibmDialog(body) { this.cdsDialog = body; } get ariaOwns() { return this.isOpen ? this.dialogConfig.compID : null; } ngOnChanges(changes) { // set the config object (this can [and should!] be added to in child classes depending on what they need) this.dialogConfig = { title: this.title, content: this.cdsDialog, placement: this.placement, parentRef: this.elementRef, gap: this.gap, trigger: this.trigger, closeTrigger: this.closeTrigger, shouldClose: this.shouldClose || (() => true), appendInline: this.appendInline, wrapperClass: this.wrapperClass, data: this.data, offset: this.offset, disabled: this.disabled }; if (changes.isOpen) { if (changes.isOpen.currentValue) { this.open(); } else if (!changes.isOpen.firstChange) { this.close({ reason: CloseReasons.programmatic }); } } // Run any code a child class may need. this.onDialogChanges(changes); this.updateConfig(); } /** * Sets the config object and binds events for hovering or clicking before * running code from child class. */ ngOnInit() { // fix for safari hijacking clicks this.dialogService.singletonClickListen(); const element = this.elementRef.nativeElement; this.eventService.on(element, "keydown", (event) => { if (event.target === this.dialogConfig.parentRef.nativeElement && (event.key === "Tab" || event.key === "Tab" && event.shiftKey) || event.key === "Escape") { this.close({ reason: CloseReasons.interaction, target: event.target }); } }); // bind events for hovering or clicking the host if (this.trigger === "hover" || this.trigger === "mouseenter") { this.eventService.on(element, "mouseenter", this.open.bind(this)); this.eventService.on(element, this.closeTrigger, (event) => { this.close({ reason: CloseReasons.interaction, target: event.target }); }); this.eventService.on(element, "focus", this.open.bind(this)); this.eventService.on(element, "blur", (event) => { this.close({ reason: CloseReasons.interaction, target: event.target }); }); } else { this.eventService.on(element, "click", (event) => { this.toggle({ reason: CloseReasons.interaction, target: event.target }); }); this.eventService.on(element, "keydown", (event) => { if (event.key === "Enter" || event.key === " ") { setTimeout(() => { this.open(); }); } }); } DialogDirective.dialogCounter++; this.dialogConfig.compID = "dialog-" + DialogDirective.dialogCounter; // run any code a child class may need this.onDialogInit(); this.updateConfig(); } /** * When the host dies, kill the popover. * - Useful for use in a modal or similar. */ ngOnDestroy() { this.close({ reason: CloseReasons.destroyed }); } /** * Helper method to call dialogService 'open'. * - Enforce accessibility by updating an aria attr for nativeElement. */ open(component) { // don't allow dialogs to be opened if they're already open if (this.dialogRef || this.disabled) { return; } // actually open the dialog, emit events, and set the open state this.dialogRef = this.dialogService.open(this.viewContainerRef, this.dialogConfig, component); this.isOpen = true; this.onOpen.emit(); this.isOpenChange.emit(true); // Handles emitting all the close events to clean everything up // Also enforce accessibility on close by updating an aria attr on the nativeElement. this.dialogRef.instance.close.subscribe((meta) => { if (!this.dialogRef) { return; } if (this.dialogConfig.shouldClose && this.dialogConfig.shouldClose(meta)) { // close the dialog, emit events, and clear out the open states this.dialogService.close(this.dialogRef); this.dialogRef = null; this.isOpen = false; this.onClose.emit(); this.isOpenChange.emit(false); } }); return this.dialogRef; } /** * Helper method to toggle the open state of the dialog */ toggle(meta = { reason: CloseReasons.interaction }) { if (!this.isOpen) { this.open(); } else { this.close(meta); } } /** * Helper method to close the dialogRef. */ close(meta = { reason: CloseReasons.interaction }) { if (this.dialogRef) { this.dialogRef.instance.doClose(meta); } } /** * Empty method for child classes to override and specify additional init steps. * Run after DialogDirective completes it's ngOnInit. */ onDialogInit() { } /** * Empty method for child to override and specify additional on changes steps. * run after DialogDirective completes it's ngOnChanges. */ onDialogChanges(_changes) { } updateConfig() { } } DialogDirective.dialogCounter = 0; DialogDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: DialogDirective, deps: [{ token: i0.ElementRef }, { token: i0.ViewContainerRef }, { token: DialogService }, { token: i2.EventService }], target: i0.ɵɵFactoryTarget.Directive }); DialogDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.3.0", type: DialogDirective, selector: "[cdsDialog], [ibmDialog]", inputs: { title: "title", ibmDialog: "ibmDialog", cdsDialog: "cdsDialog", trigger: "trigger", closeTrigger: "closeTrigger", placement: "placement", offset: "offset", wrapperClass: "wrapperClass", gap: "gap", appendInline: "appendInline", data: "data", isOpen: "isOpen", disabled: "disabled", shouldClose: "shouldClose" }, outputs: { onClose: "onClose", onOpen: "onOpen", isOpenChange: "isOpenChange" }, host: { properties: { "attr.aria-expanded": "this.isOpen", "attr.role": "this.role", "attr.aria-haspopup": "this.hasPopup", "attr.aria-owns": "this.ariaOwns" } }, providers: [ DialogService ], exportAs: ["dialog"], usesOnChanges: true, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: DialogDirective, decorators: [{ type: Directive, args: [{ selector: "[cdsDialog], [ibmDialog]", exportAs: "dialog", providers: [ DialogService ] }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.ViewContainerRef }, { type: DialogService }, { type: i2.EventService }]; }, propDecorators: { title: [{ type: Input }], ibmDialog: [{ type: Input }], cdsDialog: [{ type: Input }], trigger: [{ type: Input }], closeTrigger: [{ type: Input }], placement: [{ type: Input }], offset: [{ type: Input }], wrapperClass: [{ type: Input }], gap: [{ type: Input }], appendInline: [{ type: Input }], data: [{ type: Input }], isOpen: [{ type: Input }, { type: HostBinding, args: ["attr.aria-expanded"] }], disabled: [{ type: Input }], shouldClose: [{ type: Input }], onClose: [{ type: Output }], onOpen: [{ type: Output }], isOpenChange: [{ type: Output }], role: [{ type: HostBinding, args: ["attr.role"] }], hasPopup: [{ type: HostBinding, args: ["attr.aria-haspopup"] }], ariaOwns: [{ type: HostBinding, args: ["attr.aria-owns"] }] } }); /** * Extend the `Dialog` component to create an overflow menu. * * Not used directly. See overflow-menu.component and overflow-menu.directive for more */ class OverflowMenuPane extends Dialog { constructor(elementRef, i18n, experimental, animationFrameService = null, // mark `elementService` as optional since making it mandatory would be a breaking change elementService = null) { super(elementRef, elementService, animationFrameService); this.elementRef = elementRef; this.i18n = i18n; this.experimental = experimental; this.animationFrameService = animationFrameService; this.elementService = elementService; } onDialogInit() { const positionOverflowMenu = pos => { let offset; /* * 20 is half the width of the overflow menu trigger element. * we also move the element by half of it's own width, since * position service will try and center everything */ const closestRel = closestAttr("position", ["relative", "fixed", "absolute"], this.elementRef.nativeElement); const topFix = closestRel ? closestRel.getBoundingClientRect().top * -1 : 0; const leftFix = closestRel ? closestRel.getBoundingClientRect().left * -1 : 0; offset = Math.round(this.dialog.nativeElement.offsetWidth / 2) - 20; if (this.dialogConfig.flip) { return position.addOffset(pos, topFix, (-offset + leftFix)); } return position.addOffset(pos, topFix, (offset + leftFix)); }; this.addGap["bottom"] = positionOverflowMenu; this.addGap["top"] = positionOverflowMenu; if (!this.dialogConfig.menuLabel) { this.dialogConfig.menuLabel = this.i18n.get().OVERFLOW_MENU.OVERFLOW; } } hostkeys(event) { const listItems = this.listItems(); switch (event.key) { case "ArrowDown": event.preventDefault(); if (!isFocusInLastItem(event, listItems)) { const index = listItems.findIndex(item => item === event.target); listItems[index + 1].focus(); } else { listItems[0].focus(); } break; case "ArrowUp": event.preventDefault(); if (!isFocusInFirstItem(event, listItems)) { const index = listItems.findIndex(item => item === event.target); listItems[index - 1].focus(); } else { listItems[listItems.length - 1].focus(); } break; case "Home": event.preventDefault(); listItems[0].focus(); break; case "End": event.preventDefault(); listItems[listItems.length - 1].focus(); break; case "Escape": case "Tab": event.stopImmediatePropagation(); this.doClose({ reason: CloseReasons.interaction, target: event.target }); break; default: break; } } onClose(event) { this.doClose({ reason: CloseReasons.interaction, target: event.target }); } afterDialogViewInit() { const focusElementList = this.listItems(); focusElementList.forEach(button => { // Allows user to set tabindex to 0. if (button.getAttribute("tabindex") === null) { button.tabIndex = -1; } }); if (focusElementList[0]) { focusElementList[0].tabIndex = 0; focusElementList[0].focus(); } } listItems() { const selector = ".cds--overflow-menu-options__option:not([disabled]) .cds--overflow-menu-options__btn"; return Array.from(this.elementRef.nativeElement.querySelectorAll(selector)); } } OverflowMenuPane.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: OverflowMenuPane, deps: [{ token: i0.ElementRef }, { token: i1$1.I18n }, { token: i2$1.ExperimentalService }, { token: i2.AnimationFrameService, optional: true }, { token: i2.ElementService, optional: true }], target: i0.ɵɵFactoryTarget.Component }); OverflowMenuPane.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: OverflowMenuPane, selector: "cds-overflow-menu-pane, ibm-overflow-menu-pane", host: { listeners: { "keydown": "hostkeys($event)" } }, usesInheritance: true, ngImport: i0, template: ` <ul [attr.id]="dialogConfig.compID" [attr.aria-label]="dialogConfig.menuLabel" [attr.data-floating-menu-direction]="placement ? placement : null" [ngClass]="{'cds--overflow-menu--flip': dialogConfig.flip}" role="menu" #dialog class="cds--overflow-menu-options cds--overflow-menu-options--open" (click)="onClose($event)" [attr.aria-label]="dialogConfig.menuLabel"> <ng-template [ngTemplateOutlet]="dialogConfig.content" [ngTemplateOutletContext]="{overflowMenu: this}"> </ng-template> </ul> `, isInline: true, dependencies: [{ kind: "directive", type: i2$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: OverflowMenuPane, decorators: [{ type: Component, args: [{ selector: "cds-overflow-menu-pane, ibm-overflow-menu-pane", template: ` <ul [attr.id]="dialogConfig.compID" [attr.aria-label]="dialogConfig.menuLabel" [attr.data-floating-menu-direction]="placement ? placement : null" [ngClass]="{'cds--overflow-menu--flip': dialogConfig.flip}" role="menu" #dialog class="cds--overflow-menu-options cds--overflow-menu-options--open" (click)="onClose($event)" [attr.aria-label]="dialogConfig.menuLabel"> <ng-template [ngTemplateOutlet]="dialogConfig.content" [ngTemplateOutletContext]="{overflowMenu: this}"> </ng-template> </ul> ` }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i1$1.I18n }, { type: i2$1.ExperimentalService }, { type: i2.AnimationFrameService, decorators: [{ type: Optional }] }, { type: i2.ElementService, decorators: [{ type: Optional }] }]; }, propDecorators: { hostkeys: [{ type: HostListener, args: ["keydown", ["$event"]] }] } }); /** * @deprecated as of v5 * Use Toggletip or Popover components instead */ class OverflowMenuCustomPane extends Dialog { constructor(elementRef, i18n, animationFrameService = null, // mark `elementService` as optional since making it mandatory would be a breaking change elementService = null) { super(elementRef, elementService, animationFrameService); this.elementRef = elementRef; this.i18n = i18n; this.animationFrameService = animationFrameService; this.elementService = elementService; } onClick(event) { this.doClose({ reason: CloseReasons.interaction, target: event.target }); } onDialogInit() { const positionOverflowMenu = pos => { let offset; /* * 20 is half the width of the overflow menu trigger element. * we also move the element by half of it's own width, since * position service will try and center everything */ const closestRel = closestAttr("position", ["relative", "fixed", "absolute"], this.elementRef.nativeElement); const topFix = closestRel ? closestRel.getBoundingClientRect().top * -1 : 0; const leftFix = closestRel ? closestRel.getBoundingClientRect().left * -1 : 0; offset = Math.round(this.dialog.nativeElement.offsetWidth / 2) - 20; if (this.dialogConfig.flip) { return position.addOffset(pos, topFix, (-offset + leftFix)); } return position.addOffset(pos, topFix, (offset + leftFix)); }; this.addGap["bottom"] = positionOverflowMenu; this.addGap["top"] = positionOverflowMenu; if (!this.dialogConfig.menuLabel) { this.dialogConfig.menuLabel = this.i18n.get().OVERFLOW_MENU.OVERFLOW; } } } OverflowMenuCustomPane.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: OverflowMenuCustomPane, deps: [{ token: i0.ElementRef }, { token: i1$1.I18n }, { token: i2.AnimationFrameService, optional: true }, { token: i2.ElementService, optional: true }], target: i0.ɵɵFactoryTarget.Component }); OverflowMenuCustomPane.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: OverflowMenuCustomPane, selector: "cds-overflow-custom-menu-pane, ibm-overflow-custom-menu-pane", usesInheritance: true, ngImport: i0, template: ` <div [attr.id]="dialogConfig.compID" [attr.aria-label]="dialogConfig.menuLabel" [attr.data-floating-menu-direction]="placement ? placement : null" [ngClass]="{'cds--overflow-menu--flip': dialogConfig.flip}" class="cds--overflow-menu-options cds--overflow-menu-options--open" role="menu" (click)="onClick($event)" #dialog [attr.aria-label]="dialogConfig.menuLabel"> <ng-template [ngTemplateOutlet]="dialogConfig.content" [ngTemplateOutletContext]="{overflowMenu: this}"> </ng-template> </div> `, isInline: true, dependencies: [{ kind: "directive", type: i2$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: OverflowMenuCustomPane, decorators: [{ type: Component, args: [{ selector: "cds-overflow-custom-menu-pane, ibm-overflow-custom-menu-pane", template: ` <div [attr.id]="dialogConfig.compID" [attr.aria-label]="dialogConfig.menuLabel" [attr.data-floating-menu-direction]="placement ? placement : null" [ngClass]="{'cds--overflow-menu--flip': dialogConfig.flip}" class="cds--overflow-menu-options cds--overflow-menu-options--open" role="menu" (click)="onClick($event)" #dialog [attr.aria-label]="dialogConfig.menuLabel"> <ng-template [ngTemplateOutlet]="dialogConfig.content" [ngTemplateOutletContext]="{overflowMenu: this}"> </ng-template> </div> ` }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i1$1.I18n }, { type: i2.AnimationFrameService, decorators: [{ type: Optional }] }, { type: i2.ElementService, decorators: [{ type: Optional }] }]; } }); /** * Directive for extending `Dialog` to create overflow menus. * * class: OverflowMenuDirective (extends DialogDirective) * * * selector: `cdsOverflowMenu` * * * ```html * <div [cdsOverflowMenu]="templateRef"></div> * <ng-template #templateRef> * <!-- overflow menu options here --> * </ng-template> * ``` * * ```html * <div [cdsOverflowMenu]="templateRef" [customPane]="true"></div> * <ng-template #templateRef> * <!-- custom content goes here --> * </ng-template> * ``` */ class OverflowMenuDirective extends DialogDirective { /** * Creates an instance of `OverflowMenuDirective`. */ constructor(elementRef, viewContainerRef, dialogService, eventService) { super(elementRef, viewContainerRef, dialogService, eventService); this.elementRef = elementRef; this.viewContainerRef = viewContainerRef; this.dialogService = dialogService; this.eventService = eventService; /** * Controls wether the overflow menu is flipped */ this.flip = false; /** * Classes to add to the dialog container */ this.wrapperClass = ""; /** * Set to true to for custom content */ this.customPane = false; } /** * @deprecated as of v5 * Takes a template ref of `OverflowMenuOptions`s */ set ibmOverflowMenu(template) { this.cdsOverflowMenu = template; } updateConfig() { this.dialogConfig.content = this.cdsOverflowMenu; this.dialogConfig.flip = this.flip; this.dialogConfig.offset = this.offset; this.dialogConfig.wrapperClass = this.wrapperClass; } hostkeys(event) { switch (event.key) { case "Enter": case " ": event.preventDefault(); break; } } open() { return super.open(this.customPane ? OverflowMenuCustomPane : OverflowMenuPane); } } OverflowMenuDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: OverflowMenuDirective, deps: [{ token: i0.ElementRef }, { token: i0.ViewContainerRef }, { token: DialogService }, { token: i2.EventService }], target: i0.ɵɵFactoryTarget.Directive }); OverflowMenuDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.3.0", type: OverflowMenuDirective, selector: "[cdsOverflowMenu], [ibmOverflowMenu]", inputs: { ibmOverflowMenu: "ibmOverflowMenu", cdsOverflowMenu: "cdsOverflowMenu", flip: "flip", offset: "offset", wrapperClass: "wrapperClass", customPane: "customPane" }, host: { listeners: { "keydown": "hostkeys($event)" } }, providers: [ DialogService ], exportAs: ["overflowMenu"], usesInheritance: true, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: OverflowMenuDirective, decorators: [{ type: Directive, args: [{ selector: "[cdsOverflowMenu], [ibmOverflowMenu]", exportAs: "overflowMenu", providers: [ DialogService ] }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.ViewContainerRef }, { type: DialogService }, { type: i2.EventService }]; }, propDecorators: { ibmOverflowMenu: [{ type: Input }], cdsOverflowMenu: [{ type: Input }], flip: [{ type: Input }], offset: [{ type: Input }], wrapperClass: [{ type: Input }], customPane: [{ type: Input }], hostkeys: [{ type: HostListener, args: ["keydown", ["$event"]] }] } }); /** * The OverFlow menu component encapsulates the OverFlowMenu directive, and the menu iconography into one convienent component. * * Get started with importing the module: * * ```typescript * import { DialogModule } from 'carbon-components-angular'; * ``` * * ```html * <cds-overflow-menu> * <cds-overflow-menu-option>Option 1</cds-overflow-menu-option> * <cds-overflow-menu-option>Option 2</cds-overflow-menu-option> * </cds-overflow-menu> * ``` * * [See demo](../../?path=/story/components-overflow-menu--basic) */ class OverflowMenu { constructor(elementRef, i18n) { this.elementRef = elementRef; this.i18n = i18n; this.buttonLabel = this.i18n.get().OVERFLOW_MENU.OVERFLOW; this.flip = false; this.placement = "bottom"; this.open = false; this.openChange = new EventEmitter(); this.wrapperClass = ""; /** * This appends additional classes to the overflow trigger/button. */ this.triggerClass = ""; } handleOpenChange(event) { this.open = event; this.openChange.emit(event); } } OverflowMenu.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: OverflowMenu, deps: [{ token: i0.ElementRef }, { token: i1$1.I18n }], target: i0.ɵɵFactoryTarget.Component }); OverflowMenu.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: OverflowMenu, selector: "cds-overflow-menu, ibm-overflow-menu", inputs: { buttonLabel: "buttonLabel", flip: "flip", placement: "placement", open: "open", customTrigger: "customTrigger", offset: "offset", wrapperClass: "wrapperClass", triggerClass: "triggerClass" }, outputs: { openChange: "openChange" }, queries: [{ propertyName: "overflowMenuDirective", first: true, predicate: OverflowMenuDirective, descendants: true }], ngImport: i0, template: ` <button [cdsOverflowMenu]="options" [ngClass]="{'cds--overflow-menu--open': open}" class="cds--overflow-menu {{triggerClass}}" [attr.aria-label]="buttonLabel" [flip]="flip" [isOpen]="open" (isOpenChange)="handleOpenChange($event)" [offset]="offset" [wrapperClass]="wrapperClass" aria-haspopup="true" class="cds--overflow-menu" type="button" [placement]="placement"> <ng-template *ngIf="customTrigger; else defaultIcon" [ngTemplateOutlet]="customTrigger"></ng-template> </button> <ng-template #options> <ng-content></ng-content> </ng-template> <ng-template #defaultIcon> <svg cdsIcon="overflow-menu--vertical" size="16" class="cds--overflow-menu__icon"></svg> </ng-template> `, isInline: true, styles: [".cds--overflow-menu--open{opacity:1}.cds--data-table-v2 .cds--overflow-menu{transform:rotate(90deg)}.cds--data-table-v2 .cds--overflow-menu__icon{transform:rotate(180deg)}\n"], dependencies: [{ kind: "directive", type: i2$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i3.IconDirective, selector: "[cdsIcon], [ibmIcon]", inputs: ["ibmIcon", "cdsIcon", "size", "title", "ariaLabel", "ariaLabelledBy", "ariaHidden", "isFocusable"] }, { kind: "directive", type: OverflowMenuDirective, selector: "[cdsOverflowMenu], [ibmOverflowMenu]", inputs: ["ibmOverflowMenu", "cdsOverflowMenu", "flip", "offset", "wrapperClass", "customPane"], exportAs: ["overflowMenu"] }], encapsulation: i0.ViewEncapsulation.None }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: OverflowMenu, decorators: [{ type: Component, args: [{ selector: "cds-overflow-menu, ibm-overflow-menu", template: ` <button [cdsOverflowMenu]="options" [ngClass]="{'cds--overflow-menu--open': open}" class="cds--overflow-menu {{triggerClass}}" [attr.aria-label]="buttonLabel" [flip]="flip" [isOpen]="open" (isOpenChange)="handleOpenChange($event)" [offset]="offset" [wrapperClass]="wrapperClass" aria-haspopup="true" class="cds--overflow-menu" type="button" [placement]="placement"> <ng-template *ngIf="customTrigger; else defaultIcon" [ngTemplateOutlet]="customTrigger"></ng-template> </button> <ng-template #options> <ng-content></ng-content> </ng-template> <ng-template #defaultIcon> <svg cdsIcon="overflow-menu--vertical" size="16" class="cds--overflow-menu__icon"></svg> </ng-template> `, encapsulation: ViewEncapsulation.None, styles: [".cds--overflow-menu--open{opacity:1}.cds--data-table-v2 .cds--overflow-menu{transform:rotate(90deg)}.cds--data-table-v2 .cds--overflow-menu__icon{transform:rotate(180deg)}\n"] }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i1$1.I18n }]; }, propDecorators: { buttonLabel: [{ type: Input }], flip: [{ type: Input }], placement: [{ type: Input }], open: [{ type: Input }], openChange: [{ type: Output }], customTrigger: [{ type: Input }], offset: [{ type: Input }], wrapperClass: [{ type: Input }], triggerClass: [{ type: Input }], overflowMenuDirective: [{ type: ContentChild, args: [OverflowMenuDirective] }] } }); /** * Available HTML anchor targets */ var Target; (function (Target) { Target["self"] = "_self"; Target["blank"] = "_blank"; Target["parent"] = "_parent"; Target["top"] = "_top"; })(Target || (Target = {})); /** * Security HTML anchor rel when target is set */ const REL = "noreferrer noopener"; /** * `OverflowMenuOption` represents a single option in an overflow menu * * Presently it has three possible states - normal, disabled, and danger: * ``` * <cds-overflow-menu-option>Simple option</cds-overflow-menu-option> * <cds-overflow-menu-option disabled="true">Disabled</cds-overflow-menu-option> * <cds-overflow-menu-option type="danger">Danger option</cds-overflow-menu-option> * ``` * * For content that expands beyond the overflow menu `OverflowMenuOption` automatically adds a title attribute. */ class OverflowMenuOption { constructor(elementRef) { this.elementRef = elementRef; this.optionClass = true; this.role = "presentation"; /** * Set to `true` to display a dividing line above this option */ this.divider = false; /** * toggles between `normal` and `danger` states */ this.type = "normal"; /** * disable/enable interactions */ this.disabled = false; /** * Apply a custom class to the inner button/anchor