UNPKG

@cbpds/web-components

Version:
865 lines (864 loc) 35.4 kB
/*! * CPB Design System web components - built with Stencil */ import { Host, h } from "@stencil/core"; import { setCSSProps, createNamespaceKey, clickAwayListener } from "../../utils/utils"; export class CbpDropdown { constructor() { this.dropdownItems = []; this.generatedItems = []; this.multiple = false; this.filter = false; this.async = false; this.minimumInputLength = undefined; this.items = undefined; this.fieldId = createNamespaceKey('cbp-dropdown'); this.name = this.fieldId; this.placeholder = 'Choose Item'; this.selectedLabel = undefined; this.value = undefined; this.open = false; this.error = false; this.readonly = false; this.disabled = false; this.context = undefined; this.sx = {}; this.selectedItems = []; this.selectedItemCount = 0; this.searchString = ''; } selectDropdownItem({ target }) { if (target.tagName != "LABEL") { this.valueChange.emit({ host: this.host, value: this.value }); } } handleDropdownItemClick({ detail: { host, label, value } }) { var _a; let oldIndex = this.focusIndex; if (this.multiple) { let newValue = host.selected = !host.selected; setTimeout(() => { this.selectedItems = Array.from(this.host.querySelectorAll('cbp-dropdown-item[selected]')); this.placeholder = this.selectedItems.length != 1 ? 'Selected Items' : 'Selected Item'; }, 50); if (this.value && typeof this.value == "string") this.value = this.value.split(','); newValue ? (this.value = [...this.value, value]) : (this.value = this.value.filter(item => item !== value)); this.control.focus(); } else { this.dropdownItems.forEach(item => { if (item === host) { item.selected = true; } else item.selected = false; }); this.selectedLabel = label; this.value = value; this.open = false; setTimeout(() => { this.control.focus(); }, 100); } this.setCurrent((_a = this.dropdownItems) === null || _a === void 0 ? void 0 : _a.indexOf(host), oldIndex); this.valueChange.emit({ host: this.host, nativeElement: this.formField, value: this.value, label: this.selectedLabel, }); } watchOpen(newValue) { if (newValue) { if (!this.items) { this.dropdownItems = Array.from(this.host.querySelectorAll('cbp-dropdown-item')); this.selectedItems = Array.from(this.host.querySelectorAll('cbp-dropdown-item[selected]')); } if (this.async && !this.items) this.items = [this.showNoResultsItem()]; if (this.dropdownItems.length) this.setDefaultItem(); clickAwayListener(this.host, _ => { this.open = false; }); } else { this.dropdownItems.forEach(item => { item.current = false; }); this.control.removeAttribute('aria-activedescendant'); this.clearFilters(); } } watchValue(newValue) { var _a; if (newValue != ((_a = this.formField) === null || _a === void 0 ? void 0 : _a.value) && newValue != '') { this.setSelectedFromValue(); } } watchItems(newValue) { this.generatedItems = this.generateItems(newValue); if (this.async) { let matches = []; this.generatedItems.forEach((index) => { matches = [...matches, index]; }); this.matches = matches; } } setSelectedFromValue() { if (!!this.value) { this.dropdownItems = Array.from(this.host.querySelectorAll('cbp-dropdown-item')); let selectedItems = []; if (this.multiple) { let values = (typeof this.value == "string") ? this.value.split(",") : this.value; this.dropdownItems.forEach(item => { if (item.value) { if (values.includes(item.value)) { item.selected = true; selectedItems = [...selectedItems, item]; } } else { if (values.includes(item.innerText.trim())) { item.selected = true; selectedItems = [...selectedItems, item]; } } }); this.selectedItems = [...selectedItems]; } else { this.dropdownItems.forEach((item) => { if (item.value == this.value) { this.selectedLabel = item.innerText.trim(); item.selected = true; this.selectedItems = [...selectedItems, item]; } else item.selected = false; }); } } } generateItems(items) { let firstSelected; if (this.multiple && this.value && typeof this.value == "string") this.value = this.value.split(','); if (typeof items == 'string') { items = JSON.parse(items) || {}; } if (this.async && this.searchString.length < this.minimumInputLength) items = this.dropdownItems = []; let generatedItems = []; items.map(({ label, value = label }, index) => { var _a, _b; let newItem = h("cbp-dropdown-item", { value: `${value}`, key: `cbp-dropdown-item-${value}`, selected: (this.multiple && ((_a = this.value) === null || _a === void 0 ? void 0 : _a.includes(value))) ? true : (!this.multiple && this.value === value) ? true : false }, this.multiple ? h("cbp-checkbox", { context: this.context }, h("input", { type: "checkbox", name: `${this.name}-selection`, value: `${value}`, tabindex: -1 }), label) : `${label}`); if (this.multiple && ((_b = this.value) === null || _b === void 0 ? void 0 : _b.includes(value))) { if (firstSelected == undefined) this.focusIndex = this.matchIndex = index; } else if (!this.multiple && value === this.value) { this.focusIndex = this.matchIndex = index; } generatedItems = [...generatedItems, newItem]; }); if (generatedItems.length == 0) { generatedItems = [...generatedItems, this.showNoResultsItem()]; } return generatedItems; } showNoResultsItem() { let newItem = h("cbp-dropdown-item", { value: "", key: "cbp-dropdown-item-no-results", disabled: true }, "No results."); return newItem; } async clearSelections() { this.selectedItems = Array.from(this.host.querySelectorAll('cbp-dropdown-item[selected]')); this.selectedItems.forEach(item => { item.selected = false; }); this.multiple ? (this.value = []) : (this.formField.value = undefined); setTimeout(() => { this.selectedItems = Array.from(this.host.querySelectorAll('cbp-dropdown-item[selected]')); }, 100); this.valueChange.emit({ host: this.host, nativeElement: this.formField, value: this.value, label: undefined, }); } handleSlotChange(e) { console.log('Dropdown Slot Change: ', e); } handleCounterClick(e) { this.clearSelections(); e.stopImmediatePropagation(); this.counterControl.focus(); } handleCounterKeydown(e) { const { key } = e; if (key == ' ' || key == 'Enter') { this.clearSelections(); e.preventDefault(); this.counterControl.focus(); } } handleDropdownClick(e) { if (e.detail && !this.readonly && !this.disabled) this.open = !this.open; } getActionFromKey(event) { var _a, _b, _c; const { key, altKey, ctrlKey, metaKey } = event; const selectKeys = ['Enter', ' ']; const openKeys = ['ArrowDown', 'ArrowUp', 'Enter', ' ']; const navKeys = ['ArrowDown', 'ArrowUp', 'Enter', 'Home', 'End']; if (this.open && selectKeys.includes(key) && !(key == ' ' && this.searchString !== '')) { (_a = this.dropdownItems[this.focusIndex]) === null || _a === void 0 ? void 0 : _a.click(); return; } if (this.open) { const i = (this.filter && this.searchString) ? this.matchIndex : this.focusIndex; const l = (this.filter && this.searchString) ? ((_b = this.matches) === null || _b === void 0 ? void 0 : _b.length) - 1 || 0 : ((_c = this.dropdownItems) === null || _c === void 0 ? void 0 : _c.length) - 1 || 0; const n = { Home: 0, ArrowUp: -1 < i + -1 ? i + -1 : l, ArrowDown: l + 1 > i + 1 ? i + 1 : 0, End: l, }[key]; if (n !== undefined && key !== 'Tab') { this.matchIndex = n; this.setCurrent((this.filter && this.searchString) ? this.matches[n] : n, this.focusIndex); if (!this.filter) this.searchString = ''; } } if (openKeys.includes(key) && !this.readonly && !this.disabled) { if (!this.open) this.open = true; } if (key == 'Escape') { this.open = false; this.control.focus(); } if (key == 'Tab') { this.open = false; } if (key === 'Backspace' || key === 'Clear' || (key == ' ' && this.searchString !== '') || (key.length === 1 && !altKey && !ctrlKey && !metaKey && !navKeys.includes(key))) { this.open = true; this.filter ? this.searchByString(key.toLowerCase()) : this.jumpToLetter(key.toLowerCase()); } } jumpToLetter(letter) { if (letter != this.searchString) { this.searchString = letter; this.getFirstLetterMatches(letter); if (this.matches.length > 0) { this.matchIndex = 0; this.setCurrent(this.matches[0], this.focusIndex); } } else { if (this.matches.length > 0) { if (this.matchIndex + 1 < this.matches.length) this.matchIndex += 1; else this.matchIndex = 0; this.setCurrent(this.matches[this.matchIndex], this.focusIndex); } } } getFirstLetterMatches(letter) { let matches = []; this.dropdownItems.forEach((item, index) => { const label = item.innerText.toLowerCase().trim(); if (label.startsWith(letter)) { matches = [...matches, index]; } }); this.matches = matches; } searchByString(letter) { if (letter == 'backspace' || letter == 'clear') { const l = this.searchString.length; if (l <= 1) { this.clearFilters(); return; } else this.searchString = this.searchString.substring(0, l - 1); } else { this.searchString += letter; } if (this.async) { if (this.searchString.length >= this.minimumInputLength) { this.populateCombobox.emit({ searchString: this.searchString, host: this.host }); } else { this.items = [this.showNoResultsItem()]; } } else { this.getSearchStringMatches(this.searchString); this.filterDropdownItems(this.matches); if (this.matches.length > 0) { this.setCurrent(this.matches[0], this.focusIndex); } else { } } } ; getSearchStringMatches(searchString) { let matches = []; this.dropdownItems.forEach((item, index) => { const label = item.innerText.toLowerCase().trim(); if (label.indexOf(searchString) >= 0) { matches = [...matches, index]; } }); this.matches = matches; this.matchIndex = 0; } filterDropdownItems(matches) { this.dropdownItems.forEach((item, index) => { matches.includes(index) ? item.removeAttribute('hidden') : item.setAttribute('hidden', ''); }); } clearFilters() { this.matches = []; this.matchIndex = undefined; this.searchString = ''; this.dropdownItems.forEach(item => { item.removeAttribute('hidden'); }); } setDefaultItem() { let oldIndex = this.focusIndex; let newIndex; if (this.selectedItems.length) { newIndex = this.dropdownItems.indexOf(this.selectedItems[0]); } else newIndex = 0; this.setCurrent(newIndex, oldIndex); } setCurrent(newValue = 0, oldValue = undefined) { if (oldValue != undefined && oldValue != newValue && this.dropdownItems[oldValue]) { this.dropdownItems[oldValue].current = false; } if (this.dropdownItems[newValue]) { this.dropdownItems[newValue].current = true; this.control.setAttribute('aria-activedescendant', this.dropdownItems[newValue].id); this.focusIndex = newValue; if (this.async) this.matchIndex = newValue; setTimeout(() => { if (this.isScrollable(this.listbox)) this.maintainScrollVisibility(this.dropdownItems[newValue], this.listbox); }, 10); } else { } } isScrollable(element) { return element && element.clientHeight < element.scrollHeight; } maintainScrollVisibility(activeElement, scrollParent) { const { offsetHeight, offsetTop } = activeElement; const { offsetHeight: parentOffsetHeight, scrollTop } = scrollParent; const isAbove = offsetTop < scrollTop; const isBelow = offsetTop + offsetHeight > scrollTop + parentOffsetHeight; if (isAbove) { scrollParent.scrollTo(0, offsetTop); } else if (isBelow) { scrollParent.scrollTo(0, offsetTop - parentOffsetHeight + offsetHeight); } } componentWillLoad() { if (!!this.items) { this.generatedItems = this.generateItems(this.items); } else { this.dropdownItems = Array.from(this.host.querySelectorAll('cbp-dropdown-item')); this.selectedItems = Array.from(this.host.querySelectorAll('cbp-dropdown-item[selected]')); } this.attachedButtonStart = this.host.querySelector('[slot=cbp-dropdown-attached-button-start]'); this.attachedButtonEnd = this.host.querySelector('[slot=cbp-dropdown-attached-button-end]'); if (this.multiple && !this.value) { if (!!this.selectedItems) this.value = []; let temp = []; this.selectedItems.forEach(item => { const checkbox = item.querySelector('input[type=checkbox]'); temp = [...temp, checkbox.value]; }); this.value = temp; this.placeholder = this.selectedItems.length != 1 ? 'Selected Items' : 'Selected Item'; } else if (this.filter && this.minimumInputLength && !this.value && !this.selectedLabel) { this.placeholder = 'Begin typing to search'; } if (this.multiple && !!this.value) { if (typeof this.value == "string") this.value = this.value.split(","); this.selectedItemCount = this.value.length; } if (typeof this.sx == 'string') { this.sx = JSON.parse(this.sx) || {}; } setCSSProps(this.host, Object.assign({}, this.sx)); } componentDidLoad() { var _a; this.attachedButtonStartWidth = this.attachedButtonStart ? this.attachedButtonStart.offsetWidth : 0; this.attachedButtonEndWidth = this.attachedButtonEnd ? this.attachedButtonEnd.offsetWidth : 0; setCSSProps(this.host, { "--cbp-dropdown-attached-button-start-width": `${this.attachedButtonStartWidth}px`, "--cbp-dropdown-attached-button-end-width": `${this.attachedButtonEndWidth}px`, }); this.dropdownItems = Array.from(this.host.querySelectorAll('cbp-dropdown-item')); this.selectedItems = Array.from(this.host.querySelectorAll('cbp-dropdown-item[selected]')); if (!this.multiple) { if (!this.value || !this.selectedLabel) { if (this.selectedItems.length > 0) { this.value = this.selectedItems[0].value || this.selectedItems[0].innerText.trim(); this.selectedLabel = (_a = this.selectedItems[0]) === null || _a === void 0 ? void 0 : _a.innerText.trim(); } } } if (!this.selectedItems.length && !!this.value) { this.setSelectedFromValue(); } } componentWillRender() { if (this.attachedButtonStart) this.attachedButtonStart.disabled = this.disabled || !this.dropdownItems.length; if (this.attachedButtonEnd) this.attachedButtonEnd.disabled = this.disabled || !this.dropdownItems.length; } componentDidRender() { if (this.items) { this.dropdownItems = Array.from(this.host.querySelectorAll('cbp-dropdown-item')); if (this.async) { this.matches = []; for (let i = 0; i < this.dropdownItems.length; i++) { this.matches = [...this.matches, i]; } } } } render() { if (this.multiple) { this.selectedItemCount = this.value.length; } return (h(Host, { key: 'b24bd618fd9ff09a447b6203acc55d61ec326c68' }, h("div", { key: '384676899a6e9342a463816ebb02cd167d9c6998', class: "cbp-dropdown-shrinkwrap" }, h("slot", { key: 'a99db5f3a4866f48300008ae1b88b816f2b860cf', name: "cbp-dropdown-attached-button-start" }), h("button", { key: 'f6b3782cd60d3f697d33b36cd4135d3c7a32b57c', type: "button", class: "cbp-custom-form-control", id: this.fieldId, role: "combobox", "aria-controls": `${this.fieldId}-menu`, "aria-expanded": `${this.open}`, "aria-haspopup": "listbox", "aria-invalid": this.error ? 'true' : false, disabled: this.disabled || this.readonly || ((!this.async) && (!this.items) && (this.dropdownItems.length < 1)), onClick: (e) => this.handleDropdownClick(e), onKeyDown: e => this.getActionFromKey(e), ref: el => (this.control = el) }, (this.selectedLabel || (this.filter && this.searchString)) ? h("div", { class: "cbp-dropdown-label" }, this.filter && this.searchString ? this.searchString : this.selectedLabel) : h("div", { class: "cbp-dropdown-placeholder" }, this.multiple && (h("span", { role: "button", tabindex: 0, class: "cbp-dropdown-multiselect-counter", title: `Click to clear selections`, onClick: e => this.handleCounterClick(e), onKeyDown: e => this.handleCounterKeydown(e), ref: el => (this.counterControl = el) }, this.selectedItemCount, h("cbp-icon", { name: "circle-xmark", size: "var(--cbp-space-3x)", sx: { 'margin-inline-start': 'var(--cbp-space-2x)' } }))), this.placeholder)), h("slot", { key: '8ff00cbc93bebb736ef811924b93fdd72b64970c', name: "cbp-dropdown-attached-button-end" }), h("input", { key: '2a81f369e311b2a2bab07732752dcaf42947df0f', type: "hidden", id: `${this.fieldId}-field`, name: `${this.name}`, value: `${this.value}`, disabled: this.disabled, ref: el => (this.formField = el) }), h("div", { key: '2492e274077d430a31f838abb845c661470a6bf6', role: "listbox", class: "cbp-dropdown-menu", tabIndex: -1, id: `${this.fieldId}-menu`, ref: el => (this.listbox = el) }, !!this.items ? [...this.generatedItems] : h("slot", { onSlotchange: (e) => this.handleSlotChange(e) }))))); } static get is() { return "cbp-dropdown"; } static get originalStyleUrls() { return { "$": ["cbp-dropdown.scss"] }; } static get styleUrls() { return { "$": ["cbp-dropdown.css"] }; } static get properties() { return { "multiple": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies whether multiple selections are supported, in which case checkboxes shall be slotted in accordance with the design system specified pattern. Defaults to false, which renders a single-select dropdown." }, "attribute": "multiple", "reflect": true, "defaultValue": "false" }, "filter": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies whether the dropdown accepts key presses to filter results, enabling combobox functionality." }, "attribute": "filter", "reflect": true, "defaultValue": "false" }, "async": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Indicates that the filtering will be performed by asyncronous calls (handled by application logic)." }, "attribute": "async", "reflect": true, "defaultValue": "false" }, "minimumInputLength": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the number of characters need to emit an event to make an API call and return filtered results. This property is only used when" }, "attribute": "minimum-input-length", "reflect": false }, "items": { "type": "string", "mutable": false, "complexType": { "original": "string | object", "resolved": "object | string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "A JSON object (or stringified JSON) containing an array of labels and values. Labels may contain markup as needed, but in such cases, a value should always be specified explicitly." }, "attribute": "items", "reflect": false }, "fieldId": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Optionally specify the ID of the visible control here, which is used to generate related pattern node IDs and associate everything for accessibility." }, "attribute": "field-id", "reflect": false, "defaultValue": "createNamespaceKey('cbp-dropdown')" }, "name": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the name of the (hidden) form field" }, "attribute": "name", "reflect": false, "defaultValue": "this.fieldId" }, "placeholder": { "type": "string", "mutable": true, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Represents placeholder text on the dropdown control, displayed in a distinctive style from the selected item. Defaults to \"Choose Item\". Has no effect on multi-selects, as the component manages this text." }, "attribute": "placeholder", "reflect": false, "defaultValue": "'Choose Item'" }, "selectedLabel": { "type": "string", "mutable": true, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the visible label on the dropdown control of the selected item. Primarily updated dynamically by the component." }, "attribute": "selected-label", "reflect": false }, "value": { "type": "any", "mutable": true, "complexType": { "original": "any", "resolved": "any", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the value of the hidden input holding the value (or barring one, the text label) of the selected item. Primarily updated dynamically by the component." }, "attribute": "value", "reflect": false }, "open": { "type": "boolean", "mutable": true, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies whether the dropdown menu is open/visible." }, "attribute": "open", "reflect": true, "defaultValue": "false" }, "error": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies that the field has an error (and sets aria-invalid accordingly). Primarily controlled by the parent `cbp-form-field` component." }, "attribute": "error", "reflect": true, "defaultValue": "false" }, "readonly": { "type": "boolean", "mutable": true, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies that the field is readonly. Primarily controlled by the parent `cbp-form-field` component." }, "attribute": "readonly", "reflect": true, "defaultValue": "false" }, "disabled": { "type": "boolean", "mutable": true, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies that the field is disabled. Primarily controlled by the parent `cbp-form-field` component." }, "attribute": "disabled", "reflect": true, "defaultValue": "false" }, "context": { "type": "string", "mutable": false, "complexType": { "original": "'light-inverts' | 'light-always' | 'dark-inverts' | 'dark-always'", "resolved": "\"dark-always\" | \"dark-inverts\" | \"light-always\" | \"light-inverts\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the context of the component as it applies to the visual design and whether it inverts when light/dark mode is toggled. Default behavior is \"light-inverts\" and does not have to be specified." }, "attribute": "context", "reflect": true }, "sx": { "type": "any", "mutable": false, "complexType": { "original": "any", "resolved": "any", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Supports adding inline styles as an object" }, "attribute": "sx", "reflect": false, "defaultValue": "{}" } }; } static get states() { return { "selectedItems": {}, "selectedItemCount": {}, "searchString": {} }; } static get events() { return [{ "method": "valueChange", "name": "valueChange", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "A custom event emitted when the click event occurs for either a rendered button or anchor/link." }, "complexType": { "original": "any", "resolved": "any", "references": {} } }, { "method": "populateCombobox", "name": "populateCombobox", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "A custom event emitted for asynchronous comboboxes (`async=true` and `filter=true`) and \nthe search string meets the `minimumInputLength` requirement. \nThis event can be listened for and the `items` (JSON) updated via application logic/service call\u00DF." }, "complexType": { "original": "any", "resolved": "any", "references": {} } }]; } static get methods() { return { "clearSelections": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global", "id": "global::Promise" } }, "return": "Promise<void>" }, "docs": { "text": "A public method to clear all selected items in a dropdown (single or multi-select).\nEmits the valueChange event afterward.", "tags": [] } } }; } static get elementRef() { return "host"; } static get watchers() { return [{ "propName": "open", "methodName": "watchOpen" }, { "propName": "value", "methodName": "watchValue" }, { "propName": "items", "methodName": "watchItems" }]; } static get listeners() { return [{ "name": "dropdownItemClick", "method": "handleDropdownItemClick", "target": undefined, "capture": false, "passive": false }]; } } //# sourceMappingURL=cbp-dropdown.js.map