UNPKG

@synergy-design-system/components

Version:
1,382 lines (1,369 loc) 48.1 kB
import { combobox_styles_default } from "./chunk.4P4UJDDZ.js"; import { defaultOptionRenderer } from "./chunk.KLL2SVR7.js"; import { checkValueBelongsToOption, createOptionFromDifferentTypes, filterOnlyOptgroups, getAllOptions, getAssignedElementsForSlot, getValueFromOption, getValuesFromOptions, normalizeString } from "./chunk.FVIRHFY6.js"; import { SynOption } from "./chunk.ZQGIIVB5.js"; import { compareValues, isAllowedValue } from "./chunk.VECCQZP5.js"; import { scrollIntoView } from "./chunk.2DT3C6WE.js"; import { form_control_styles_default } from "./chunk.34WYIU4C.js"; import { FormControlController } from "./chunk.YDQ424MR.js"; import { SynTag } from "./chunk.3TG5EZUB.js"; import { HasSlotController } from "./chunk.CHFWLQN5.js"; import { SynIcon } from "./chunk.KODS2CCZ.js"; import { enableDefaultSettings } from "./chunk.WYMKPLYK.js"; import { SynPopup } from "./chunk.C2RDCOO4.js"; import { waitForEvent } from "./chunk.LQDIH4Z5.js"; import { animateTo, stopAnimations } from "./chunk.DLSBQOBA.js"; import { getAnimation, setDefaultAnimation } from "./chunk.NIRQP534.js"; import { LocalizeController } from "./chunk.ZXMSJF2M.js"; import { watch } from "./chunk.UR2E7Y2Q.js"; import { component_styles_default } from "./chunk.2NT3B5WJ.js"; import { SynergyElement } from "./chunk.L2IYPRPF.js"; import { __decorateClass } from "./chunk.VK2FVWOF.js"; // src/components/combobox/combobox.component.ts import { classMap } from "lit/directives/class-map.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { html } from "lit"; import { property, query, state } from "lit/decorators.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; var SynCombobox = class extends SynergyElement { constructor() { super(...arguments); this.formControlController = new FormControlController(this, { assumeInteractionOn: ["syn-blur", "syn-input"] }); this.hasSlotController = new HasSlotController(this, "help-text", "label"); this.localize = new LocalizeController(this); /** * Cache of the last syn-options that were selected by user interaction (click or keyboard navigation). * Used to track user selections and maintain selection state during value changes. */ this.lastOptions = []; this.isInitialized = false; /** * Flag to prevent infinite loops when the option renderer programmatically updates options. * Set to true during option rendering to ignore slot change events triggered by our own updates. */ this.isOptionRendererTriggered = false; this.hasFocus = false; this.isUserInput = false; this.displayLabel = ""; this.selectedOptions = []; this.numberFilteredOptions = 0; this.cachedOptions = []; this.valueHasChanged = false; this.hideOptions = false; this.name = ""; this._value = ""; this.defaultValue = ""; this.size = "medium"; this.placeholder = ""; this.disabled = false; this.readonly = false; this.clearable = false; this.open = false; this.label = ""; this.placement = "bottom"; this.helpText = ""; this.form = ""; this.required = false; this.restricted = false; this.multiple = false; this.getOption = defaultOptionRenderer; this.filter = (option, queryStr) => { var _a; let content = (option == null ? void 0 : option.textContent) || ""; if (option instanceof SynOption) { content = option.getTextLabel(); } const normalizedOption = normalizeString(content); const normalizedQuery = normalizeString(queryStr); if (normalizedOption.includes(normalizedQuery)) { return true; } return ((_a = option == null ? void 0 : option.value) == null ? void 0 : _a.toString()) === queryStr; }; this.delimiter = " "; this.maxOptionsVisible = 3; this.getTag = (option) => html` <syn-tag part="tag" exportparts=" base:tag__base, content:tag__content, remove-button:tag__remove-button, remove-button__base:tag__remove-button__base " size=${this.size} removable @syn-remove=${(event) => this.handleTagRemove(event, option)} > ${option.getTextLabel()} </syn-tag> `; this.calculateTagMaxWidth = (entries) => { const input = entries.at(0); if (!input || !this.tagContainer) return; const inputWidth = input.contentRect.width; const tagsWidth = this.tagContainer.getBoundingClientRect().width; const availableTagSpace = Math.max(85, tagsWidth + inputWidth - 48); this.tagContainer.style.setProperty("--syn-select-tag-max-width", `${availableTagSpace}px`); }; this.handleDocumentFocusIn = (event) => { const path = event.composedPath(); if (this && !path.includes(this)) { this.hide(); } }; /* eslint-disable @typescript-eslint/no-floating-promises */ // eslint-disable-next-line complexity this.handleDocumentKeyDown = (event) => { const target = event.target; const isClearButton = target.closest(".combobox__clear") !== null; if (isClearButton) { return; } if (event.key === "Escape") { if (this.open && !this.closeWatcher) { event.preventDefault(); event.stopPropagation(); this.hide(); this.displayInput.focus({ preventScroll: true }); } else if (!this.open) { if (this.multiple) { this.clearInputField(); } else { this.clearCombobox(); } } } if (event.key === "Enter") { const currentOption = this.getCurrentOption(); const hasModifier = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey; if (!this.open && !hasModifier) { setTimeout(() => { if (!event.defaultPrevented) { this.formControlController.submit(); } }); return; } if (!this.open || currentOption && currentOption.disabled) { return; } if (currentOption) { this.isUserInput = true; this.valueHasChanged = true; const oldValue = this.lastOptions ? getValuesFromOptions(this.lastOptions) : []; if (this.multiple) { this.toggleOptionSelection(currentOption); } else { this.setSelectedOptions(currentOption); } this.selectionChanged(); const value = Array.isArray(this.value) ? this.value : [this.value]; this.updateComplete.then(() => { this.isUserInput = false; }); if (!compareValues(oldValue, value)) { this.updateComplete.then(() => { this.emit("syn-input"); this.emit("syn-change"); }); } } if (!this.multiple) { this.hide(); } this.displayInput.focus({ preventScroll: true }); return; } if (["ArrowUp", "ArrowDown"].includes(event.key)) { event.preventDefault(); event.stopPropagation(); if (!this.open) { this.show(); } this.selectNextOption(event.key === "ArrowDown"); } if (["Home", "End"].includes(event.key)) { event.preventDefault(); event.stopPropagation(); if (event.key === "Home") { this.displayInput.setSelectionRange(0, 0); } else if (event.key === "End") { this.displayInput.setSelectionRange( this.displayLabel.length, this.displayLabel.length ); } } }; /* eslint-enable @typescript-eslint/no-floating-promises */ this.handleDocumentMouseDown = (event) => { const path = event.composedPath(); if (this && !path.includes(this)) { this.hide(); } }; } get value() { return this._value; } set value(val) { if (this.multiple) { if (!Array.isArray(val)) { val = typeof val === "string" ? val.split(this.delimiter) : [val].filter(isAllowedValue); } } else { val = Array.isArray(val) ? val.join(this.delimiter) : val; } if (compareValues(this._value, val)) { return; } this.valueHasChanged = true; this._value = val; } /** Gets the validity state object */ get validity() { return this.valueInput.validity; } /** Gets the validation message */ get validationMessage() { return this.valueInput.validationMessage; } enableResizeObserver() { if (!this.multiple) return; if (!this.resizeObserver) { this.resizeObserver = new ResizeObserver(this.calculateTagMaxWidth); } this.resizeObserver.observe(this.displayInput); } connectedCallback() { super.connectedCallback(); this.mutationObserver = new MutationObserver((entries) => { var _a, _b, _c; const hasRelevantValueChange = entries.some((entry) => { if (!(entry.target instanceof SynOption)) { return false; } if (entry.type !== "attributes" || entry.attributeName !== "value") { return false; } const currentValue = entry.target.getAttribute("value"); return entry.oldValue !== currentValue && !!currentValue; }); const hasRelevantSelectedLabelChange = (this.restricted || this.multiple) && entries.some((entry) => { if (entry.type !== "characterData" && entry.type !== "childList") { return false; } return this.selectedOptions.some((option) => option === entry.target || option.contains(entry.target)); }); if (hasRelevantValueChange) { this.handleSlotContentChange(); } if (hasRelevantSelectedLabelChange) { if (this.multiple) { if (this.readonly) { this.displayLabel = this.selectedOptions.map((option) => option.getTextLabel()).join(", "); } else { this.requestUpdate(); } } else { this.displayLabel = (_c = (_b = (_a = this.selectedOptions[0]) == null ? void 0 : _a.getTextLabel) == null ? void 0 : _b.call(_a)) != null ? _c : this.displayLabel; } } }); this.mutationObserver.observe(this, { attributeFilter: ["value"], attributeOldValue: true, attributes: true, characterData: true, childList: true, subtree: true }); setTimeout(() => { this.handleSlotContentChange(); }); this.open = false; } disconnectedCallback() { var _a, _b; super.disconnectedCallback(); (_a = this.resizeObserver) == null ? void 0 : _a.disconnect(); (_b = this.mutationObserver) == null ? void 0 : _b.disconnect(); this.removeOpenListeners(); } firstUpdated() { this.isInitialized = true; this.formControlController.updateValidity(); } updated(changedProperties) { var _a; super.updated(changedProperties); if (changedProperties.has("multiple")) { if (!this.multiple) { (_a = this.resizeObserver) == null ? void 0 : _a.disconnect(); } else { this.enableResizeObserver(); } } } // eslint-disable-next-line complexity willUpdate(changedProperties) { super.willUpdate(changedProperties); const isDefaultValueEmpty = this.defaultValue == null || this.defaultValue === "" || Array.isArray(this.defaultValue) && this.defaultValue.length === 0; if (changedProperties.has("value") && isDefaultValueEmpty && this.value && !this.isUserInput) { if (this.multiple && Array.isArray(this.value)) { this.defaultValue = this.value.join(this.delimiter); } else { this.defaultValue = this.value; } this.valueHasChanged = false; } if (!this.isInitialized && changedProperties.has("value") && this.value !== void 0 && changedProperties.has("multiple") && this.multiple) { if (!Array.isArray(this.defaultValue)) { const cachedValueHasChanged = this.valueHasChanged; this.value = typeof this.defaultValue === "string" ? this.defaultValue.split(this.delimiter) : [this.defaultValue].filter(isAllowedValue); this.valueHasChanged = cachedValueHasChanged; } } } attributeChangedCallback(name, oldVal, newVal) { super.attributeChangedCallback(name, oldVal, newVal); if (name === "value") { const cachedValueHasChanged = this.valueHasChanged; this.value = this.defaultValue; this.valueHasChanged = cachedValueHasChanged; } } get tags() { return this.selectedOptions.map((option, index) => { if (index < this.maxOptionsVisible || this.maxOptionsVisible <= 0) { const tag = this.getTag(option, index); return html`<div @syn-remove=${(e) => this.handleTagRemove(e, option)}> ${typeof tag === "string" ? unsafeHTML(tag) : tag} </div>`; } if (index === this.maxOptionsVisible) { return html`<syn-tag size=${this.size}>+${this.selectedOptions.length - index}</syn-tag>`; } return html``; }); } addOpenListeners() { var _a; document.addEventListener("focusin", this.handleDocumentFocusIn); document.addEventListener("mousedown", this.handleDocumentMouseDown); if (this.getRootNode() !== document) { this.getRootNode().addEventListener("focusin", this.handleDocumentFocusIn); } if ("CloseWatcher" in window) { (_a = this.closeWatcher) == null ? void 0 : _a.destroy(); this.closeWatcher = new CloseWatcher(); this.closeWatcher.onclose = () => { if (this.open) { this.hide(); this.displayInput.focus({ preventScroll: true }); } }; } } removeOpenListeners() { var _a; document.removeEventListener("focusin", this.handleDocumentFocusIn); document.removeEventListener("mousedown", this.handleDocumentMouseDown); if (this.getRootNode() !== document) { this.getRootNode().removeEventListener("focusin", this.handleDocumentFocusIn); } (_a = this.closeWatcher) == null ? void 0 : _a.destroy(); } handleFocus() { this.hasFocus = true; this.emit("syn-focus"); } handleBlur() { this.hasFocus = false; this.emit("syn-blur"); } handleFormControlClick() { if (this.readonly) { this.displayInput.focus(); } } handleLabelClick() { this.displayInput.focus(); } handleTagRemove(event, option) { event.stopPropagation(); this.valueHasChanged = true; if (!this.disabled && !this.readonly) { this.toggleOptionSelection(option, false); this.selectionChanged(); this.updateComplete.then(() => { this.emit("syn-input"); this.emit("syn-change"); }); } } handleComboboxMouseDown(event) { const path = event.composedPath(); const isIconButton = path.some((el) => el instanceof Element && el.tagName.toLowerCase() === "syn-icon-button"); if (this.disabled || this.readonly || isIconButton) { return; } const toggleListboxOpen = () => this.open ? this.hide() : this.show(); event.preventDefault(); toggleListboxOpen().then(() => { setTimeout(() => this.displayInput.focus({ preventScroll: true })); }); } handleComboboxKeyDown(event) { if (event.key === "Tab") { return; } this.handleDocumentKeyDown(event); } handleClearClick(event) { event.stopPropagation(); this.clearCombobox(); } clearInputField() { if (this.displayLabel !== "") { const cachedValueHasChanged = this.valueHasChanged; this.value = getValuesFromOptions(this.selectedOptions); this.valueHasChanged = cachedValueHasChanged; this.displayInput.focus({ preventScroll: true }); this.updateComplete.then(() => { this.emit("syn-input"); }); } } clearCombobox() { this.valueHasChanged = true; if (this.value !== "") { this.value = ""; this.displayLabel = ""; this.lastOptions = []; this.setSelectedOptions([]); this.selectionChanged(); this.displayInput.focus({ preventScroll: true }); this.updateComplete.then(() => { this.emit("syn-clear"); this.emit("syn-input"); this.emit("syn-change"); }); } } // eslint-disable-next-line class-methods-use-this preventLoosingFocus(event) { event.stopPropagation(); event.preventDefault(); } /* eslint-disable @typescript-eslint/no-floating-promises */ handleOptionClick(event) { const target = event.target; const option = target.closest("syn-option"); const oldValue = this.lastOptions ? getValuesFromOptions(this.lastOptions) : []; if (option && !option.disabled) { this.isUserInput = true; this.valueHasChanged = true; if (this.multiple) { this.toggleOptionSelection(option); } else { this.setSelectedOptions(option); } this.selectionChanged(); this.updateComplete.then(() => { this.displayInput.focus({ preventScroll: true }); this.isUserInput = false; }); const value = Array.isArray(this.value) ? this.value : [this.value]; if (!compareValues(oldValue, value)) { this.updateComplete.then(() => { this.emit("syn-input"); this.emit("syn-change"); }); } if (!this.multiple) { this.hide(); this.displayInput.focus({ preventScroll: true }); } } } /* eslint-enable @typescript-eslint/no-floating-promises */ /** * Selects the following or previous option. * * @param isNext - A boolean indicating whether to select the following option (true) * or the previous option (false). */ selectNextOption(isNext) { const filteredOptions = this.getAllFilteredOptions(); if (filteredOptions.length === 0) { return; } const currentOption = this.getCurrentOption(); const currentIndex = filteredOptions.indexOf(currentOption); let newIndex = Math.max(0, currentIndex); if (isNext) { const nextIndex = currentIndex + 1; newIndex = nextIndex > filteredOptions.length - 1 ? 0 : nextIndex; } else { const previousIndex = currentIndex - 1; newIndex = previousIndex < 0 ? filteredOptions.length - 1 : previousIndex; } this.setCurrentOption(filteredOptions[newIndex]); scrollIntoView(this.getCurrentOption(), this.listbox, "vertical", "auto"); } // Toggles an option's selected state // eslint-disable-next-line class-methods-use-this toggleOptionSelection(option, force) { if (force === true || force === false) { option.selected = force; } else { option.selected = !option.selected; } const cachedOption = this.cachedOptions.find((opt) => opt.id === option.id); if (cachedOption) { cachedOption.selected = option.selected; } } // Sets the selected option(s) setSelectedOptions(option) { const newSelectedOptions = Array.isArray(option) ? option : [option]; if (!this.multiple && newSelectedOptions.length > 1) { newSelectedOptions.splice(1); } const slottedOptions = this.getSlottedOptions(); slottedOptions.forEach((opt) => { opt.selected = newSelectedOptions.some( (selectedOpt) => selectedOpt.id === opt.id ); }); this.cachedOptions.forEach((opt) => { opt.selected = newSelectedOptions.some( (selectedOpt) => selectedOpt.id === opt.id ); }); } getAllFilteredOptions() { return this.getSlottedOptions().filter((option) => !option.hidden); } getCurrentOption() { return this.getAllFilteredOptions().find((option) => option.current); } // Sets the current option, which is the option the user is currently interacting with // (e.g. via keyboard). Only one option may be "current" at a time. setCurrentOption(option) { const allOptions = this.getAllFilteredOptions(); this.displayInput.removeAttribute("aria-activedescendant"); allOptions.forEach((el) => { el.current = false; el.setAttribute("aria-selected", "false"); }); if (option) { option.current = true; option.setAttribute("aria-selected", "true"); this.displayInput.setAttribute("aria-activedescendant", option.id); } } /** * Updates the component state after selection changes. * * This method synchronizes: * 1. The selectedOptions cache with currently selected options * 2. The component's value property (string or array) * 3. The display label shown in the input * 4. Form validation state * * **Validation Logic:** * - In restricted mode, invalid values trigger a reset to last valid state * - Multiple mode requires all values to correspond to existing options * - Single mode allows free text input when not restricted */ // eslint-disable-next-line complexity selectionChanged() { const options = this.getSlottedOptions(); this.selectedOptions = options.filter((opt) => opt.selected); if (this.selectedOptions.length === 0) { this.displayLabel = Array.isArray(this.value) ? this.value.join(", ") : String(this.value); } let optionValue; const cachedValueHasChanged = this.valueHasChanged; if (this.multiple) { this.value = this.selectedOptions.map((opt) => getValueFromOption(opt)); if (this.value.length === 0 && this.selectedOptions.length !== 0) { this.valueHasChanged = cachedValueHasChanged; this.resetToLastValidValue(); return; } } else { if (this.selectedOptions.length !== 0) { optionValue = getValueFromOption(this.selectedOptions[0]); } else if (this.restricted && !this.isValidValue(this.displayLabel) && this.displayLabel !== "" && !this.isUserInput) { this.resetToLastValidValue(); this.valueHasChanged = cachedValueHasChanged; return; } this.value = optionValue != null ? optionValue : this.displayLabel; } this.valueHasChanged = cachedValueHasChanged; this.lastOptions = [...this.selectedOptions]; this.updateComplete.then(() => { var _a, _b; let newLabel = this.displayLabel; if (this.multiple && this.readonly) { newLabel = this.selectedOptions.map((opt) => opt.getTextLabel()).join(", "); } else if (this.multiple && !this.readonly) { newLabel = ""; } else { newLabel = (_b = (_a = this.selectedOptions[0]) == null ? void 0 : _a.getTextLabel()) != null ? _b : this.displayLabel; } this.displayLabel = newLabel; this.formControlController.updateValidity(); }); } handleInvalid(event) { this.formControlController.setValidity(false); this.formControlController.emitInvalidEvent(event); } handlePropertiesChange() { this.createComboboxOptionsFromQuery(this.displayLabel); if (this.open) { this.updateComplete.then(() => { this.open = this.multiple || this.restricted || this.numberFilteredOptions > 0; }); } } handleDisplayInputValueChange() { this.createComboboxOptionsFromQuery(this.displayLabel); } handleDisabledChange() { if (this.disabled) { this.formControlController.setValidity(this.disabled); } if (this.disabled || this.readonly) { this.open = false; this.handleOpenChange(); } this.selectionChanged(); } handleDelimiterChange() { this.getSlottedOptions().forEach((option) => { option.delimiter = this.delimiter; }); } handleValueChange() { if (!this.valueHasChanged) { const cachedValueHasChanged = this.valueHasChanged; this.value = this.defaultValue; this.valueHasChanged = cachedValueHasChanged; } this.updateSelectedOptionFromValue(); } async handleOpenChange() { if (this.open && (!this.disabled && !this.readonly)) { if (this.numberFilteredOptions === 0 && !this.restricted && !this.multiple) { this.open = false; this.emit("syn-error"); return; } this.emit("syn-show"); this.addOpenListeners(); await stopAnimations(this); this.listbox.hidden = false; this.popup.active = true; const { keyframes: keyframes2, options: options2 } = getAnimation(this, "combobox.show", { dir: this.localize.dir() }); await animateTo(this.popup.popup, keyframes2, options2); this.emit("syn-after-show"); return; } this.setCurrentOption(null); this.displayInput.removeAttribute("aria-activedescendant"); this.emit("syn-hide"); this.removeOpenListeners(); await stopAnimations(this); const { keyframes, options } = getAnimation(this, "combobox.hide", { dir: this.localize.dir() }); await animateTo(this.popup.popup, keyframes, options); this.listbox.hidden = true; this.popup.active = false; this.emit("syn-after-hide"); } /** * Shows the listbox. If it is not possible to open the listbox, because there are no * appropriate filtered options, a syn-error is emitted and the listbox stays closed. */ async show() { if (this.open || this.disabled || this.readonly) { this.open = false; return void 0; } this.open = true; return Promise.race([waitForEvent(this, "syn-after-show"), waitForEvent(this, "syn-error")]); } /** Hides the listbox. */ async hide() { if (!this.open || this.disabled || this.readonly) { this.open = false; return void 0; } this.open = false; return waitForEvent(this, "syn-after-hide"); } /** * Checks for validity but does not show a validation message. * Returns `true` when valid and `false` when invalid. */ checkValidity() { return this.valueInput.checkValidity(); } /** Gets the associated form, if one exists. */ getForm() { return this.formControlController.getForm(); } /** Checks for validity and shows the browser's validation message if the control is invalid. */ reportValidity() { return this.valueInput.reportValidity(); } /** Sets a custom validation message. Pass an empty string to restore validity. */ setCustomValidity(message) { this.valueInput.setCustomValidity(message); this.formControlController.updateValidity(); } /** Sets focus on the control. */ focus(options) { this.displayInput.focus(options); } /** Removes focus from the control. */ blur() { this.displayInput.blur(); } /** * Updates the visibility and rendering of options based on the current query string. * * This method performs several critical tasks: * 1. **Option Filtering**: Uses the `filter` function to determine which options should be visible * 2. **Custom Rendering**: Applies the `getOption` function to customize option appearance * 3. **Optgroup Management**: Shows/hides optgroups based on their visible children * 4. **Counts visible options**: Tracks the number of visible options for UI logic * * **Performance Considerations:** * - Uses cached options to avoid repeated DOM queries * - Prevents infinite loops during option updates with `isOptionRendererTriggered` * * @param queryString - The current user input to filter and highlight options with */ /* eslint-disable no-param-reassign, @typescript-eslint/no-floating-promises */ createComboboxOptionsFromQuery(queryString) { var _a; this.numberFilteredOptions = 0; this.isOptionRendererTriggered = true; if (this.cachedOptions.length === 0) { this.cacheSlottedOptionsAndOptgroups(); } this.getSlottedOptions().forEach((option) => { const cachedOption = this.cachedOptions.find((o) => o.id === option.id) || option; const optionResult = this.getOption(cachedOption, queryString); let updatedOption = createOptionFromDifferentTypes(optionResult); if (!updatedOption) { updatedOption = cachedOption; } const hideOption = !(this.filter(updatedOption, queryString) || queryString === ""); updatedOption.hidden = hideOption; option.replaceWith(updatedOption); if (!hideOption) { this.numberFilteredOptions += 1; } }); const visibleOptgroups = this.getSlottedOptGroups().filter((optgroup) => { const options = getAllOptions(Array.from(optgroup.children)).flat(); const isVisible = options.some((option) => !option.hidden); optgroup.hidden = !isVisible; return isVisible; }); (_a = visibleOptgroups[0]) == null ? void 0 : _a.style.setProperty("--display-divider", "none"); setTimeout(() => { this.isOptionRendererTriggered = false; }); } /* eslint-enable no-param-reassign, @typescript-eslint/no-floating-promises */ // eslint-disable-next-line complexity async handleInput() { const inputValue = this.displayInput.value; this.displayLabel = inputValue; const cachedLastOption = this.lastOptions; this.isUserInput = true; if (!this.multiple) { this.selectedOptions = []; } if (this.multiple) { const validValues = getValuesFromOptions(this.selectedOptions); this.value = [...validValues, inputValue]; } else { this.value = inputValue; } await this.updateComplete; this.isUserInput = false; this.lastOptions = cachedLastOption; this.open = this.multiple || this.restricted || this.numberFilteredOptions > 0; this.formControlController.updateValidity(); this.emit("syn-input"); } /** * Checks if the value is available in the options list. * This is used to determine if the value is valid when the combobox is restricted. * @param value - The value to check for validity. * @returns `true` if the current value is available in the options list, * otherwise `false`. */ isValidValue(value) { const isValid = this.cachedOptions.some( (option) => checkValueBelongsToOption(value, option) ); return isValid; } getOptionsFromValue() { const value = this.valueHasChanged ? this.value : this.defaultValue; let convertedValue; if (Array.isArray(value)) { convertedValue = value; } else if (value === void 0 || value == null) { convertedValue = []; } else if (this.multiple && typeof value === "string") { convertedValue = value.split(this.delimiter); } else { convertedValue = [value]; } return convertedValue.map((val) => this.cachedOptions.find((option) => checkValueBelongsToOption(val, option))).filter((opt) => opt !== void 0); } /** * Resets the value to the last valid value or to an empty string. */ resetToLastValidValue() { var _a, _b, _c, _d; let label = ""; let value = []; if (this.lastOptions.length !== 0) { value = getValuesFromOptions(this.lastOptions); if (!this.multiple) { label = this.lastOptions[0].getTextLabel(); } } const popupAnimations = (_d = (_c = (_b = (_a = this.popup) == null ? void 0 : _a.popup) == null ? void 0 : _b.getAnimations) == null ? void 0 : _c.call(_b)) != null ? _d : []; const waitForAnimations = popupAnimations.length ? Promise.all( popupAnimations.map((animation) => animation.playState === "finished" ? Promise.resolve() : new Promise((resolve) => { animation.addEventListener("finish", () => resolve(), { once: true }); })) ) : Promise.resolve(); this.hideOptions = true; waitForAnimations.then(() => { this.hideOptions = false; }); const cachedValueHasChanged = this.valueHasChanged; this.value = value; this.displayLabel = label; this.formControlController.updateValidity(); this.valueHasChanged = cachedValueHasChanged; } // eslint-disable-next-line complexity handleChange() { const isSameSelection = this.selectedOptions.length !== 0 && this.selectedOptions.length === this.getOptionsFromValue().length; let values; if (Array.isArray(this.value)) { values = this.value; } else if (typeof this.value === "string") { values = this.value.split(this.delimiter); } else { values = [this.value]; } const allValuesValid = values.every((val) => this.isValidValue(val)); const isMultipleAndUserInput = this.multiple && isSameSelection && allValuesValid; const isNonMultipleAndUserInput = !this.multiple && this.selectedOptions.length > 0; if (isNonMultipleAndUserInput || isMultipleAndUserInput) { return; } const oldValue = this.lastOptions ? getValuesFromOptions(this.lastOptions) : []; if ((this.restricted || this.multiple) && !this.isValidValue(this.displayLabel) && this.displayLabel !== "") { this.resetToLastValidValue(); return; } const options = this.getOptionsFromValue(); this.setSelectedOptions(options); this.selectionChanged(); this.lastOptions = [...options]; this.updateComplete.then(() => { this.formControlController.updateValidity(); }); const value = Array.isArray(this.value) ? this.value : [this.value]; if (!compareValues(oldValue, value)) { this.emit("syn-change"); } } getSlottedOptions() { if (!this.defaultSlot) { return []; } return getAllOptions(getAssignedElementsForSlot(this.defaultSlot)).flat(); } getSlottedOptGroups() { return filterOnlyOptgroups(getAssignedElementsForSlot(this.defaultSlot)); } /* eslint-disable no-param-reassign */ cacheSlottedOptionsAndOptgroups() { const slottedOptions = this.getSlottedOptions(); const slottedOptgroups = this.getSlottedOptGroups(); slottedOptions.forEach((option, index) => { option.id = option.id || `syn-combobox-option-${index}`; }); slottedOptgroups.forEach((optgroup, index) => { optgroup.id = optgroup.id || `syn-combobox-optgroup-${index}`; }); this.cachedOptions = [...slottedOptions]; } /* eslint-enable no-param-reassign */ updateSelectedOptionFromValue() { if (!this.isUserInput) { const options = this.getOptionsFromValue(); this.setSelectedOptions(options); this.selectionChanged(); } let queryString = ""; if (this.multiple) { queryString = this.displayLabel; } else { queryString = Array.isArray(this.value) ? this.value.join(", ") : String(this.value); } this.createComboboxOptionsFromQuery(queryString); } /** * Synchronizes the internal component state with changes to slotted options. * * This method is automatically triggered by: * - MutationObserver when option 'value' attributes change * - Initial component setup during connectedCallback (after timeout) * - Default slot changes (via handleDefaultSlotChange) * - Custom element registration completion for syn-option (deferred execution) * * The synchronization process: * 1. Waits for syn-option custom elements to be registered before processing * 2. Updates delimiter settings on all slotted options * 3. Refreshes the internal cache of options and optgroups * 4. Synchronizes selected options based on current component value * 5. Auto-opens listbox if component has focus, has value, and is currently closed * * This ensures the component's internal state stays consistent with the slotted * DOM content after options are added, removed, or their values change. * */ /* eslint-disable @typescript-eslint/no-floating-promises */ handleSlotContentChange() { if (!customElements.get("syn-option")) { customElements.whenDefined("syn-option").then(() => this.handleSlotContentChange()); return; } this.handleDelimiterChange(); this.cacheSlottedOptionsAndOptgroups(); this.updateSelectedOptionFromValue(); let hasValue; if (Array.isArray(this.value)) { hasValue = this.value.length > 0; } else if (typeof this.value === "string") { hasValue = this.value.length > 0; } else { hasValue = this.value !== void 0 && this.value !== null; } if (this.hasFocus && hasValue && !this.open) { this.show(); } } /* eslint-enable @typescript-eslint/no-floating-promises */ handleDefaultSlotChange() { if (this.isOptionRendererTriggered) { return; } this.handleSlotContentChange(); } /* eslint-disable @typescript-eslint/unbound-method */ // eslint-disable-next-line complexity render() { var _a; const hasLabelSlot = this.hasSlotController.test("label"); const hasHelpTextSlot = this.hasSlotController.test("help-text"); const hasLabel = this.label ? true : !!hasLabelSlot; const hasHelpText = this.helpText ? true : !!hasHelpTextSlot; let hasValue; if (Array.isArray(this.value)) { hasValue = this.value.length > 0; } else if (typeof this.value === "string") { hasValue = this.value.length > 0; } else { hasValue = this.value !== void 0 && this.value !== null && typeof this.value === "number"; } const hasClearIcon = this.clearable && (!this.disabled && !this.readonly) && hasValue; const isPlaceholderVisible = this.placeholder && !hasValue; const tagsVisible = this.multiple && this.selectedOptions.length > 0; return html` <div part="form-control" class=${classMap({ "form-control": true, "form-control--has-help-text": hasHelpText, "form-control--has-label": hasLabel, "form-control--large": this.size === "large", "form-control--medium": this.size === "medium", "form-control--small": this.size === "small" })} @click=${this.handleFormControlClick} > <label id="label" part="form-control-label" class="form-control__label" aria-hidden=${hasLabel ? "false" : "true"} @click=${this.handleLabelClick} > <slot name="label">${this.label}</slot> </label> <div part="form-control-input" class="form-control-input"> <syn-popup class=${classMap({ combobox: true, "combobox--bottom": this.placement === "bottom", "combobox--disabled": this.disabled, "combobox--focused": this.hasFocus, "combobox--large": this.size === "large", "combobox--medium": this.size === "medium", "combobox--multiple": this.multiple, "combobox--open": this.open, "combobox--placeholder-visible": isPlaceholderVisible, "combobox--readonly": this.readonly, "combobox--small": this.size === "small", "combobox--standard": true, "combobox--tags-visible": tagsVisible, "combobox--top": this.placement === "top" })} placement=${`${this.placement}-start`} flip shift sync="width" auto-size="vertical" auto-size-padding="10" exportparts="popup" > <div part="combobox" class="combobox__inputs" slot="anchor" @keydown=${this.handleComboboxKeyDown} @mousedown=${this.handleComboboxMouseDown} > <slot part="prefix" name="prefix" class="combobox__prefix"></slot> ${this.multiple && !this.readonly ? html`<div part="tags" class="combobox__tags">${this.tags}</div>` : ""} <input part="display-input" class="combobox__display-input" type="text" placeholder=${this.placeholder} .disabled=${this.disabled} .readOnly=${this.readonly} .value=${this.displayLabel} maxlength=${ifDefined(this.maxlength)} autocomplete="off" spellcheck="false" autocapitalize="off" aria-controls="listbox" aria-expanded=${this.open ? "true" : "false"} aria-haspopup="listbox" aria-labelledby="label" aria-disabled=${this.disabled ? "true" : "false"} aria-describedby="help-text" role="combobox" tabindex="0" @focus=${this.handleFocus} @blur=${this.handleBlur} aria-autocomplete="list" aria-owns="listbox" @input=${this.handleInput} @change=${this.handleChange} /> <input class="combobox__value-input" type="text" ?disabled=${this.disabled} ?readonly=${this.readonly} ?required=${this.required} .value=${Array.isArray(this.value) ? this.value.join(", ") : (_a = this.value) == null ? void 0 : _a.toString()} tabindex="-1" aria-hidden="true" @focus=${() => this.focus()} @invalid=${this.handleInvalid} /> ${hasClearIcon ? html` <button part="clear-button" class="combobox__clear" type="button" aria-label=${this.localize.term("clearEntry")} @mousedown=${this.preventLoosingFocus} @click=${this.handleClearClick} tabindex="-1" > <slot name="clear-icon"> <syn-icon name="x-circle-fill" library="system"></syn-icon> </slot> </button> ` : ""} <slot name="suffix" part="suffix" class="combobox__suffix"></slot> <slot name="expand-icon" part="expand-icon" class="combobox__expand-icon"> <syn-icon library="system" name="chevron-down"></syn-icon> </slot> </div> <div id="listbox" role="listbox" aria-expanded=${this.open ? "true" : "false"} aria-labelledby="label" aria-multiselectable=${this.multiple ? "true" : "false"} part="listbox" class="combobox__listbox" tabindex="-1" @mousedown=${this.preventLoosingFocus} @mouseup=${this.handleOptionClick} > <div class="listbox__options" part="filtered-listbox"> ${this.hideOptions || this.numberFilteredOptions === 0 ? html`<span class="listbox__no-results" aria-hidden="true" part="no-results" >${this.localize.term("noResults")}</span >` : ""} <slot class=${classMap({ options__hide: this.hideOptions })} @slotchange=${this.handleDefaultSlotChange}></slot> </div> </div> </syn-popup> </div> <div part="form-control-help-text" id="help-text" class="form-control__help-text" aria-hidden=${hasHelpText ? "false" : "true"} > <slot name="help-text">${this.helpText}</slot> </div> </div> `; } /* eslint-enable @typescript-eslint/unbound-method */ }; SynCombobox.styles = [ component_styles_default, form_control_styles_default, combobox_styles_default ]; SynCombobox.dependencies = { "syn-icon": SynIcon, "syn-popup": SynPopup, "syn-tag": SynTag }; __decorateClass([ query(".combobox") ], SynCombobox.prototype, "popup", 2); __decorateClass([ query(".combobox__inputs") ], SynCombobox.prototype, "combobox", 2); __decorateClass([ query(".combobox__display-input") ], SynCombobox.prototype, "displayInput", 2); __decorateClass([ query(".combobox__value-input") ], SynCombobox.prototype, "valueInput", 2); __decorateClass([ query(".combobox__listbox") ], SynCombobox.prototype, "listbox", 2); __decorateClass([ query("slot:not([name])") ], SynCombobox.prototype, "defaultSlot", 2); __decorateClass([ query(".combobox__tags") ], SynCombobox.prototype, "tagContainer", 2); __decorateClass([ state() ], SynCombobox.prototype, "hasFocus", 2); __decorateClass([ state() ], SynCombobox.prototype, "isUserInput", 2); __decorateClass([ state() ], SynCombobox.prototype, "displayLabel", 2); __decorateClass([ state() ], SynCombobox.prototype, "selectedOptions", 2); __decorateClass([ state() ], SynCombobox.prototype, "numberFilteredOptions", 2); __decorateClass([ state() ], SynCombobox.prototype, "cachedOptions", 2); __decorateClass([ state() ], SynCombobox.prototype, "valueHasChanged", 2); __decorateClass([ state() ], SynCombobox.prototype, "hideOptions", 2); __decorateClass([ property() ], SynCombobox.prototype, "name", 2); __decorateClass([ state() ], SynCombobox.prototype, "value", 1); __decorateClass([ property({ attribute: "value" }) ], SynCombobox.prototype, "defaultValue", 2); __decorateClass([ property({ reflect: true }) ], SynCombobox.prototype, "size", 2); __decorateClass([ property() ], SynCombobox.prototype, "placeholder", 2); __decorateClass([ property({ reflect: true, type: Boolean }) ], SynCombobox.prototype, "disabled", 2); __decorateClass([ property({ reflect: true, type: Boolean }) ], SynCombobox.prototype, "readonly", 2); __decorateClass([ property({ type: Boolean }) ], SynCombobox.prototype, "clearable", 2); __decorateClass([ property({ reflect: true, type: Boolean }) ], SynCombobox.prototype, "open", 2); __decorateClass([ property() ], SynCombobox.prototype, "label", 2); __decorateClass([ property({ type: Number }) ], SynCombobox.prototype, "maxlength", 2); __decorateClass([ property({ reflect: true }) ], SynCombobox.prototype, "placement", 2); __decorateClass([ property({ attribute: "help-text" }) ], SynCombobox.prototype, "helpText", 2); __decorateClass([ property({ reflect: true }) ], SynCombobox.prototype, "form", 2); __decorateClass([ property({ reflect: true, type: Boolean }) ], SynCombobox.prototype, "required", 2); __decorateClass([ property({ reflect: true, type: Boolean }) ], SynCombobox.prototype, "restricted", 2); __decorateClass([ property({ reflect: true, type: Boolean }) ], SynCombobox.prototype, "multiple", 2); __decorateClass([ property() ], SynCombobox.prototype, "getOption", 2); __decorateClass([ property() ], SynCombobox.prototype, "filter", 2); __decorateClass([ property() ], SynCombobox.prototype, "delimiter", 2); __decorateClass([ property({ attribute: "max-options-visible", type: Number }) ], SynCombobox.prototype, "maxOptionsVisible", 2); __decorateClass([ property() ], SynCombobox.prototype, "getTag", 2); __decorateClass([ watch(["filter", "getOption"], { waitUntilFirstUpdate: true }) ], SynCombobox.prototype, "handlePropertiesChange", 1); __decorateClass([ watch("displayLabel", { waitUntilFirstUpdate: true }) ], SynCombobox.prototype, "handleDisplayInputValueChange", 1); __decorateClass([ watch(["disabled", "readonly"], { waitUntilFirstUpdate: true }) ], SynCombobox.prototype, "handleDisabledChange", 1); __decorateClass([ watch("delimiter") ], SynCombobox.prototype, "handleDelimiterChange", 1); __decorateClass([ watch(["defaultValue", "value", "delimiter", "multiple", "restricted"], { waitUntilFirstUpdate: true }) ], SynCombobox.prototype, "handleValueChange", 1); __decorateClass([ watch("open", { waitUntilFirstUpdate: true }) ], SynCombobox.prototype, "handleOpenChange", 1); SynCombobox = __decorateClass([ enableDefaultSettings("SynCombobox") ], SynCombobox); setDefaultAnimation("combobox.show", { keyframes: [ { opacity: 0, scale: 0.9 }, { opacity: 1, scale: 1 } ], options: { duration: 100, easing: "ease" } }); setDefaultAnimation("combobox.hide", { keyframes: [ { opacity: 1, scale: 1 }, { opacity: 0, scale: 0.9 } ], options: { duration: 100, easing: "ease" } }); export { SynCombobox }; //# sourceMappingURL=chunk.A66CI5MO.js.map