UNPKG

@trimble-oss/moduswebcomponents

Version:

Modus Web Components is a modern, accessible UI library built with Stencil JS that provides reusable web components following Trimble's Modus design system. This updated version focuses on improved flexibility, enhanced theming options, comprehensive cust

277 lines (272 loc) 20 kB
'use strict'; var index = require('./index-Cr4UXaM1.js'); var searchSolid_icon = require('./search-solid.icon-DQLKmTI4.js'); var utils = require('./utils-C0e12S4M.js'); const modusWcAutocompleteCss = "modus-wc-autocomplete.modus-wc-autocomplete{display:inline-block;position:relative;width:100%}modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-input-label{padding-bottom:var(--modus-wc-spacing-sm)}modus-wc-autocomplete.modus-wc-autocomplete modus-wc-loader{display:flex;justify-content:center}modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-input-bordered{border-radius:var(--modus-wc-border-radius-md)}modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-multi-select{align-items:center;display:flex;flex-wrap:wrap;gap:0.5rem;padding:var(--modus-wc-spacing-xs);}modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-multi-select.modus-wc-autocomplete-multi-select--bordered{border:var(--modus-wc-border-width-xs) solid var(--modus-wc-color-gray-0);border-radius:var(--modus-wc-border-radius-md) var(--modus-wc-border-radius-md) 0 0}modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-multi-select .chip{align-items:center;background-color:var(--modus-wc-color-gray-1);border-radius:var(--modus-wc-border-radius-xl);display:flex}modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-multi-select .chip button i{font-size:0.75rem}modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-multi-select .chip .label{font-size:var(--modus-wc-font-size-md);padding:0 var(--modus-wc-spacing-xs)}modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-menu{border-radius:0 0 var(--modus-wc-border-radius-md) var(--modus-wc-border-radius-md);left:0;margin-top:var(--modus-wc-spacing-2xs);position:absolute;top:100%;z-index:99}modus-wc-autocomplete.modus-wc-autocomplete .menu-hidden{display:none}modus-wc-autocomplete.modus-wc-autocomplete .menu-visible{display:block}modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-no-results{padding:var(--modus-wc-spacing-md)}modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-no-results .icon-label{align-items:center;display:flex;justify-content:center}modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-no-results .icon-label .modus-wc-autocomplete-search-icon{color:var(--modus-wc-color-gray-6);height:1.5rem;width:1.5rem}modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-no-results .icon-label .label{font-size:var(--modus-wc-font-size-md);font-weight:var(--modus-wc-font-weight-bold);margin-inline-start:var(--modus-wc-spacing-xs)}modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-no-results .sub-label{color:var(--modus-wc-color-gray-7);font-size:var(--modus-wc-font-size-sm);text-align:center}[data-theme=modus-classic-light] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-multi-select--bordered,[data-theme=modus-classic-dark] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-multi-select--bordered{border:var(--modus-wc-border-width-xs) solid var(--modus-wc-color-gray-6)}[data-theme=modus-classic-light] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-multi-select--bordered:focus-within,[data-theme=modus-classic-dark] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-multi-select--bordered:focus-within{border-color:var(--modus-wc-color-highlight-blue);border-width:var(--modus-wc-border-width-sm)}[data-theme=modus-classic-light] modus-wc-autocomplete.modus-wc-autocomplete ul.modus-wc-menu,[data-theme=modus-classic-dark] modus-wc-autocomplete.modus-wc-autocomplete ul.modus-wc-menu{box-shadow:0 0 4px 0 rgba(54, 53, 69, 0.3)}[data-theme=modus-classic-light] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-input-disabled,[data-theme=modus-classic-light] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-text-input-readonly,[data-theme=modus-classic-dark] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-input-disabled,[data-theme=modus-classic-dark] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-text-input-readonly{background-color:var(--modus-wc-color-gray-10)}[data-theme=modus-classic-light] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-multi-select--disabled,[data-theme=modus-classic-dark] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-multi-select--disabled{cursor:not-allowed;opacity:0.6;user-select:none}[data-theme=modus-classic-light] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-multi-select--disabled .modus-wc-input-disabled,[data-theme=modus-classic-dark] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-multi-select--disabled .modus-wc-input-disabled{border:none}[data-theme=modus-classic-light] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-multi-select--readonly .modus-wc-text-input-readonly,[data-theme=modus-classic-dark] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-multi-select--readonly .modus-wc-text-input-readonly{border:none}[data-theme=modus-classic-light] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-multi-select--disabled,[data-theme=modus-classic-light] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-input-disabled,[data-theme=modus-classic-light] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-multi-select--readonly,[data-theme=modus-classic-light] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-text-input-readonly{background-color:var(--modus-wc-color-gray-0);border:none}[data-theme=modus-classic-light] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-menu{background-color:var(--modus-wc-color-white)}[data-theme=modus-classic-dark] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-autocomplete-no-results .sub-label{color:var(--modus-wc-color-white)}[data-theme=modus-classic-dark] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-menu{background-color:var(--modus-wc-color-gray-10)}[data-theme=modus-modern-light] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-menu{background-color:var(--modus-wc-color-white)}[data-theme=modus-modern-dark] modus-wc-autocomplete.modus-wc-autocomplete .modus-wc-menu{background-color:var(--modus-wc-color-gray-10)}"; const ModusWcAutocomplete = class { constructor(hostRef) { index.registerInstance(this, hostRef); this.chipRemove = index.createEvent(this, "chipRemove"); this.inputBlur = index.createEvent(this, "inputBlur"); this.inputChange = index.createEvent(this, "inputChange"); this.inputFocus = index.createEvent(this, "inputFocus"); this.itemSelect = index.createEvent(this, "itemSelect"); this.inheritedAttributes = {}; this.menuVisible = false; /** Indicates that the autocomplete should have a border. */ this.bordered = true; /** Custom CSS class to apply to host element. */ this.customClass = ''; /** * The debounce timeout in milliseconds. * Set to 0 to disable debouncing. */ this.debounceMs = 300; /** Whether the form control is disabled. */ this.disabled = false; /** Show the clear button within the input field. */ this.includeClear = false; /** Show the search icon within the input field. */ this.includeSearch = false; /** * The items to display in the menu. * Creating a new array of items will ensure proper component re-render. **/ this.items = []; /** Whether the menu should remain open after an item is selected. */ this.leaveMenuOpen = false; /** The minimum number of characters required to render the menu. */ this.minChars = 0; /** Whether the input allows multiple items to be selected. */ this.multiSelect = false; /** The content to display when no results are found. */ this.noResults = { ariaLabel: 'No results found', label: 'No results found', subLabel: 'Check spelling or try a different keyword', }; /** Text that appears in the form control when it has no value set. */ this.placeholder = ''; /** Whether the value is editable. */ this.readOnly = false; /** A value is required for the form to be submittable. */ this.required = false; /** Whether to show the menu whenever the input has focus, regardless of input value. */ this.showMenuOnFocus = false; /** The size of the autocomplete (input and menu). */ this.size = 'md'; /** A spinner that appears when set to true */ this.showSpinner = false; /** The value of the control. */ this.value = ''; this.handleBlur = (event) => { // Stop text input blur event from bubbling up event.stopPropagation(); // Hide menu after a short delay to allow for item selection // istanbul ignore next - TODO setTimeout(() => { const relatedTarget = event.detail.relatedTarget; // Only emit blur if focus is moving completely outside the autocomplete widget // If focus stays within the component (input -> menu item), don't emit blur if (!relatedTarget || !this.el.contains(relatedTarget)) { this.menuVisible = false; this.inputBlur.emit(event.detail); } }, 200); }; // istanbul ignore next - TODO this.handleMenuFocusout = (event) => { setTimeout(() => { const relatedTarget = event.detail.relatedTarget; // Only emit blur if focus is moving completely outside the autocomplete widget if (!relatedTarget || !this.el.contains(relatedTarget)) { this.menuVisible = false; this.inputBlur.emit(event.detail); } }, 200); }; this.handleChange = (event) => { const value = event.detail.target.value; // Show menu based on either showMenuOnFocus prop or minimum character threshold if (this.showMenuOnFocus) { this.menuVisible = true; } else { this.menuVisible = value.length >= this.minChars; } // Clear any existing timer // istanbul ignore next - TODO if (this.debounceTimer) { window.clearTimeout(this.debounceTimer); } // If debouncing is disabled, emit immediately // istanbul ignore next - TODO if (!this.debounceMs) { this.inputChange.emit(event.detail); return; } // Set up new debounce timer // istanbul ignore next - TODO this.debounceTimer = window.setTimeout(() => { this.inputChange.emit(event.detail); }, this.debounceMs); }; this.handleFocus = (event) => { if (this.showMenuOnFocus) { this.menuVisible = true; } this.inputFocus.emit(event.detail); }; // TODO - add code coverage once autocomplete is updated // istanbul ignore next this.handleItemSelect = (item) => { if (this.disabled || this.readOnly) return; this.menuVisible = !!this.leaveMenuOpen; this.itemSelect.emit(item); }; // TODO - add code coverage once chip component is implemented // istanbul ignore next this.handleChipRemove = (item) => { if (this.disabled || this.readOnly) { return; // Do nothing if the component is disabled } this.chipRemove.emit(item); }; this.handleOutsideClick = (event) => { if (!this.el.contains(event.target)) { this.menuVisible = false; // Close menu if click is outside } }; } handleMenuVisibilityChange() { if (this.disabled || this.readOnly) { this.menuVisible = false; // Close the menu immediately } } // istanbul ignore next - TODO disconnectedCallback() { // Clean up any existing debounce timer when component is destroyed if (this.debounceTimer) { window.clearTimeout(this.debounceTimer); } document.removeEventListener('click', this.handleOutsideClick); } componentWillLoad() { if (!this.el.ariaLabel) { this.el.ariaLabel = 'Autocomplete input'; } this.inheritedAttributes = utils.inheritAriaAttributes(this.el); document.addEventListener('click', this.handleOutsideClick); } getClasses() { const classList = ['modus-wc-autocomplete']; // The order CSS classes are added matters to CSS specificity if (this.customClass) classList.push(this.customClass); return classList.join(' '); } getMultiSelectClasses() { return [ 'modus-wc-autocomplete-multi-select', this.bordered && 'modus-wc-autocomplete-multi-select--bordered', this.disabled && 'modus-wc-autocomplete-multi-select--disabled', this.readOnly && 'modus-wc-autocomplete-multi-select--readonly', ] .filter(Boolean) .join(' '); } handleKeyDown(event) { var _a; if (!(event.target instanceof HTMLInputElement)) return; const input = event.target; switch (event.key) { case utils.KEY.ArrowDown: event.preventDefault(); // Show menu based on either showMenuOnFocus prop or minimum character threshold if (this.showMenuOnFocus || input.value.length >= this.minChars) { this.menuVisible = true; } break; case utils.KEY.Backspace: if (this.multiSelect && input.value.length === 0) { let selectedItems = []; if (this.items) { selectedItems = this.items.filter((item) => item.selected); } const lastSelectedItem = selectedItems[selectedItems.length - 1]; if (lastSelectedItem) { this.chipRemove.emit(lastSelectedItem); } } break; case utils.KEY.Escape: event.preventDefault(); this.menuVisible = false; break; case utils.KEY.Enter: event.preventDefault(); if (this.multiSelect) { let selectedItems = []; if (this.items) { selectedItems = this.items.filter((item) => item.selected); } const lastSelectedItem = selectedItems[selectedItems.length - 1]; if (lastSelectedItem) { this.itemSelect.emit(lastSelectedItem); } } else { const selectedItem = (_a = this.items) === null || _a === void 0 ? void 0 : _a.find((item) => item.selected); if (selectedItem) { this.itemSelect.emit(selectedItem); } } if (this.menuVisible && !this.leaveMenuOpen) { // Don't call input.blur() here as it will trigger unwanted blur events this.menuVisible = false; } break; } } renderNoResults() { var _a, _b, _c; return (index.h("div", { class: "modus-wc-autocomplete-no-results" }, index.h("div", { class: "icon-label", "aria-label": (_a = this.noResults) === null || _a === void 0 ? void 0 : _a.ariaLabel }, index.h(searchSolid_icon.SearchSolidIcon, { className: "modus-wc-autocomplete-search-icon" }), index.h("div", { class: "label" }, (_b = this.noResults) === null || _b === void 0 ? void 0 : _b.label)), index.h("div", { class: "sub-label" }, (_c = this.noResults) === null || _c === void 0 ? void 0 : _c.subLabel))); } render() { const getChips = () => { var _a; const selectedItems = (_a = this.items) === null || _a === void 0 ? void 0 : _a.filter((item) => item.selected); // TODO - use chip component // TODO - add code coverage once chip component is implemented // istanbul ignore next return (index.h(index.Fragment, null, selectedItems === null || selectedItems === void 0 ? void 0 : selectedItems.map((item) => (index.h("modus-wc-chip", { "aria-label": "Remove item button", label: item.label, "show-remove": true, size: "sm", disabled: this.disabled || this.readOnly, onChipRemove: () => this.handleChipRemove(item), variant: "filled" }))))); }; const getInput = () => (index.h("modus-wc-text-input", Object.assign({ bordered: this.bordered && !this.multiSelect, disabled: this.disabled, includeClear: this.includeClear, includeSearch: this.includeSearch, inputId: this.inputId, inputTabIndex: this.inputTabIndex, name: this.name, onInputBlur: this.handleBlur, onInputChange: this.handleChange, onInputFocus: this.handleFocus, placeholder: this.placeholder, readOnly: this.readOnly, required: this.required, size: this.size, value: this.value }, this.inheritedAttributes))); // TODO - to improve flexibility, allow users to pass their own `<modus-wc-menu-item>` elements // TODO - add code coverage once autocomplete is updated // istanbul ignore next const getMenuItems = () => { var _a, _b, _c, _d; if (this.showSpinner) { return (index.h("li", null, index.h("modus-wc-loader", { variant: "spinner", size: this.size }))); } const menuItems = ((_a = this.items) === null || _a === void 0 ? void 0 : _a.filter((item) => item.visibleInMenu)) || []; const noResults = ((_b = this.noResults) === null || _b === void 0 ? void 0 : _b.label) || ((_c = this.noResults) === null || _c === void 0 ? void 0 : _c.subLabel) || ((_d = this.noResults) === null || _d === void 0 ? void 0 : _d.ariaLabel); return (index.h(index.Fragment, null, (menuItems === null || menuItems === void 0 ? void 0 : menuItems.length) > 0 || !noResults ? menuItems.map((item) => (index.h("modus-wc-menu-item", { disabled: item.disabled, focused: item.focused, label: item.label, onItemSelect: () => this.handleItemSelect(item), selected: item.selected, value: item.value }))) : this.renderNoResults())); }; return (index.h(index.Host, { class: this.getClasses() }, this.label && (index.h("modus-wc-input-label", { forId: this.inputId, labelText: this.label, required: this.required, size: this.size })), this.multiSelect ? (index.h("div", { class: this.getMultiSelectClasses() }, getChips(), getInput())) : (index.h(index.Fragment, null, getInput())), index.h("modus-wc-menu", { "aria-label": "Autocomplete menu", bordered: this.bordered, class: this.menuVisible ? 'menu-visible' : ' menu-hidden', onMenuFocusout: this.handleMenuFocusout, size: this.size }, getMenuItems(), index.h("slot", { name: "menu-items" })))); } get el() { return index.getElement(this); } static get watchers() { return { "disabled": ["handleMenuVisibilityChange"], "readOnly": ["handleMenuVisibilityChange"] }; } }; ModusWcAutocomplete.style = modusWcAutocompleteCss; exports.modus_wc_autocomplete = ModusWcAutocomplete;