carbon-components-angular
Version:
Next generation components
1,178 lines (1,168 loc) • 58.6 kB
JavaScript
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