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

360 lines (359 loc) 16.3 kB
import { Fragment, h } from "@stencil/core"; import { CloseSolidIcon } from "../../icons/close-solid.icon"; import { SearchSolidIcon } from "../../icons/search-solid.icon"; import { KEY } from "../utils"; // Timeout constants for consistent behavior export const BLUR_FOCUSOUT_DELAY_MS = 200; // Delay before handling blur/focusout to allow related element focus export function getClasses(customClass) { const classList = ['modus-wc-autocomplete']; if (customClass) classList.push(customClass); return classList.join(' '); } export function getMultiSelectClasses(props) { return [ 'modus-wc-autocomplete-multi-select', 'modus-wc-input', 'modus-wc-w-full', 'modus-wc-flex', 'modus-wc-items-center', 'modus-wc-gap-1', props.bordered && 'modus-wc-input-bordered', props.disabled && 'modus-wc-input-disabled', props.readOnly && 'modus-wc-text-input--readonly', props.size && `modus-wc-input-${props.size}`, props.bordered && 'modus-wc-autocomplete-multi-select--bordered', props.disabled && 'modus-wc-autocomplete-multi-select--disabled', props.readOnly && 'modus-wc-autocomplete-multi-select--readonly', ] .filter(Boolean) .join(' '); } export function getVisibleItems(filteredItems) { return (filteredItems === null || filteredItems === void 0 ? void 0 : filteredItems.filter((item) => !item.disabled)) || []; } export function syncFilteredItems(items, value, leaveMenuOpen, customInputChange) { if (!items) { return []; } // When leaveMenuOpen is true and an item is selected, show all items if (leaveMenuOpen && items.some((item) => item.selected)) { return [...items]; } // if customInputChange is defined, return items that are visibleInMenu if (customInputChange) { return items.filter((item) => item.visibleInMenu); } const currentSearchText = (value === null || value === void 0 ? void 0 : value.toLowerCase()) || ''; if (currentSearchText === '') { // When no search text, show all items that are visibleInMenu return items.filter((item) => item.visibleInMenu); } else { // Filter items based on current search text AND visibleInMenu return items.filter((item) => item.visibleInMenu && item.label.toLowerCase().includes(currentSearchText)); } } export function updateItemFocus(items, targetValue) { if (!items) return []; return [ ...items.map((item) => (Object.assign(Object.assign({}, item), { focused: item.value === targetValue }))), ]; } export function clearAllFocus(items) { if (!items) return []; return [ ...items.map((item) => (Object.assign(Object.assign({}, item), { focused: false }))), ]; } export function handleArrowDown(params) { const { showMenuOnFocus, minChars, inputValue, initialNavigation, visibleItems, onUpdateFocus, onSetMenuVisible, onSetInitialNavigation, } = params; if (showMenuOnFocus || inputValue.length >= minChars) { onSetMenuVisible(true); } if (initialNavigation) { onSetInitialNavigation(false); return; } const currentIndex = visibleItems.findIndex((item) => item.focused); const nextIndex = currentIndex < 0 ? 0 : Math.min(currentIndex + 1, visibleItems.length - 1); if (nextIndex >= 0 && nextIndex < visibleItems.length && visibleItems[nextIndex]) { const item = visibleItems[nextIndex]; if (item && item.value) { onUpdateFocus(item.value); } } } export function handleArrowUp(params) { const { initialNavigation, visibleItems, onUpdateFocus, onSetInitialNavigation, } = params; if (initialNavigation) { onSetInitialNavigation(false); return; } const currentIndex = visibleItems.findIndex((item) => item.focused); const prevIndex = currentIndex < 0 ? visibleItems.length - 1 : Math.max(currentIndex - 1, 0); if (prevIndex >= 0 && prevIndex < visibleItems.length && visibleItems[prevIndex]) { const item = visibleItems[prevIndex]; if (item && item.value) { onUpdateFocus(item.value); } } } export function processKeyEvent(event, params) { if (params.customKeyDown) { params.customKeyDown(event); return { handled: true, keyLower: '' }; } // Don't process keyboard events when disabled or readOnly if (params.disabled || params.readOnly) { return { handled: true, keyLower: '' }; } if (!(event.target instanceof HTMLInputElement)) { return { handled: true, keyLower: '' }; } const keyLower = event.key.toLowerCase(); if ([KEY.ArrowDown, KEY.ArrowUp, KEY.Enter, KEY.Escape] .map((k) => k.toLowerCase()) .includes(keyLower)) { event.preventDefault(); } return { handled: false, keyLower }; } export function handleBackspace(input, params) { var _a; if (params.multiSelect && input.value.length === 0) { // Get the last selected chip in selection order if (params.selectionOrder.length > 0) { const lastSelectedValue = params.selectionOrder[params.selectionOrder.length - 1]; const lastSelectedItem = (_a = params.items) === null || _a === void 0 ? void 0 : _a.find((item) => item.value === lastSelectedValue); if (lastSelectedItem) { // Remove the chip internally params.onChipRemove(lastSelectedItem); } } } } export function processItemSelection(item, params) { if (params.disabled || params.readOnly || !params.items) { return { updatedItems: params.items, updatedValue: undefined, updatedSelectionOrder: params.selectionOrder, shouldExpandChips: false, shouldCloseMenu: false, }; } if (params.customItemSelect) { params.customItemSelect(item); return { updatedItems: undefined, updatedValue: undefined, updatedSelectionOrder: params.selectionOrder, shouldExpandChips: false, shouldCloseMenu: false, }; } let updatedItems; let updatedValue = undefined; let updatedSelectionOrder = params.selectionOrder; let shouldExpandChips = false; if (params.multiSelect) { const currentItem = params.items.find((menuItem) => menuItem.value === item.value); const isCurrentlySelected = (currentItem === null || currentItem === void 0 ? void 0 : currentItem.selected) || false; // Also check if item is already in selectionOrder to prevent duplicates const isInSelectionOrder = params.selectionOrder.includes(item.value); if (isCurrentlySelected || isInSelectionOrder) { return { updatedItems: params.items, updatedValue: '', updatedSelectionOrder: params.selectionOrder, shouldExpandChips: false, shouldCloseMenu: !params.leaveMenuOpen, }; } updatedItems = [ ...params.items.map((menuItem) => (Object.assign(Object.assign({}, menuItem), { selected: menuItem.value === item.value ? true : menuItem.selected, focused: params.leaveMenuOpen ? menuItem.value === item.value : false }))), ]; // Add to end of selection order (now guaranteed not to be a duplicate) updatedSelectionOrder = [...params.selectionOrder, item.value]; // Clear the input value in multi-select mode updatedValue = ''; // If we exceed maxChips, automatically expand if (params.maxChips && params.maxChips > 0 && updatedSelectionOrder.length > params.maxChips) { shouldExpandChips = true; } } else { updatedItems = [ ...params.items.map((menuItem) => (Object.assign(Object.assign({}, menuItem), { selected: menuItem.value === item.value, focused: params.leaveMenuOpen ? menuItem.value === item.value : false }))), ]; // Always set the input value to show the selected item's label updatedValue = item.label; } return { updatedItems, updatedValue, updatedSelectionOrder, shouldExpandChips, shouldCloseMenu: !params.leaveMenuOpen, }; } export function processChipRemoval(item, params) { if (params.disabled || params.readOnly || !params.items) { return { updatedItems: params.items, updatedSelectionOrder: params.selectionOrder, }; } const updatedItems = [ ...params.items.map((menuItem) => (Object.assign(Object.assign({}, menuItem), { selected: menuItem.value === item.value ? false : menuItem.selected }))), ]; // Remove from selection order const updatedSelectionOrder = params.selectionOrder.filter((value) => value !== item.value); return { updatedItems, updatedSelectionOrder, }; } export function processInputChange(event, params) { if (params.disabled || params.readOnly) { return { inputValue: '', shouldShowMenu: false, updatedItems: params.items, shouldResetNavigation: false, }; } // Add null checks for edge cases if (!event.detail || !event.detail.target) { return { inputValue: '', shouldShowMenu: false, updatedItems: params.items, shouldResetNavigation: false, }; } const input = event.detail.target; const inputValue = input.value || ''; if (params.customInputChange) { params.customInputChange(inputValue); return { inputValue, shouldShowMenu: false, updatedItems: undefined, // Don't update items - custom handler will do it shouldResetNavigation: false, }; } // Update menu visibility let shouldShowMenu; if (params.showMenuOnFocus) { shouldShowMenu = true; } else { shouldShowMenu = inputValue.length >= params.minChars; } let updatedItems = params.items; if (params.items) { // Clear the focused state from all items updatedItems = [ ...params.items.map((item) => (Object.assign(Object.assign({}, item), { focused: false }))), ]; // In single select mode, if the input is cleared, also clear the selection if (!params.multiSelect && inputValue === '') { updatedItems = [ ...updatedItems.map((item) => (Object.assign(Object.assign({}, item), { selected: false }))), ]; } } return { inputValue, shouldShowMenu, updatedItems, shouldResetNavigation: !!inputValue, }; } export function renderNoResults(params) { var _a, _b, _c; return (h("div", { class: "modus-wc-autocomplete-no-results" }, h("div", { class: "icon-label", "aria-label": (_a = params.noResults) === null || _a === void 0 ? void 0 : _a.ariaLabel }, h(SearchSolidIcon, { className: "modus-wc-autocomplete-search-icon" }), h("div", { class: "label" }, (_b = params.noResults) === null || _b === void 0 ? void 0 : _b.label)), h("div", { class: "sub-label" }, (_c = params.noResults) === null || _c === void 0 ? void 0 : _c.subLabel))); } export function renderChips(params) { // Get selected items in selection order const selectedItems = params.selectionOrder .map((value) => { var _a; return (_a = params.items) === null || _a === void 0 ? void 0 : _a.find((item) => item.value === value && item.selected); }) .filter(Boolean); if (selectedItems.length === 0) { return h(Fragment, null); } // Chip display logic: // - Not expanded: show up to maxChips (compact view) // - Expanded: show all chips regardless of focus state const effectiveMaxChips = !params.isChipsExpanded && params.maxChips && params.maxChips > 0 ? params.maxChips : selectedItems.length; const visibleItems = selectedItems.slice(0, effectiveMaxChips); return (h(Fragment, null, visibleItems.map((item) => (h("modus-wc-chip", { "aria-label": "Remove item button", label: item.label, "show-remove": true, size: "sm", disabled: params.disabled || params.readOnly, onChipRemove: (event) => { event.stopPropagation(); params.onChipRemove(item); }, variant: "filled" }))))); } export function renderClearButton(params) { var _a, _b; const showClear = params.includeClear && !params.disabled && !params.readOnly && (params.selectionOrder.length > 0 || ((_b = (_a = params.value) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0); if (!showClear) { return null; } return (h("modus-wc-button", { onClick: params.onClearAll, variant: "borderless", color: "secondary", "aria-label": "Clear all", disabled: params.disabled || params.readOnly, size: "xs", shape: "circle", type: "button" }, h(CloseSolidIcon, null))); } export function renderExpandCollapseButton(params) { const selectedItemsCount = params.selectionOrder.length; // Show expand/collapse button when there are more chips than maxChips if (!params.maxChips || params.maxChips <= 0 || selectedItemsCount <= params.maxChips) { return null; } const remainingCount = selectedItemsCount - params.maxChips; return (h("modus-wc-button", { "custom-class": `modus-wc-autocomplete-expand-button ${params.isChipsExpanded ? 'expanded' : ''}`, onClick: params.onToggleExpansion, variant: "borderless", color: "secondary", "aria-label": params.isChipsExpanded ? 'Collapse chips' : `Show ${remainingCount} more`, disabled: params.disabled || params.readOnly, size: "xs", shape: "circle", type: "button" }, h("modus-wc-icon", { "aria-label": params.isChipsExpanded ? 'Collapse chips' : 'Expand chips', name: params.isChipsExpanded ? 'caret_up' : 'caret_down', size: "md" }))); } export function renderMoreChipsIndicator(params) { const selectedItemsCount = params.selectionOrder.length; // Show "+N more" when there are more chips than maxChips and not expanded if (!params.maxChips || params.maxChips <= 0 || params.isChipsExpanded) { return null; } const remainingCount = selectedItemsCount - params.maxChips; if (remainingCount <= 0) { return null; } return (h("modus-wc-chip", { label: `+${remainingCount}`, size: "sm", variant: "filled" })); } export function renderInput(params) { return (h("modus-wc-text-input", Object.assign({ bordered: params.bordered && !params.multiSelect, disabled: params.disabled, includeClear: !params.multiSelect && params.includeClear, includeSearch: !params.multiSelect && params.includeSearch, inputId: params.inputId, inputTabIndex: params.inputTabIndex, name: params.name, onInputBlur: params.onBlur, onInputChange: params.onChange, onInputFocus: params.onFocus, placeholder: params.placeholder, readOnly: params.readOnly, required: params.required, size: params.size, value: params.value }, params.inheritedAttributes))); } export function renderMenuItems(params) { var _a, _b, _c; if (params.showSpinner) { return (h("li", null, h("modus-wc-loader", { variant: "spinner", size: params.size }))); } const menuItems = params.filteredItems || params.items || []; const noResults = ((_a = params.noResults) === null || _a === void 0 ? void 0 : _a.label) || ((_b = params.noResults) === null || _b === void 0 ? void 0 : _b.subLabel) || ((_c = params.noResults) === null || _c === void 0 ? void 0 : _c.ariaLabel); return (h(Fragment, null, menuItems.length > 0 || !noResults || params.hasSlottedContent ? menuItems.map((item) => (h("modus-wc-menu-item", { disabled: item.disabled, focused: item.focused, label: item.label, onItemSelect: () => params.onItemSelect(item.value), onMouseDown: (e) => e.preventDefault(), selected: item.selected, value: item.value }))) : renderNoResults({ noResults: params.noResults }))); }