UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

503 lines (498 loc) • 23 kB
import { r as registerInstance, c as createEvent, h, H as Host, g as getElement } from './index-8fd57462.js'; import { g as getElementDir } from './dom-d9ba1da4.js'; import { g as getKey } from './key-477fdfc4.js'; import { g as guid } from './guid-09142681.js'; import { d as debounce } from './debounce-4dee3b1c.js'; import { f as forIn } from './forIn-9b0d149f.js'; import { u as updatePopper, c as createPopper, C as CSS } from './popper-9e938e6c.js'; const filter = (data, value) => { const regex = new RegExp(value, "ig"); if (data.length === 0) { console.warn(`No data was passed to the filter function. The data argument should be an array of objects`); } const find = (input, RE) => { let found = false; forIn(input, (val) => { if (typeof val === "function") { return; } if (Array.isArray(val) || (typeof val === "object" && val !== null)) { if (find(val, RE)) { found = true; } } else if (RE.test(val)) { found = true; } }); return found; }; const result = data.filter((item) => { return find(item, regex); }); return result; }; const calciteComboboxCss = "@-webkit-keyframes in{0%{opacity:0}100%{opacity:1}}@keyframes in{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes in-down{0%{opacity:0;-webkit-transform:translate3D(0, -5px, 0);transform:translate3D(0, -5px, 0)}100%{opacity:1;-webkit-transform:translate3D(0, 0, 0);transform:translate3D(0, 0, 0)}}@keyframes in-down{0%{opacity:0;-webkit-transform:translate3D(0, -5px, 0);transform:translate3D(0, -5px, 0)}100%{opacity:1;-webkit-transform:translate3D(0, 0, 0);transform:translate3D(0, 0, 0)}}@-webkit-keyframes in-up{0%{opacity:0;-webkit-transform:translate3D(0, 5px, 0);transform:translate3D(0, 5px, 0)}100%{opacity:1;-webkit-transform:translate3D(0, 0, 0);transform:translate3D(0, 0, 0)}}@keyframes in-up{0%{opacity:0;-webkit-transform:translate3D(0, 5px, 0);transform:translate3D(0, 5px, 0)}100%{opacity:1;-webkit-transform:translate3D(0, 0, 0);transform:translate3D(0, 0, 0)}}@-webkit-keyframes in-scale{0%{opacity:0;-webkit-transform:scale3D(0.95, 0.95, 1);transform:scale3D(0.95, 0.95, 1)}100%{opacity:1;-webkit-transform:scale3D(1, 1, 1);transform:scale3D(1, 1, 1)}}@keyframes in-scale{0%{opacity:0;-webkit-transform:scale3D(0.95, 0.95, 1);transform:scale3D(0.95, 0.95, 1)}100%{opacity:1;-webkit-transform:scale3D(1, 1, 1);transform:scale3D(1, 1, 1)}}:root{--calcite-popper-transition:150ms ease-in-out}:host([hidden]){display:none}:host{display:block;position:relative}:host-context([theme=dark]){--calcite-ui-blue-1:#00A0FF;--calcite-ui-blue-2:#0087D7;--calcite-ui-blue-3:#47BBFF;--calcite-ui-green-1:#36DA43;--calcite-ui-green-2:#11AD1D;--calcite-ui-green-3:#44ED51;--calcite-ui-yellow-1:#FFC900;--calcite-ui-yellow-2:#F4B000;--calcite-ui-yellow-3:#FFE24D;--calcite-ui-red-1:#FE583E;--calcite-ui-red-2:#F3381B;--calcite-ui-red-3:#FF7465;--calcite-ui-background:#202020;--calcite-ui-foreground-1:#2b2b2b;--calcite-ui-foreground-2:#353535;--calcite-ui-foreground-3:#404040;--calcite-ui-text-1:#ffffff;--calcite-ui-text-2:#bfbfbf;--calcite-ui-text-3:#9f9f9f;--calcite-ui-border-1:#4a4a4a;--calcite-ui-border-2:#404040;--calcite-ui-border-3:#353535;--calcite-ui-border-4:#757575;--calcite-ui-border-5:#9f9f9f}:host([disabled]){pointer-events:none;-webkit-user-select:none;-ms-user-select:none;-moz-user-select:none;user-select:none;opacity:0.5}:host([scale=s]){font-size:var(--calcite-font-size--2);--calcite-combobox-item-spacing-unit-l:0.75rem;--calcite-combobox-item-spacing-unit-m:0.5rem;--calcite-combobox-item-spacing-unit-s:0.25rem}:host([scale=s]) .input{height:1.25rem;line-height:1.25rem;margin-bottom:0.5rem}:host([scale=m]){font-size:var(--calcite-font-size--1);--calcite-combobox-item-spacing-unit-l:1rem;--calcite-combobox-item-spacing-unit-m:0.75rem;--calcite-combobox-item-spacing-unit-s:0.5rem}:host([scale=m]) .input{height:2rem;line-height:2rem;margin-bottom:0.75rem}:host([scale=l]){font-size:var(--calcite-font-size-0);--calcite-combobox-item-spacing-unit-l:1.25rem;--calcite-combobox-item-spacing-unit-m:1rem;--calcite-combobox-item-spacing-unit-s:0.75rem}:host([scale=l]) .input{height:2.5rem;line-height:2.5rem;margin-bottom:1rem}.wrapper{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:var(--calcite-combobox-item-spacing-unit-m) var(--calcite-combobox-item-spacing-unit-l) 0 var(--calcite-combobox-item-spacing-unit-l);background-color:var(--calcite-ui-foreground-1);border:1px solid var(--calcite-ui-border-1);color:var(--calcite-ui-text-1);outline-offset:0;outline-color:transparent;-webkit-transition:outline-offset 100ms ease-in-out, outline-color 100ms ease-in-out;transition:outline-offset 100ms ease-in-out, outline-color 100ms ease-in-out}.wrapper--active{outline:2px solid var(--calcite-ui-blue-1);outline-offset:-2px}.input{-ms-flex-positive:1;flex-grow:1;font-size:inherit;font-family:inherit;padding:0;background-color:transparent;border:none;color:var(--calcite-ui-text-1);-webkit-appearance:none;-moz-appearance:none;appearance:none;min-width:120px;margin-top:1px}.input:focus{outline:none}.input--hidden{opacity:0}.popper-container{display:block;position:absolute;z-index:999;-webkit-transform:scale(0);transform:scale(0);visibility:hidden;pointer-events:none;width:100%}.popper-container .calcite-popper-anim{position:relative;z-index:1;-webkit-transition:var(--calcite-popper-transition);transition:var(--calcite-popper-transition);visibility:hidden;-webkit-transition-property:visibility, opacity, -webkit-transform;transition-property:visibility, opacity, -webkit-transform;transition-property:transform, visibility, opacity;transition-property:transform, visibility, opacity, -webkit-transform;opacity:0;-webkit-box-shadow:0 0 16px 0 rgba(0, 0, 0, 0.16);box-shadow:0 0 16px 0 rgba(0, 0, 0, 0.16);border-radius:var(--calcite-border-radius)}.popper-container[data-popper-placement^=bottom] .calcite-popper-anim{-webkit-transform:translateY(-5px);transform:translateY(-5px)}.popper-container[data-popper-placement^=top] .calcite-popper-anim{-webkit-transform:translateY(5px);transform:translateY(5px)}.popper-container[data-popper-placement^=left] .calcite-popper-anim{-webkit-transform:translateX(5px);transform:translateX(5px)}.popper-container[data-popper-placement^=right] .calcite-popper-anim{-webkit-transform:translateX(-5px);transform:translateX(-5px)}.popper-container[data-popper-placement] .calcite-popper-anim--active{opacity:1;visibility:visible;-webkit-transform:translate(0);transform:translate(0)}:host([active]) .popper-container{pointer-events:initial;visibility:visible}.screen-readers-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border-width:0}.list-container{overflow-y:auto;max-height:100vh;width:var(--calcite-dropdown-width);background:var(--calcite-ui-foreground-1)}.list{display:block;margin:0;padding:0}.chip{margin-right:var(--calcite-combobox-item-spacing-unit-s);margin-bottom:var(--calcite-combobox-item-spacing-unit-s)}.chip--active{background-color:var(--calcite-ui-foreground-3)}.chip:last-child{margin-right:0}:host([dir=rtl]) .chip{margin-right:unset;margin-left:var(--calcite-combobox-item-spacing-unit-m)}:host([dir=rtl]) .chip:last-child{margin-left:0}.item{display:block}"; const COMBO_BOX_ITEM = "calcite-combobox-item"; const DEFAULT_PLACEMENT = "bottom-start"; const CalciteCombobox = class { constructor(hostRef) { registerInstance(this, hostRef); this.calciteLookupChange = createEvent(this, "calciteLookupChange", 7); this.calciteComboboxChipDismiss = createEvent(this, "calciteComboboxChipDismiss", 7); //-------------------------------------------------------------------------- // // Public Properties // //-------------------------------------------------------------------------- /** Open and close combobox */ this.active = false; /** Disable combobox input */ this.disabled = false; /** Specify the maximum number of combobox items (including nested children) to display before showing the scroller */ this.maxItems = 0; /** Specify the scale of the combobox, defaults to m */ this.scale = "m"; //-------------------------------------------------------------------------- // // Private State/Props // //-------------------------------------------------------------------------- this.items = []; this.selectedItems = []; this.visibleItems = []; this.activeItemIndex = -1; this.activeChipIndex = -1; this.activeDescendant = ""; this.text = ""; this.textInput = null; this.observer = null; this.guid = guid(); /** specifies the item wrapper height; it is updated when maxItems is > 0 **/ this.maxScrollerHeight = 0; this.inputHeight = 0; // -------------------------------------------------------------------------- // // Private Methods // // -------------------------------------------------------------------------- this.setInactiveIfNotContained = (target) => { if (!this.active || this.el.contains(target)) { return; } this.active = false; }; this.setMenuEl = (el) => { this.menuEl = el; }; this.setListContainerEl = (el) => { this.listContainerEl = el; }; this.setReferenceEl = (el) => { this.referenceEl = el; }; this.inputHandler = (event) => { const value = event.target.value; this.text = value; this.filterItems(value); if (value) { this.activeChipIndex = -1; } }; this.filterItems = debounce((value) => { const filteredData = filter(this.data, value); const values = filteredData.map((item) => item.value); this.items.forEach((item) => { const hidden = values.indexOf(item.value) === -1; item.hidden = hidden; const [parent, grandparent] = item.anscestors; if ((parent || grandparent) && (values.indexOf(parent === null || parent === void 0 ? void 0 : parent.value) > -1 || values.indexOf(grandparent === null || grandparent === void 0 ? void 0 : grandparent.value) > -1)) { item.hidden = false; } if (!hidden) { item.anscestors.forEach((anscestor) => (anscestor.hidden = false)); } }); this.visibleItems = this.getVisibleItems(); }, 100); this.updateItems = () => { this.items = this.getItems(); this.data = this.getData(); this.selectedItems = this.getSelectedItems(); this.visibleItems = this.getVisibleItems(); }; this.comboboxFocusHandler = () => { this.active = true; }; this.comboboxBlurHandler = (event) => { const relatedTarget = event.relatedTarget; this.setInactiveIfNotContained(relatedTarget); }; } activeHandler() { this.reposition(); } //-------------------------------------------------------------------------- // // Event Listeners // //-------------------------------------------------------------------------- documentClickHandler(event) { const target = event.target; this.setInactiveIfNotContained(target); } calciteComboboxItemChangeHandler(event) { this.toggleSelection(event.detail); } calciteChipDismissHandler(event) { var _a; this.active = false; const value = (_a = event.detail) === null || _a === void 0 ? void 0 : _a.value; const comboboxItem = this.items.find((item) => item.value === value); if (comboboxItem) { this.toggleSelection(comboboxItem, false); } this.calciteComboboxChipDismiss.emit(event.detail); } keydownHandler(event) { const key = getKey(event.key, getElementDir(this.el)); switch (key) { case "Tab": this.activeChipIndex = -1; this.activeItemIndex = -1; this.active = false; break; case "ArrowLeft": this.previousChip(); break; case "ArrowRight": this.nextChip(); break; case "ArrowUp": event.preventDefault(); this.active = true; this.shiftActiveItemIndex(-1); break; case "ArrowDown": event.preventDefault(); this.active = true; this.shiftActiveItemIndex(1); break; case "Home": this.active = true; this.updateActiveItemIndex(0); break; case "End": this.active = true; this.updateActiveItemIndex(this.visibleItems.length - 1); break; case "Escape": this.active = false; break; case "Enter": if (this.activeItemIndex > -1) { this.toggleSelection(this.visibleItems[this.activeItemIndex]); } else if (this.activeChipIndex > -1) { this.removeActiveChip(); } else if (this.allowCustomValues && this.text) { this.addCustomChip(this.text); } break; case "Delete": case "Backspace": if (this.activeChipIndex > -1) { this.removeActiveChip(); } else if (!this.text) { this.removeLastChip(); } break; default: if (!this.active) { this.setFocus(); } break; } } //-------------------------------------------------------------------------- // // Public Methods // //-------------------------------------------------------------------------- async reposition() { const { popper, menuEl } = this; const modifiers = this.getModifiers(); popper ? updatePopper({ el: menuEl, modifiers, placement: DEFAULT_PLACEMENT, popper }) : this.createPopper(); } async setFocus() { var _a; this.active = true; (_a = this.textInput) === null || _a === void 0 ? void 0 : _a.focus(); this.activeChipIndex = -1; this.activeItemIndex = -1; } // -------------------------------------------------------------------------- // // Lifecycle // // -------------------------------------------------------------------------- connectedCallback() { { this.observer = new MutationObserver(this.updateItems); } this.createPopper(); } componentWillLoad() { this.updateItems(); } componentDidLoad() { var _a; (_a = this.observer) === null || _a === void 0 ? void 0 : _a.observe(this.el, { childList: true, subtree: true }); this.maxScrollerHeight = this.getMaxScrollerHeight(this.items); } componentDidRender() { if (this.el.offsetHeight !== this.inputHeight) { this.reposition(); this.inputHeight = this.el.offsetHeight; } } disconnectedCallback() { var _a; (_a = this.observer) === null || _a === void 0 ? void 0 : _a.disconnect(); this.destroyPopper(); } /** when search text is cleared, reset active to */ textHandler() { this.updateActiveItemIndex(-1); } getModifiers() { const flipModifier = { name: "flip", enabled: true }; flipModifier.options = { fallbackPlacements: ["top-start", "top", "top-end", "bottom-start", "bottom", "bottom-end"] }; return [flipModifier]; } createPopper() { this.destroyPopper(); const { menuEl, referenceEl } = this; const modifiers = this.getModifiers(); this.popper = createPopper({ el: menuEl, modifiers, placement: DEFAULT_PLACEMENT, referenceEl }); } destroyPopper() { const { popper } = this; if (popper) { popper.destroy(); } this.popper = null; } getMaxScrollerHeight(items) { const { maxItems } = this; let itemsToProcess = 0; let maxScrollerHeight = 0; items.forEach((item) => { if (itemsToProcess < maxItems && maxItems > 0) { maxScrollerHeight += this.calculateSingleItemHeight(item); itemsToProcess++; } }); return maxScrollerHeight; } calculateSingleItemHeight(item) { let height = item.offsetHeight; // if item has children items, don't count their height twice const children = item.querySelectorAll("calcite-combobox-item"); children.forEach((child) => { height -= child.offsetHeight; }); return height; } toggleSelection(item, value = !item.selected) { item.selected = value; this.selectedItems = this.getSelectedItems(); this.calciteLookupChange.emit(this.selectedItems); this.resetText(); this.textInput.focus(); this.filterItems(""); } getVisibleItems() { return this.items.filter((item) => !item.hidden); } getSelectedItems() { return (this.items .filter((item) => item.selected) /** Preserve order of entered tags */ .sort((a, b) => { const aIdx = this.selectedItems.indexOf(a); const bIdx = this.selectedItems.indexOf(b); if (aIdx > -1 && bIdx > -1) { return aIdx - bIdx; } return bIdx - aIdx; })); } getData() { return this.items.map((item) => ({ value: item.value, label: item.textLabel, guid: item.guid })); } resetText() { this.textInput.value = ""; this.text = ""; } getItems() { const items = Array.from(this.el.querySelectorAll(COMBO_BOX_ITEM)); return items.filter((item) => !item.disabled); } addCustomChip(value) { const existingItem = this.items.find((el) => el.value === value || el.textLabel === value); if (existingItem) { this.toggleSelection(existingItem, true); } else { const item = document.createElement("calcite-combobox-item"); item.value = value; item.textLabel = value; item.guid = guid(); item.selected = true; this.el.appendChild(item); this.resetText(); this.setFocus(); this.updateItems(); this.filterItems(""); } } removeActiveChip() { this.toggleSelection(this.selectedItems[this.activeChipIndex], false); this.setFocus(); } removeLastChip() { this.toggleSelection(this.selectedItems[this.selectedItems.length - 1], false); this.setFocus(); } previousChip() { if (this.text) { return; } const length = this.selectedItems.length - 1; const active = this.activeChipIndex; this.activeChipIndex = active === -1 ? length : Math.max(active - 1, 0); this.updateActiveItemIndex(-1); this.focusChip(); } nextChip() { if (this.text || this.activeChipIndex === -1) { return; } const last = this.selectedItems.length - 1; const newIndex = this.activeChipIndex + 1; if (newIndex > last) { this.activeChipIndex = -1; this.setFocus(); } else { this.activeChipIndex = newIndex; this.focusChip(); } this.updateActiveItemIndex(-1); } focusChip() { var _a; const guid = (_a = this.selectedItems[this.activeChipIndex]) === null || _a === void 0 ? void 0 : _a.guid; const chip = this.referenceEl.querySelector(`#chip-${guid}`); chip === null || chip === void 0 ? void 0 : chip.setFocus(); } shiftActiveItemIndex(delta) { const length = this.visibleItems.length; const newIndex = (this.activeItemIndex + length + delta) % length; this.updateActiveItemIndex(newIndex); // ensure active item is in view if we have scrolling const activeItem = this.visibleItems[this.activeItemIndex]; const height = this.calculateSingleItemHeight(activeItem); const { offsetHeight, scrollTop } = this.listContainerEl; if (offsetHeight + scrollTop < activeItem.offsetTop + height) { this.listContainerEl.scrollTop = activeItem.offsetTop - offsetHeight + height; } else if (activeItem.offsetTop < scrollTop) { this.listContainerEl.scrollTop = activeItem.offsetTop; } } updateActiveItemIndex(index) { this.activeItemIndex = index; let activeDescendant = null; this.visibleItems.forEach((el, i) => { if (i === index) { el.active = true; activeDescendant = el.guid; } else { el.active = false; } }); this.activeDescendant = activeDescendant; if (this.activeItemIndex > -1) { this.activeChipIndex = -1; this.textInput.focus(); } } //-------------------------------------------------------------------------- // // Render Methods // //-------------------------------------------------------------------------- renderChips() { const { activeChipIndex, scale } = this; return this.selectedItems.map((item, i) => { const chipClasses = { chip: true, "chip--active": activeChipIndex === i }; return (h("calcite-chip", { class: chipClasses, dismissLabel: "remove tag", dismissible: true, id: `chip-${item.guid}`, key: item.value, scale: scale, value: item.value }, item.textLabel)); }); } renderListBoxOptions() { return this.visibleItems.map((item) => (h("li", { "aria-selected": (!!item.selected).toString(), id: item.guid, role: "option", tabindex: "-1" }, item.value))); } renderPopperContainer() { const { active, maxScrollerHeight, setMenuEl, setListContainerEl } = this; const classes = { "list-container": true, [CSS.animation]: true, [CSS.animationActive]: active }; const style = { maxHeight: maxScrollerHeight > 0 ? `${maxScrollerHeight}px` : "" }; return (h("div", { "aria-hidden": "true", class: "popper-container", ref: setMenuEl }, h("div", { class: classes, ref: setListContainerEl, style: style }, h("ul", { class: "list" }, h("slot", null))))); } render() { const { guid, active, disabled, el, label, placeholder } = this; const dir = getElementDir(el); const labelId = `${guid}-label`; return (h(Host, { active: active, dir: dir }, h("div", { "aria-autocomplete": "list", "aria-expanded": active.toString(), "aria-haspopup": "listbox", "aria-labelledby": labelId, "aria-owns": guid, class: { wrapper: true, "wrapper--active": active }, onClick: () => this.setFocus(), ref: this.setReferenceEl, role: "combobox" }, this.renderChips(), h("label", { class: "screen-readers-only", htmlFor: `${guid}-input`, id: labelId }, label), h("input", { "aria-activedescendant": this.activeDescendant, "aria-autocomplete": "list", "aria-controls": guid, class: { input: true, "input--hidden": this.activeChipIndex > -1 }, disabled: disabled, id: `${guid}-input`, onBlur: this.comboboxBlurHandler, onFocus: this.comboboxFocusHandler, onInput: this.inputHandler, placeholder: placeholder, ref: (el) => (this.textInput = el), type: "text" })), h("ul", { "aria-labelledby": labelId, "aria-multiselectable": "true", class: "screen-readers-only", id: guid, role: "listbox", tabIndex: -1 }, this.renderListBoxOptions()), this.renderPopperContainer())); } get el() { return getElement(this); } static get watchers() { return { "active": ["activeHandler"], "text": ["textHandler"] }; } }; CalciteCombobox.style = calciteComboboxCss; export { CalciteCombobox as calcite_combobox };