carbon-components-angular
Version:
Next generation components
499 lines (496 loc) • 20.6 kB
JavaScript
/*!
*
* 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