UNPKG

carbon-components-angular

Version:
499 lines (496 loc) 20.6 kB
/*! * * Neutrino v0.0.0 | dropdown.component.js * * Copyright 2014, 2018 IBM * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Component, Input, Output, EventEmitter, ElementRef, ContentChild, ViewChild, HostListener } from "@angular/core"; import { NG_VALUE_ACCESSOR } from "@angular/forms"; // Observable import is required here so typescript can compile correctly import { fromEvent, of } from "rxjs"; import { throttleTime } from "rxjs/operators"; import { AbstractDropdownView } from "./abstract-dropdown-view.class"; import { position } from "../utils/position"; import { I18n } from "./../i18n/i18n.module"; /** * Drop-down lists enable users to select one or more items from a list. * */ var Dropdown = /** @class */ (function () { /** * Creates an instance of Dropdown. */ function Dropdown(elementRef, i18n) { this.elementRef = elementRef; this.i18n = i18n; /** * Value displayed if no item is selected. */ this.placeholder = ""; /** * The selected value from the `Dropdown`. */ this.displayValue = ""; /** * Size to render the dropdown field. */ this.size = "md"; /** * Defines whether or not the `Dropdown` supports selecting multiple items as opposed to single * item selection. */ this.type = "single"; /** * Set to `true` to disable the dropdown. */ this.disabled = false; /** * set to `true` to place the dropdown view inline with the component */ this.appendInline = false; /** * Accessible label for the button that opens the dropdown list. * Defaults to the `DROPDOWN.OPEN` value from the i18n service. */ this.menuButtonLabel = this.i18n.get().DROPDOWN.OPEN; /** * Provides the label for the "# selected" text. * Defaults to the `DROPDOWN.SELECTED` value from the i18n service. */ this.selectedLabel = this.i18n.get().DROPDOWN.SELECTED; /** * Emits selection events. */ this.selected = new EventEmitter(); /** * Emits event notifying to other classes that the `Dropdown` has been closed (collapsed). */ this.onClose = new EventEmitter(); /** * Emits event notifying to other classes that the `Dropdown` has been closed (collapsed). */ this.close = new EventEmitter(); /** * Set to `true` if the dropdown is closed (not expanded). */ this.menuIsClosed = true; /** * controls wether the `drop-up` class is applied */ this.dropUp = false; // .bind creates a new function, so we declare the methods below // but .bind them up here this.noop = this._noop.bind(this); this.outsideClick = this._outsideClick.bind(this); this.outsideKey = this._outsideKey.bind(this); this.keyboardNav = this._keyboardNav.bind(this); this.onTouchedCallback = this._noop; this.propagateChange = function (_) { }; } Object.defineProperty(Dropdown.prototype, "appendToBody", { get: function () { return !this.appendInline; }, /** * Deprecated. Dropdown now defaults to appending inline * Set to `true` if the `Dropdown` is to be appended to the DOM body. */ set: function (v) { console.log("`appendToBody` has been deprecated. Dropdowns now append to the body by default."); console.log("Ensure you have an `ibm-placeholder` in your app."); console.log("Use `appendInline` if you need to position your dropdowns within the normal page flow."); this.appendInline = !v; }, enumerable: true, configurable: true }); /** * Updates the `type` property in the `@ContentChild`. * The `type` property specifies whether the `Dropdown` allows single selection or multi selection. */ Dropdown.prototype.ngOnInit = function () { this.view.type = this.type; }; /** * Initializes classes and subscribes to events for single or multi selection. */ Dropdown.prototype.ngAfterContentInit = function () { var _this = this; this.view.type = this.type; this.view.size = this.size; this.view.select.subscribe(function (event) { if (_this.type === "multi") { _this.propagateChange(_this.view.getSelected()); } else { _this.closeMenu(); _this.dropdownButton.nativeElement.focus(); if (event.item && event.item.selected) { if (_this.value) { _this.propagateChange(event.item[_this.value]); } else { _this.propagateChange(event.item); } } else { _this.propagateChange(null); } } _this.selected.emit(event); }); }; /** * Removing the `Dropdown` from the body if it is appended to the body. */ Dropdown.prototype.ngOnDestroy = function () { if (this.appendToBody) { this._appendToDropdown(); } }; /** * Propagates the injected `value`. */ Dropdown.prototype.writeValue = function (value) { var _this = this; if (this.type === "single") { if (this.value) { var newValue = Object.assign({}, this.view.items.find(function (item) { return item[_this.value] === value; })); newValue.selected = true; this.view.propagateSelected([newValue]); } else { this.view.propagateSelected([value]); } } else { this.view.propagateSelected(value); } }; Dropdown.prototype.onBlur = function () { this.onTouchedCallback(); }; Dropdown.prototype.registerOnChange = function (fn) { this.propagateChange = fn; }; /** * Registering the function injected to control the touch use of the `Dropdown`. */ Dropdown.prototype.registerOnTouched = function (fn) { this.onTouchedCallback = fn; }; /** * Adds keyboard functionality for navigation, selection and closing of the `Dropdown`. */ Dropdown.prototype.onKeyDown = function (event) { if (event.key === "Escape" && !this.menuIsClosed) { event.stopImmediatePropagation(); // don't unintentionally close other widgets that listen for Escape } if (event.key === "Escape" || (event.key === "ArrowUp" && event.altKey)) { event.preventDefault(); this.closeMenu(); this.dropdownButton.nativeElement.focus(); } else if (event.key === "ArrowDown" && event.altKey) { event.preventDefault(); this.openMenu(); } if (!this.menuIsClosed && event.key === "Tab" && this.dropdownMenu.nativeElement.contains(event.target)) { this.closeMenu(); } if (!this.menuIsClosed && event.key === "Tab" && event.shiftKey) { this.closeMenu(); } if (this.type === "multi") { return; } if (this.menuIsClosed) { this.closedDropdownNavigation(event); } }; Dropdown.prototype.closedDropdownNavigation = function (event) { if (event.key === "ArrowDown") { event.preventDefault(); this.view.getCurrentItem().selected = false; var item = this.view.getNextItem(); if (item) { item.selected = true; } } else if (event.key === "ArrowUp") { event.preventDefault(); this.view.getCurrentItem().selected = false; var item = this.view.getPrevItem(); if (item) { item.selected = true; } } }; /** * Returns the display value if there is no selection, otherwise the selection will be returned. */ Dropdown.prototype.getDisplayValue = function () { var selected = this.view.getSelected(); if (selected && !this.displayValue) { if (this.type === "multi") { return of(selected.length + " " + this.selectedLabel); } else { return of(selected[0].content); } } else if (selected) { return of(this.displayValue); } return of(this.placeholder); }; /** * Returns `true` if there is a value selected. */ Dropdown.prototype.valueSelected = function () { if (this.view.getSelected()) { return true; } return false; }; Dropdown.prototype._noop = function () { }; /** * Handles clicks outside of the `Dropdown`. */ Dropdown.prototype._outsideClick = function (event) { if (!this.elementRef.nativeElement.contains(event.target) && // if we're appendToBody the list isn't within the _elementRef, // so we've got to check if our target is possibly in there too. !this.dropdownMenu.nativeElement.contains(event.target)) { this.closeMenu(); } }; Dropdown.prototype._outsideKey = function (event) { if (!this.menuIsClosed && event.key === "Tab" && this.dropdownMenu.nativeElement.contains(event.target)) { this.closeMenu(); } }; /** * Handles keyboard events so users are controlling the `Dropdown` instead of unintentionally controlling outside elements. */ Dropdown.prototype._keyboardNav = function (event) { if (event.key === "Escape" && !this.menuIsClosed) { event.stopImmediatePropagation(); // don't unintentionally close modal if inside of it } if (event.key === "Escape" || (event.key === "ArrowUp" && event.altKey)) { event.preventDefault(); this.closeMenu(); this.dropdownButton.nativeElement.focus(); } else if (!this.menuIsClosed && event.key === "Tab") { // this way focus will start on the next focusable item from the dropdown // not the top of the body! this.dropdownButton.nativeElement.focus(); this.dropdownButton.nativeElement.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "Tab" })); this.closeMenu(); } }; /** * Creates the `Dropdown` list appending it to the dropdown parent object instead of the body. */ Dropdown.prototype._appendToDropdown = function () { if (document.body.contains(this.dropdownWrapper)) { this.dropdownMenu.nativeElement.style.display = "none"; this.elementRef.nativeElement.appendChild(this.dropdownMenu.nativeElement); document.body.removeChild(this.dropdownWrapper); this.resize.unsubscribe(); this.dropdownWrapper.removeEventListener("keydown", this.keyboardNav, true); } }; /** * Creates the `Dropdown` list as an element that is appended to the DOM body. */ Dropdown.prototype._appendToBody = function () { var _this = this; var positionDropdown = function () { var pos = position.findAbsolute(_this.dropdownButton.nativeElement, _this.dropdownWrapper, "bottom"); // add -40 to the top position to account for carbon styles pos = position.addOffset(pos, -40, 0); pos = position.addOffset(pos, window.scrollY, window.scrollX); position.setElement(_this.dropdownWrapper, pos); }; this.dropdownMenu.nativeElement.style.display = "block"; this.dropdownWrapper = document.createElement("div"); this.dropdownWrapper.className = "dropdown " + this.elementRef.nativeElement.className; this.dropdownWrapper.style.width = this.dropdownButton.nativeElement.offsetWidth + "px"; this.dropdownWrapper.style.position = "absolute"; this.dropdownWrapper.appendChild(this.dropdownMenu.nativeElement); document.body.appendChild(this.dropdownWrapper); positionDropdown(); this.dropdownWrapper.addEventListener("keydown", this.keyboardNav, true); this.resize = fromEvent(window, "resize") .pipe(throttleTime(100)) .subscribe(function () { return positionDropdown(); }); }; /** * Expands the dropdown menu in the view. */ Dropdown.prototype.openMenu = function () { var _this = this; this.menuIsClosed = false; // move the dropdown list to the body if we're not appending inline // and position it relative to the dropdown wrapper if (!this.appendInline) { this.addScrollEventListener(); this._appendToBody(); } // set the dropdown menu to drop up if it's near the bottom of the screen // setTimeout lets us measure after it's visible in the DOM setTimeout(function () { var menu = _this.dropdownMenu.nativeElement; var boundingClientRect = menu.getBoundingClientRect(); if (boundingClientRect.bottom > window.innerHeight) { // min height of 100px if (window.innerHeight - boundingClientRect.top > 100) { // remove the conditional once this api is settled and part of abstract-dropdown-view.class if (_this.view["enableScroll"]) { _this.view["enableScroll"](); } } else { _this.dropUp = true; } } else { _this.dropUp = false; } }, 0); // we bind noop to document.body.firstElementChild to allow safari to fire events // from document. Then we unbind everything later to keep things light. document.body.firstElementChild.addEventListener("click", this.noop, true); document.body.firstElementChild.addEventListener("keydown", this.noop, true); document.addEventListener("click", this.outsideClick, true); document.addEventListener("keydown", this.outsideKey, true); // setTimeout(() => this.view.initFocus(), 0); }; /** * Collapsing the dropdown menu and removing unnecessary `EventListeners`. */ Dropdown.prototype.closeMenu = function () { this.menuIsClosed = true; this.onClose.emit(); this.close.emit(); // remove the conditional once this api is settled and part of abstract-dropdown-view.class if (this.view["disableScroll"]) { this.view["disableScroll"](); } // move the list back in the component on close if (!this.appendInline) { this.removeScrollEventListener(); this._appendToDropdown(); } document.body.firstElementChild.removeEventListener("click", this.noop, true); document.body.firstElementChild.removeEventListener("keydown", this.noop, true); document.removeEventListener("click", this.outsideClick, true); document.removeEventListener("keydown", this.outsideKey, true); }; /** * Add scroll event listenter if scrollableContainer is provided */ Dropdown.prototype.addScrollEventListener = function () { var _this = this; if (this.scrollableContainer) { var container_1 = document.querySelector(this.scrollableContainer); if (container_1) { this.scroll = fromEvent(container_1, "scroll") .subscribe(function () { if (_this.isVisibleInContainer(_this.elementRef.nativeElement, container_1)) { position.setElement(_this.dropdownWrapper, position.addOffset(position.findAbsolute(_this.elementRef.nativeElement, _this.dropdownWrapper, "bottom"))); } else { _this.closeMenu(); } }); } } }; /** * Removes any `EventListeners` responsible for scroll functionality. */ Dropdown.prototype.removeScrollEventListener = function () { if (this.scroll) { this.scroll.unsubscribe(); } }; /** * Controls toggling menu states between open/expanded and closed/collapsed. */ Dropdown.prototype.toggleMenu = function () { if (this.menuIsClosed) { this.openMenu(); } else { this.closeMenu(); } }; /** * Returns `true` if the `elem` is visible within the `container`. */ Dropdown.prototype.isVisibleInContainer = function (elem, container) { var containerTop = container.scrollTop; var containerBottom = containerTop + container.offsetHeight; var elemTop = elem.offsetTop + elem.offsetHeight; var elemBottom = elemTop; if ((elemBottom <= containerBottom) && (elemTop >= containerTop)) { return true; } return false; }; Dropdown.decorators = [ { type: Component, args: [{ selector: "ibm-dropdown", template: "\n\t<div class=\"bx--list-box\">\n\t\t<button\n\t\t\ttype=\"button\"\n\t\t\t#dropdownButton\n\t\t\tclass=\"bx--list-box__field\"\n\t\t\t[ngClass]=\"{'a': !menuIsClosed}\"\n\t\t\t[attr.aria-expanded]=\"!menuIsClosed\"\n\t\t\t[attr.aria-disabled]=\"disabled\"\n\t\t\t(click)=\"toggleMenu()\"\n\t\t\t(blur)=\"onBlur()\"\n\t\t\t[disabled]=\"disabled\">\n\t\t\t<span class=\"bx--list-box__label\">{{getDisplayValue() | async}}</span>\n\t\t\t<div class=\"bx--list-box__menu-icon\" [ngClass]=\"{'bx--list-box__menu-icon--open': !menuIsClosed }\">\n\t\t\t\t<svg fill-rule=\"evenodd\" height=\"5\" role=\"img\" viewBox=\"0 0 10 5\" width=\"10\" alt=\"Open menu\" [attr.aria-label]=\"menuButtonLabel\">\n\t\t\t\t\t<title>{{menuButtonLabel}}</title>\n\t\t\t\t\t<path d=\"M0 0l5 4.998L10 0z\"></path>\n\t\t\t\t</svg>\n\t\t\t</div>\n\t\t</button>\n\t\t<div\n\t\t\t#dropdownMenu\n\t\t\t[ngClass]=\"{\n\t\t\t\t'drop-up': dropUp\n\t\t\t}\">\n\t\t\t<ng-content *ngIf=\"!menuIsClosed\"></ng-content>\n\t\t</div>\n\t</div>\n\t", providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: Dropdown, multi: true } ] },] }, ]; /** @nocollapse */ Dropdown.ctorParameters = function () { return [ { type: ElementRef }, { type: I18n } ]; }; Dropdown.propDecorators = { placeholder: [{ type: Input }], displayValue: [{ type: Input }], size: [{ type: Input }], type: [{ type: Input }], disabled: [{ type: Input }], appendToBody: [{ type: Input }], appendInline: [{ type: Input }], scrollableContainer: [{ type: Input }], value: [{ type: Input }], menuButtonLabel: [{ type: Input }], selectedLabel: [{ type: Input }], selected: [{ type: Output }], onClose: [{ type: Output }], close: [{ type: Output }], view: [{ type: ContentChild, args: [AbstractDropdownView,] }], dropdownButton: [{ type: ViewChild, args: ["dropdownButton",] }], dropdownMenu: [{ type: ViewChild, args: ["dropdownMenu",] }], onKeyDown: [{ type: HostListener, args: ["keydown", ["$event"],] }] }; return Dropdown; }()); export { Dropdown }; //# sourceMappingURL=dropdown.component.js.map