@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
JavaScript
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 })));
}