@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
1,208 lines (1,207 loc) • 67.2 kB
JavaScript
import { Fragment, h, Host, } from "@stencil/core";
import { SearchSolidIcon } from "../../icons/search-solid.icon";
import { handleShadowDOMStyles } from "../base-component";
import { createEffectiveIdResolver, inheritAriaAttributes, KEY, } from "../utils";
import { BLUR_FOCUSOUT_DELAY_MS, clearAllFocus, getClasses, getMultiSelectClasses, getVisibleItems, handleArrowDown as processArrowDown, handleArrowUp as processArrowUp, handleBackspace as processBackspace, processChipRemoval, processInputChange, processItemSelection, processKeyEvent, renderChips, renderClearButton, renderExpandCollapseButton, renderInput, renderMenuItems, renderMoreChipsIndicator, syncFilteredItems, updateItemFocus, } from "./modus-wc-autocomplete-core";
/**
* A customizable autocomplete component used to create searchable text inputs.
*
* The component supports a `<slot>` for injecting custom content.
*/
export class ModusWcAutocomplete {
constructor() {
this.menuVisible = false;
this.isChipsExpanded = false;
this.initialNavigation = true;
this.filteredItems = [];
this.selectionOrder = []; // Track order of chip selection
this.searchText = ''; // Dedicated state for active search query
this.showFeedback = true; // Track whether to show feedback
this.resolveEffectiveId = createEffectiveIdResolver();
this.inheritedAttributes = {};
this.programmaticOpen = false;
this.isNavigating = false; // Flag to prevent re-filtering during navigation
/** 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 = '';
/** Maximum number of chips to display. When exceeded, shows expand/collapse button. Set to -1 to disable limit. */
this.maxChips = -1;
/** Minimum width for the text input in pixels. When chips would make input smaller, container height increases instead. */
this.minInputWidth = 10;
this.handleFocusOutside = (event) => {
const relatedTarget = event.relatedTarget;
// Use composedPath() for Shadow DOM compatibility
const path = event.composedPath && event.composedPath();
const focusedInside = relatedTarget &&
(this.el.contains(relatedTarget) || (path && path.includes(this.el)));
if (!focusedInside) {
// Hide menu immediately to prevent flicker
if (!this.programmaticOpen) {
this.menuVisible = false;
this.showFeedback = true;
}
// Use setTimeout for cleanup and blur event
setTimeout(() => {
// Reset filtered items after menu is hidden
if (this.items) {
this.filteredItems = this.items.filter((item) => item.visibleInMenu);
}
this.inputBlur.emit(event);
}, BLUR_FOCUSOUT_DELAY_MS);
}
};
this.handleBlur = (event) => {
if (this.customBlur) {
this.customBlur(event.detail);
return;
}
event.stopPropagation();
this.initialNavigation = true;
if (this.items) {
this.items = [
...this.items.map((item) => (Object.assign(Object.assign({}, item), { focused: false }))),
];
}
this.handleFocusOutside(event.detail);
};
this.handleMenuFocusout = (event) => {
this.handleFocusOutside(event.detail);
};
this.handleChange = (event) => {
// Prevent the child text-input's immediate inputChange event
event.stopPropagation();
const result = processInputChange(event, {
disabled: this.disabled,
readOnly: this.readOnly,
customInputChange: this.customInputChange,
showMenuOnFocus: this.showMenuOnFocus,
minChars: this.minChars,
items: this.items,
multiSelect: this.multiSelect,
debounceMs: this.debounceMs,
});
if (!result.inputValue && !result.shouldShowMenu) {
return;
}
this.menuVisible = result.shouldShowMenu;
if (result.updatedItems) {
this.items = result.updatedItems;
}
this.value = result.inputValue;
this.searchText = result.inputValue; // Update search text as user types
// Hide feedback when typing and menu is visible
if (this.menuVisible) {
this.showFeedback = false;
}
// Sync filtered items based on new search value
this.syncFilteredItems();
if (result.shouldResetNavigation) {
this.initialNavigation = false;
}
// Handle immediate emit if no debounce
if (!this.debounceMs) {
this.inputChange.emit(event.detail);
}
else if (!this.customInputChange) {
// Handle debounced emit
if (this.debounceTimer) {
window.clearTimeout(this.debounceTimer);
}
this.debounceTimer = window.setTimeout(() => {
this.inputChange.emit(event.detail);
}, this.debounceMs);
}
};
this.handleFocus = (event) => {
var _a, _b;
// Prevent the child text-input's immediate inputFocus event
event.stopPropagation();
if (!this.disabled && !this.readOnly) {
// When focusing, clear searchText if value matches a selected item
// This prevents treating the display value as a search query
const hasSelectedItem = (_a = this.items) === null || _a === void 0 ? void 0 : _a.some((item) => item.selected && item.label === this.value);
if (hasSelectedItem) {
this.searchText = '';
}
// Show all items on focus
if (this.items) {
this.filteredItems = this.items.filter((item) => item.visibleInMenu);
}
if (this.showMenuOnFocus) {
this.menuVisible = true;
// Hide feedback when menu opens
this.showFeedback = false;
// Scroll to selected item when menu opens via mouse focus
if ((_b = this.items) === null || _b === void 0 ? void 0 : _b.some((item) => item.selected)) {
requestAnimationFrame(() => {
this.scrollToOptionSelected();
});
}
}
}
this.inputFocus.emit(event.detail);
};
this.handleItemSelectByValue = (value) => {
var _a;
if (this.disabled || this.readOnly)
return;
const currentItem = (_a = this.items) === null || _a === void 0 ? void 0 : _a.find((item) => item.value === value);
if (!currentItem)
return;
this.handleItemSelect(currentItem);
};
this.handleItemSelect = (item) => {
const result = processItemSelection(item, {
disabled: this.disabled,
readOnly: this.readOnly,
items: this.items,
multiSelect: this.multiSelect,
leaveMenuOpen: this.leaveMenuOpen,
selectionOrder: this.selectionOrder,
maxChips: this.maxChips,
customItemSelect: this.customItemSelect,
});
const isSelected = item.selected;
if (this.multiSelect && isSelected && item.checkbox) {
this.handleChipRemove(item);
}
if (result.updatedItems && result.updatedItems !== this.items) {
this.items = result.updatedItems;
}
if (result.updatedValue !== undefined) {
this.value = result.updatedValue;
}
if (result.updatedSelectionOrder) {
this.selectionOrder = result.updatedSelectionOrder;
}
if (result.shouldExpandChips) {
this.isChipsExpanded = true;
}
if (result.shouldCloseMenu) {
this.menuVisible = false;
this.showFeedback = true;
}
// Clear search text after selection - this is critical to prevent state ambiguity
this.searchText = '';
// Reset filtered items to show all items after selection
if (this.items) {
this.filteredItems = this.items.filter((item) => item.visibleInMenu);
}
// Only emit event and update navigation if not disabled/readonly
if (!this.disabled && !this.readOnly && this.items) {
this.initialNavigation = true;
this.itemSelect.emit(item);
// Scroll to selected option after selection
if (!this.multiSelect && this.menuVisible) {
this.scrollToOptionSelected();
}
}
};
this.handleChipRemove = (item) => {
const result = processChipRemoval(item, {
disabled: this.disabled,
readOnly: this.readOnly,
items: this.items,
selectionOrder: this.selectionOrder,
});
if (result.updatedItems) {
this.items = result.updatedItems;
this.selectionOrder = result.updatedSelectionOrder;
// When removing chips, show all items instead of applying text filtering
this.filteredItems = this.items.filter((item) => item.visibleInMenu);
}
// Emit event for external handlers who want to know about the removal
if (!this.disabled && !this.readOnly) {
this.chipRemove.emit(item);
}
};
this.handleClearAll = (event) => {
// This method is called by the text input and a button, stop propagation of the text input event
// istanbul ignore next
event === null || event === void 0 ? void 0 : event.stopPropagation();
void this.clearInput();
this.clearClick.emit();
};
this.toggleChipsExpansion = () => {
if (this.leaveMenuOpen && this.isChipsExpanded) {
this.menuVisible = false;
this.showFeedback = true;
}
this.isChipsExpanded = !this.isChipsExpanded;
this.chipsExpansionChange.emit({ expanded: this.isChipsExpanded });
};
this.handleOutsideClick = (event) => {
// Use composedPath() for Shadow DOM compatibility
const path = event.composedPath();
const clickedInside = path.includes(this.el);
if (!clickedInside && !this.programmaticOpen) {
this.menuVisible = false;
this.showFeedback = true;
this.isChipsExpanded = false;
}
// Reset programmaticOpen flag after handling the click
if (this.programmaticOpen) {
this.programmaticOpen = false;
}
};
}
handleMenuVisibilityChange() {
if (this.disabled || this.readOnly) {
this.menuVisible = false;
this.showFeedback = true;
}
}
handleItemsChange(newItems, oldItems) {
// Only sync filtered items if items actually changed (not just focus updates)
// and we're not currently navigating
if (this.items &&
!this.isNavigating &&
JSON.stringify(newItems === null || newItems === void 0 ? void 0 : newItems.map((i) => ({
value: i.value,
selected: i.selected,
focused: i.focused,
}))) !==
JSON.stringify(oldItems === null || oldItems === void 0 ? void 0 : oldItems.map((i) => ({
value: i.value,
selected: i.selected,
focused: i.focused,
})))) {
if (this.multiSelect) {
// Keep items in selectionOrder that are still selected
const stillSelectedValues = this.selectionOrder.filter((value) => newItems.some((item) => item.value === value && item.selected));
// Add any newly selected items that aren't already in selectionOrder
const newlySelectedValues = newItems
.filter((item) => item.selected && !stillSelectedValues.includes(item.value))
.map((item) => item.value);
// Preserve the original selection order and append new selections
this.selectionOrder = [...stillSelectedValues, ...newlySelectedValues];
}
this.syncFilteredItems();
}
}
componentWillLoad() {
// Auto-inject CSS if component is used inside user's shadow DOM
handleShadowDOMStyles(this.el);
if (!this.el.ariaLabel) {
this.el.ariaLabel = 'Autocomplete input';
}
this.inheritedAttributes = inheritAriaAttributes(this.el);
document.addEventListener('click', this.handleOutsideClick);
if (this.items) {
this.filteredItems = [...this.items];
// Initialize selection order for pre-selected items
this.selectionOrder = this.items
.filter((item) => item.selected)
.map((item) => item.value);
}
}
disconnectedCallback() {
if (this.debounceTimer) {
window.clearTimeout(this.debounceTimer);
}
document.removeEventListener('click', this.handleOutsideClick);
}
getClasses() {
return getClasses(this.customClass);
}
getMultiSelectClasses() {
return getMultiSelectClasses({
bordered: this.bordered,
disabled: this.disabled,
feedback: this.showFeedback || this.menuVisible ? this.feedback : undefined,
readOnly: this.readOnly,
size: this.size,
});
}
getVisibleItems() {
return getVisibleItems(this.filteredItems);
}
syncFilteredItems() {
this.filteredItems = syncFilteredItems(this.items, this.searchText, this.leaveMenuOpen, this.customInputChange);
}
updateItemFocus(targetValue) {
this.isNavigating = true; // Prevent items watcher from re-filtering
const updated = updateItemFocus(this.items, targetValue);
if (updated) {
this.items = updated;
// We need to update filteredItems to reflect the focus change
// But only if we're actively filtering
if (this.searchText) {
this.syncFilteredItems();
}
else {
// When not filtering, update filteredItems to reflect the focus change
// without applying any filter
this.filteredItems = this.items.filter((item) => item.visibleInMenu);
}
}
this.isNavigating = false; // Reset flag
}
clearAllFocus() {
const updated = clearAllFocus(this.items);
if (updated) {
this.items = updated;
// When clearing focus (e.g., on Escape), show all items instead of filtered
this.filteredItems = this.items.filter((item) => item.visibleInMenu);
}
}
// Shared scroll engine for both "open with a preselected option" and
// "arrow-key navigation". DOM focus stays on the input, so the browser's
// focus-driven auto-scroll never fires; the autocomplete drives it.
//
// alignment:
// 'top' — always top-align the item (used when opening on a selection).
// 'auto' — top-align when above the viewport, bottom-align when below,
// so consecutive arrow presses reveal one row at a time.
scrollMenuItemIntoView(itemSelector, alignment, behavior) {
requestAnimationFrame(() => {
const menuEl = this.el.querySelector('modus-wc-menu');
if (!menuEl)
return;
const targetItem = menuEl.querySelector(itemSelector);
const scrollContainer = menuEl.querySelector('.modus-wc-menu');
if (!targetItem || !scrollContainer)
return;
const containerRect = scrollContainer.getBoundingClientRect();
const itemRect = targetItem.getBoundingClientRect();
const isAboveView = itemRect.top < containerRect.top;
const isBelowView = itemRect.bottom > containerRect.bottom;
if (!isAboveView && !isBelowView)
return;
const scrollTop = alignment === 'auto' && isBelowView
? targetItem.offsetTop +
targetItem.offsetHeight -
scrollContainer.clientHeight
: targetItem.offsetTop;
scrollContainer.scrollTo({
top: Math.max(0, scrollTop),
behavior,
});
});
}
scrollToOptionSelected() {
if (this.multiSelect)
return;
this.scrollMenuItemIntoView('.modus-wc-menu-item-active', 'top', 'smooth');
}
scrollFocusedIntoView() {
this.scrollMenuItemIntoView('.modus-wc-menu-item-focused', 'auto', 'auto');
}
handleArrowDown() {
const input = this.el.querySelector('input');
if (!input)
return;
// Check if we're in filtering mode based on searchText BEFORE clearing it
const wasFiltering = this.searchText.length > 0;
// Capture whether the menu was already open so we only auto-scroll to the
// selected item on the open transition (not on every subsequent arrow press).
const wasMenuVisible = this.menuVisible;
if (this.initialNavigation) {
if (this.searchText) {
this.searchText = '';
}
// Reset filtered items when initial navigation to ensure all items are shown
if (this.items) {
this.filteredItems = this.items.filter((item) => item.visibleInMenu);
}
}
processArrowDown({
showMenuOnFocus: this.showMenuOnFocus,
minChars: this.minChars,
inputValue: input.value,
initialNavigation: this.initialNavigation,
visibleItems: this.getVisibleItems(),
onUpdateFocus: (value) => {
this.updateItemFocus(value);
// After updating focus, if not filtering, ensure we show all items
if (!wasFiltering && !this.searchText && this.items) {
this.filteredItems = this.items.filter((item) => item.visibleInMenu);
}
this.scrollFocusedIntoView();
},
onSetMenuVisible: (visible) => {
var _a;
this.menuVisible = visible;
// Only scroll to the selected item on the hidden->visible transition.
// Otherwise repeated arrow-down presses would keep snapping the menu
// back to the selected item and fight scrollFocusedIntoView().
if (visible &&
!wasMenuVisible &&
((_a = this.items) === null || _a === void 0 ? void 0 : _a.some((item) => item.selected))) {
this.scrollToOptionSelected();
this.showFeedback = false;
}
else if (visible) {
this.showFeedback = false;
}
},
onSetInitialNavigation: (value) => (this.initialNavigation = value),
});
}
handleArrowUp() {
// Check if we're in filtering mode based on searchText
const isFiltering = this.searchText.length > 0;
processArrowUp({
initialNavigation: this.initialNavigation,
visibleItems: this.getVisibleItems(),
onUpdateFocus: (value) => {
this.updateItemFocus(value);
// After updating focus, if not filtering, ensure we show all items
if (!isFiltering && this.items) {
this.filteredItems = this.items.filter((item) => item.visibleInMenu);
}
this.scrollFocusedIntoView();
},
onSetInitialNavigation: (value) => (this.initialNavigation = value),
});
}
handleEscape() {
this.clearAllFocus();
this.initialNavigation = true;
this.menuVisible = false;
this.searchText = ''; // Clear search text on escape
}
handleEnter() {
var _a, _b;
const visibleItems = this.getVisibleItems();
const focusedItem = visibleItems.find((item) => item.focused);
if (focusedItem) {
this.handleItemSelect(focusedItem);
}
else if (this.multiSelect) {
const selectedItems = ((_a = this.items) === null || _a === void 0 ? void 0 : _a.filter((item) => item.selected)) || [];
const lastSelectedItem = selectedItems[selectedItems.length - 1];
if (lastSelectedItem) {
this.itemSelect.emit(lastSelectedItem);
}
}
else {
const selectedItem = (_b = this.items) === null || _b === void 0 ? void 0 : _b.find((item) => item.selected);
if (selectedItem) {
this.itemSelect.emit(selectedItem);
}
}
}
handleBackspace(input) {
processBackspace(input, {
multiSelect: this.multiSelect,
selectionOrder: this.selectionOrder,
items: this.items,
onChipRemove: (item) => this.handleChipRemove(item),
});
// Hide feedback when backspace is pressed and menu is visible
if (this.menuVisible) {
this.showFeedback = false;
}
}
handleKeyDown(event) {
const { handled, keyLower } = processKeyEvent(event, {
disabled: this.disabled,
readOnly: this.readOnly,
customKeyDown: this.customKeyDown,
});
if (handled)
return;
const input = event.target;
switch (keyLower) {
case KEY.ArrowDown.toLowerCase(): {
this.handleArrowDown();
break;
}
case KEY.ArrowUp.toLowerCase(): {
this.handleArrowUp();
break;
}
case KEY.Escape.toLowerCase(): {
this.handleEscape();
break;
}
case KEY.Enter.toLowerCase(): {
this.handleEnter();
break;
}
case KEY.Backspace.toLowerCase(): {
this.handleBackspace(input);
break;
}
}
}
/**
* Programmatically select an item
*/
async selectItem(item) {
if (item) {
this.handleItemSelect(item);
}
else {
this.selectionOrder = []; // Clear selection order
if (this.items) {
this.items = [
...this.items.map((menuItem) => (Object.assign(Object.assign({}, menuItem), { selected: false }))),
];
}
this.value = '';
this.searchText = ''; // Clear search text when clearing selection
}
return Promise.resolve();
}
/**
* Programmatically open the menu
*/
async openMenu() {
this.programmaticOpen = true;
this.menuVisible = true;
return Promise.resolve();
}
/**
* Programmatically close the menu
*/
async closeMenu() {
this.programmaticOpen = false;
this.menuVisible = false;
this.showFeedback = true;
return Promise.resolve();
}
/**
* Programmatically toggle the menu open/closed
*/
async toggleMenu() {
if (!this.menuVisible) {
this.programmaticOpen = true;
}
else {
this.programmaticOpen = false;
}
this.menuVisible = !this.menuVisible;
return Promise.resolve();
}
/**
* Programmatically set focus to input
*/
async focusInput() {
const inputElement = this.el.querySelector('input');
if (inputElement) {
inputElement.focus();
}
return Promise.resolve();
}
/**
* Clear the input value and reset items
*/
async clearInput() {
this.value = '';
this.searchText = ''; // Clear search text as well
this.selectionOrder = []; // Clear selection order
if (this.items) {
this.items = [
...this.items.map((item) => (Object.assign(Object.assign({}, item), { selected: false }))),
];
this.filteredItems = [...this.items];
}
return Promise.resolve();
}
render() {
const effectiveId = this.resolveEffectiveId(this.inputId);
// Set CSS custom properties for dynamic min-width control
const minWidth = this.minInputWidth || 10;
const cssVariables = {
'--modus-autocomplete-min-input-width': `${minWidth}px`,
};
// Check if we have slotted content
const hasSlottedMenuItems = !!this.el.querySelector('[slot="menu-items"]');
const hasSlottedCustomIcon = !!this.el.querySelector('[slot="custom-icon"]');
return (h(Host, { key: '98f322082fb68d3186773e0ea041435a412524b6', class: this.getClasses(), style: cssVariables }, this.label && (h("modus-wc-input-label", { key: 'd779f21d83c763d2d03a897c7bdb548b5f5ac0ca', customClass: this.feedback && (this.showFeedback || this.menuVisible)
? `modus-wc-input-label--${this.feedback.level}`
: '', forId: effectiveId, labelText: this.label, required: this.required, size: this.size })), this.multiSelect ? (h("div", { class: this.getMultiSelectClasses() }, this.includeSearch && (h(SearchSolidIcon, { className: "modus-wc-autocomplete-search-icon" })), h("div", { class: "modus-wc-autocomplete-content" }, renderChips({
selectionOrder: this.selectionOrder,
items: this.items,
isChipsExpanded: this.isChipsExpanded,
maxChips: this.maxChips,
disabled: this.disabled,
readOnly: this.readOnly,
onChipRemove: (item) => this.handleChipRemove(item),
}), renderMoreChipsIndicator({
selectionOrder: this.selectionOrder,
maxChips: this.maxChips,
isChipsExpanded: this.isChipsExpanded,
}), renderInput({
autoComplete: this.autoComplete,
bordered: this.bordered,
multiSelect: this.multiSelect,
disabled: this.disabled,
feedback: this.showFeedback || this.menuVisible
? this.feedback
: undefined,
includeClear: this.includeClear,
includeSearch: this.includeSearch,
inputId: effectiveId,
inputTabIndex: this.inputTabIndex,
name: this.name,
placeholder: this.placeholder,
readOnly: this.readOnly,
required: this.required,
size: this.size,
value: this.value,
inheritedAttributes: this.inheritedAttributes,
customIconSlot: hasSlottedCustomIcon,
onBlur: this.handleBlur,
onChange: this.handleChange,
onFocus: this.handleFocus,
})), h("div", { class: "modus-wc-autocomplete-button-container" }, renderClearButton({
includeClear: this.includeClear,
disabled: this.disabled,
readOnly: this.readOnly,
selectionOrder: this.selectionOrder,
value: this.value,
onClearAll: this.handleClearAll,
}), renderExpandCollapseButton({
selectionOrder: this.selectionOrder,
maxChips: this.maxChips,
isChipsExpanded: this.isChipsExpanded,
disabled: this.disabled,
readOnly: this.readOnly,
onToggleExpansion: this.toggleChipsExpansion,
})))) : (h(Fragment, null, renderInput({
autoComplete: this.autoComplete,
bordered: this.bordered,
multiSelect: this.multiSelect,
disabled: this.disabled,
feedback: undefined,
customClass: this.feedback && (this.showFeedback || this.menuVisible)
? `modus-wc-input--${this.feedback.level}`
: '',
includeClear: this.includeClear,
includeSearch: this.includeSearch,
inputId: effectiveId,
inputTabIndex: this.inputTabIndex,
name: this.name,
placeholder: this.placeholder,
readOnly: this.readOnly,
required: this.required,
size: this.size,
value: this.value,
inheritedAttributes: this.inheritedAttributes,
customIconSlot: hasSlottedCustomIcon,
onBlur: this.handleBlur,
onChange: this.handleChange,
onClear: this.handleClearAll,
onFocus: this.handleFocus,
}))), !this.multiSelect && this.feedback && this.showFeedback && (h("modus-wc-input-feedback", { key: 'd02769c9e2e353e60667ff5e50501655b9a37f5d', level: this.feedback.level, message: this.feedback.message, size: this.size })), hasSlottedMenuItems ? (
// When using custom slots, keep menu in DOM and use CSS to hide/show
h("modus-wc-menu", { "aria-label": "Autocomplete menu", bordered: this.bordered, class: this.menuVisible ? 'menu-visible' : 'menu-hidden', onMenuFocusout: this.handleMenuFocusout, onMouseDown: (e) => e.preventDefault(), size: this.size }, renderMenuItems({
showSpinner: this.showSpinner,
size: this.size,
filteredItems: this.filteredItems,
items: this.items,
noResults: this.noResults,
hasSlottedContent: hasSlottedMenuItems,
onItemSelect: this.handleItemSelectByValue,
}), h("slot", { name: "menu-items" }))) : (
// When NOT using slots, conditionally render menu (automatic scroll reset)
this.menuVisible && (h("modus-wc-menu", { "aria-label": "Autocomplete menu", bordered: this.bordered, class: "menu-visible", onMenuFocusout: this.handleMenuFocusout, onMouseDown: (e) => e.preventDefault(), size: this.size }, renderMenuItems({
showSpinner: this.showSpinner,
size: this.size,
filteredItems: this.filteredItems,
items: this.items,
noResults: this.noResults,
hasSlottedContent: hasSlottedMenuItems,
onItemSelect: this.handleItemSelectByValue,
}), h("slot", { name: "menu-items" })))), this.multiSelect && this.feedback && this.showFeedback && (h("modus-wc-input-feedback", { key: '442e65de40426493f3aecea0a271cb6efe5d2a5b', level: this.feedback.level, message: this.feedback.message, size: this.size }))));
}
static get is() { return "modus-wc-autocomplete"; }
static get originalStyleUrls() {
return {
"$": ["modus-wc-autocomplete.scss"]
};
}
static get styleUrls() {
return {
"$": ["modus-wc-autocomplete.css"]
};
}
static get properties() {
return {
"autoComplete": {
"type": "string",
"attribute": "auto-complete",
"mutable": false,
"complexType": {
"original": "AutocompleteTypes",
"resolved": "AutocompleteTypes | undefined",
"references": {
"AutocompleteTypes": {
"location": "import",
"path": "../types",
"id": "src/components/types.ts::AutocompleteTypes"
}
}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Hint for form autofill feature."
},
"getter": false,
"setter": false,
"reflect": false
},
"bordered": {
"type": "boolean",
"attribute": "bordered",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Indicates that the autocomplete should have a border."
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "true"
},
"customClass": {
"type": "string",
"attribute": "custom-class",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Custom CSS class to apply to host element."
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "''"
},
"debounceMs": {
"type": "number",
"attribute": "debounce-ms",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "The debounce timeout in milliseconds.\nSet to 0 to disable debouncing."
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "300"
},
"disabled": {
"type": "boolean",
"attribute": "disabled",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Whether the form control is disabled."
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "false"
},
"feedback": {
"type": "unknown",
"attribute": "feedback",
"mutable": false,
"complexType": {
"original": "IInputFeedbackProp",
"resolved": "IInputFeedbackProp | undefined",
"references": {
"IInputFeedbackProp": {
"location": "import",
"path": "../types",
"id": "src/components/types.ts::IInputFeedbackProp"
}
}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Feedback state for the input field."
},
"getter": false,
"setter": false
},
"includeClear": {
"type": "boolean",
"attribute": "include-clear",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Show the clear button within the input field."
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "false"
},
"includeSearch": {
"type": "boolean",
"attribute": "include-search",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Show the search icon within the input field."
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "false"
},
"inputId": {
"type": "string",
"attribute": "input-id",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "The ID of the input element."
},
"getter": false,
"setter": false,
"reflect": false
},
"inputTabIndex": {
"type": "number",
"attribute": "input-tab-index",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Determine the control's relative ordering for sequential focus navigation (typically with the Tab key)."
},
"getter": false,
"setter": false,
"reflect": false
},
"items": {
"type": "unknown",
"attribute": "items",
"mutable": false,
"complexType": {
"original": "IAutocompleteItem[]",
"resolved": "IAutocompleteItem[] | undefined",
"references": {
"IAutocompleteItem": {
"location": "import",
"path": "../types",
"id": "src/components/types.ts::IAutocompleteItem"
}
}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "The items to display in the menu.\nCreating a new array of items will ensure proper component re-render."
},
"getter": false,
"setter": false,
"defaultValue": "[]"
},
"label": {
"type": "string",
"attribute": "label",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "The text to display within the label."
},
"getter": false,
"setter": false,
"reflect": false
},
"leaveMenuOpen": {
"type": "boolean",
"attribute": "leave-menu-open",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Whether the menu should remain open after an item is selected."
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "false"
},
"minChars": {
"type": "number",
"attribute": "min-chars",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "The minimum number of characters required to render the menu."
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "0"
},
"multiSelect": {
"type": "boolean",
"attribute": "multi-select",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Whether the input allows multiple items to be selected."
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "false"
},
"name": {
"type": "string",
"attribute": "name",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Name of the form control. Submitted with the form as part of a name/value pair."
},
"getter": false,
"setter": false,
"reflect": false
},
"noResults": {
"type": "unknown",
"attribute": "no-results",
"mutable": false,
"complexType": {
"original": "IAutocompleteNoResults",
"resolved": "IAutocompleteNoResults | undefined",
"references": {
"IAutocompleteNoResults": {
"location": "import",
"path": "../types",
"id": "src/components/types.ts::IAutocompleteNoResults"
}
}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "The content to display when no results are found."
},
"getter": false,
"setter": false,
"defaultValue": "{\n ariaLabel: 'No results found',\n label: 'No results found',\n subLabel: 'Check spelling or try a different keyword',\n }"
},
"placeholder": {
"type": "string",
"attribute": "placeholder",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Text that appears in the form control when it has no value set."
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "''"
},
"readOnly": {
"type": "boolean",
"attribute": "read-only",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Whether the value is editable."
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "false"
},
"required": {
"type": "boolean",
"attribute": "required",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "A value is required for the form to be submittable."
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "false"
},
"showMenuOnFocus": {
"type": "boolean",
"attribute": "show-menu-on-focus",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean | undefined",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Whether to show the menu whenever the input has focus, regardless of input value."
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "false"
},
"size": {
"type": "string",
"attribute": "size",
"mutable": false,
"complexType": {
"original": "ModusSize",