UNPKG

@synergy-design-system/components

Version:

This package provides the base of the Synergy Design System as native web components. It uses [lit](https://www.lit.dev) and parts of [shoelace](https://shoelace.style/). Synergy officially supports the latest two versions of all major browsers (as define

883 lines (871 loc) 28.5 kB
import { SynTag } from "./chunk.BN6FB3CA.js"; import { select_custom_styles_default } from "./chunk.DOOJ2WJ7.js"; import { select_styles_default } from "./chunk.XTWO6ZZW.js"; import { compareValues, isAllowedValue } from "./chunk.VECCQZP5.js"; import { SynPopup } from "./chunk.6CC5CH2P.js"; import { scrollIntoView } from "./chunk.5732DMBC.js"; import { form_control_custom_styles_default, form_control_styles_default } from "./chunk.G4URZQCL.js"; import { FormControlController } from "./chunk.HP2LEQRU.js"; import { waitForEvent } from "./chunk.C2ENQBPM.js"; import { animateTo, stopAnimations } from "./chunk.G6ITZTTW.js"; import { getAnimation, setDefaultAnimation } from "./chunk.7JGKUB4A.js"; import { SynIcon } from "./chunk.WFJVDRQR.js"; import { LocalizeController } from "./chunk.OAQRCZOO.js"; import { HasSlotController } from "./chunk.WVVQK5TE.js"; import { enableDefaultSettings } from "./chunk.E5UUNP6E.js"; import { watch } from "./chunk.BVZQ6QSY.js"; import { component_styles_default } from "./chunk.NLYVOJGK.js"; import { SynergyElement } from "./chunk.3AZFEB6D.js"; import { __decorateClass } from "./chunk.Z4XV3SMG.js"; // src/components/select/select.component.ts import { classMap } from "lit/directives/class-map.js"; import { html } from "lit"; import { property, query, state } from "lit/decorators.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; var SynSelect = 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); this.isInitialized = false; this.typeToSelectString = ""; this.hasFocus = false; this.displayLabel = ""; this.selectedOptions = []; this.valueHasChanged = false; this.delimiter = " "; this.name = ""; this._value = ""; this.defaultValue = ""; this.size = "medium"; this.placeholder = ""; this.multiple = false; this.maxOptionsVisible = 3; this.disabled = false; this.clearable = false; this.open = false; this.hoist = false; this.label = ""; this.placement = "bottom"; this.helpText = ""; this.form = ""; this.required = false; this.getTag = (option) => { return 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.handleDocumentFocusIn = (event) => { const path = event.composedPath(); if (this && !path.includes(this)) { this.hide(); } }; this.handleDocumentKeyDown = (event) => { const target = event.target; const isClearButton = target.closest(".select__clear") !== null; const isIconButton = target.closest("syn-icon-button") !== null; if (isClearButton || isIconButton) { return; } if (event.key === "Escape" && this.open && !this.closeWatcher) { event.preventDefault(); event.stopPropagation(); this.hide(); this.displayInput.focus({ preventScroll: true }); } if (event.key === "Enter" || event.key === " " && this.typeToSelectString === "") { event.preventDefault(); event.stopImmediatePropagation(); if (!this.open) { this.show(); return; } if (this.currentOption && !this.currentOption.disabled) { this.valueHasChanged = true; if (this.multiple) { this.toggleOptionSelection(this.currentOption); } else { this.setSelectedOptions(this.currentOption); } 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", "Home", "End"].includes(event.key)) { const allOptions = this.getAllOptions(); const currentIndex = allOptions.indexOf(this.currentOption); let newIndex = Math.max(0, currentIndex); event.preventDefault(); if (!this.open) { this.show(); if (this.currentOption) { return; } } if (event.key === "ArrowDown") { newIndex = currentIndex + 1; if (newIndex > allOptions.length - 1) newIndex = 0; } else if (event.key === "ArrowUp") { newIndex = currentIndex - 1; if (newIndex < 0) newIndex = allOptions.length - 1; } else if (event.key === "Home") { newIndex = 0; } else if (event.key === "End") { newIndex = allOptions.length - 1; } this.setCurrentOption(allOptions[newIndex]); } if (event.key && event.key.length === 1 || event.key === "Backspace") { const allOptions = this.getAllOptions(); if (event.metaKey || event.ctrlKey || event.altKey) { return; } if (!this.open) { if (event.key === "Backspace") { return; } this.show(); } event.stopPropagation(); event.preventDefault(); clearTimeout(this.typeToSelectTimeout); this.typeToSelectTimeout = window.setTimeout(() => this.typeToSelectString = "", 1e3); if (event.key === "Backspace") { this.typeToSelectString = this.typeToSelectString.slice(0, -1); } else { this.typeToSelectString += event.key.toLowerCase(); } for (const option of allOptions) { const label = option.getTextLabel().toLowerCase(); if (label.startsWith(this.typeToSelectString)) { this.setCurrentOption(option); break; } } } }; 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) { this.resizeObserver = new ResizeObserver((entries) => { const entry = entries.at(0); this.tagContainer.style.setProperty("--syn-select-tag-max-width", `${entry.contentRect.width}px`); }); this.resizeObserver.observe(this.tagContainer); } } connectedCallback() { super.connectedCallback(); setTimeout(() => { this.handleDefaultSlotChange(); }); this.open = false; } disconnectedCallback() { var _a; super.disconnectedCallback(); (_a = this.resizeObserver) == null ? void 0 : _a.disconnect(); } addOpenListeners() { var _a; document.addEventListener("focusin", this.handleDocumentFocusIn); document.addEventListener("keydown", this.handleDocumentKeyDown); 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("keydown", this.handleDocumentKeyDown); 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.displayInput.setSelectionRange(0, 0); this.emit("syn-focus"); } handleBlur() { this.hasFocus = false; this.emit("syn-blur"); } handleLabelClick() { this.displayInput.focus(); } handleComboboxMouseDown(event) { const path = event.composedPath(); const isIconButton = path.some((el) => el instanceof Element && el.tagName.toLowerCase() === "syn-icon-button"); if (this.disabled || isIconButton) { return; } event.preventDefault(); this.displayInput.focus({ preventScroll: true }); this.open = !this.open; } handleComboboxKeyDown(event) { if (event.key === "Tab") { return; } event.stopPropagation(); this.handleDocumentKeyDown(event); } handleClearClick(event) { event.stopPropagation(); this.valueHasChanged = true; if (this.value !== "") { this.setSelectedOptions([]); this.displayInput.focus({ preventScroll: true }); this.updateComplete.then(() => { this.emit("syn-clear"); this.emit("syn-input"); this.emit("syn-change"); }); } } handleClearMouseDown(event) { event.stopPropagation(); event.preventDefault(); } handleOptionClick(event) { const target = event.target; const option = target.closest("syn-option"); const oldValue = this.value; if (option && !option.disabled) { this.valueHasChanged = true; if (this.multiple) { this.toggleOptionSelection(option); } else { this.setSelectedOptions(option); } this.updateComplete.then(() => this.displayInput.focus({ preventScroll: true })); if (this.value !== oldValue) { this.updateComplete.then(() => { this.emit("syn-input"); this.emit("syn-change"); }); } if (!this.multiple) { this.hide(); this.displayInput.focus({ preventScroll: true }); } } } /* @internal - used by options to update labels */ handleDefaultSlotChange() { if (!customElements.get("syn-option")) { customElements.whenDefined("syn-option").then(() => this.handleDefaultSlotChange()); } const allOptions = this.getAllOptions(); const val = this.valueHasChanged ? this.value : this.defaultValue; this.handleDelimiterChange(); const value = Array.isArray(val) ? val : typeof val === "string" ? val.split(this.delimiter) : [val].filter(isAllowedValue); const values = []; allOptions.forEach((option) => values.push(option.value)); const valueString = value.map(String); const allSelectedOptions = allOptions.filter( (el) => valueString.includes(String(el.value)) ); this.setSelectedOptions(allSelectedOptions); } handleTagRemove(event, option) { event.stopPropagation(); this.valueHasChanged = true; if (!this.disabled) { this.toggleOptionSelection(option, false); this.updateComplete.then(() => { this.emit("syn-input"); this.emit("syn-change"); }); } } // Gets an array of all <syn-option> elements getAllOptions() { return [...this.querySelectorAll("syn-option")]; } // Gets the first <syn-option> element getFirstOption() { return this.querySelector("syn-option"); } // 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.getAllOptions(); allOptions.forEach((el) => { el.current = false; el.tabIndex = -1; }); if (option) { this.currentOption = option; option.current = true; option.tabIndex = 0; option.focus(); } } // Sets the selected option(s) setSelectedOptions(option) { const allOptions = this.getAllOptions(); const newSelectedOptions = Array.isArray(option) ? option : [option]; allOptions.forEach((el) => el.selected = false); if (newSelectedOptions.length) { newSelectedOptions.forEach((el) => el.selected = true); } this.selectionChanged(); } // Toggles an option's selected state toggleOptionSelection(option, force) { if (force === true || force === false) { option.selected = force; } else { option.selected = !option.selected; } this.selectionChanged(); } // This method must be called whenever the selection changes. It will update the selected options cache, the current // value, and the display value selectionChanged() { var _a, _b, _c; const options = this.getAllOptions(); this.selectedOptions = options.filter((el) => el.selected); const cachedValueHasChanged = this.valueHasChanged; if (this.multiple) { this.value = this.selectedOptions.map((el) => el.value); if (this.placeholder && this.value.length === 0) { this.displayLabel = ""; } else { this.displayLabel = this.localize.term("numOptionsSelected", this.selectedOptions.length); } } else { const selectedOption = this.selectedOptions[0]; this.value = (_a = selectedOption == null ? void 0 : selectedOption.value) != null ? _a : ""; this.displayLabel = (_c = (_b = selectedOption == null ? void 0 : selectedOption.getTextLabel) == null ? void 0 : _b.call(selectedOption)) != null ? _c : ""; } this.valueHasChanged = cachedValueHasChanged; this.updateComplete.then(() => { this.formControlController.updateValidity(); }); } 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>`; } else if (index === this.maxOptionsVisible) { return html`<syn-tag size=${this.size}>+${this.selectedOptions.length - index}</syn-tag>`; } return html``; }); } handleInvalid(event) { this.formControlController.setValidity(false); this.formControlController.emitInvalidEvent(event); } handleDelimiterChange() { this.getAllOptions().forEach((option) => { option.delimiter = this.delimiter; }); } handleDisabledChange() { if (this.disabled) { this.open = false; this.handleOpenChange(); } } firstUpdated() { this.isInitialized = true; } 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(); } } } willUpdate(changedProperties) { super.willUpdate(changedProperties); if (!this.isInitialized && !this.defaultValue && this.value) { this.defaultValue = this.value; this.valueHasChanged = false; } } attributeChangedCallback(name, oldVal, newVal) { super.attributeChangedCallback(name, oldVal, newVal); if (name === "value") { const cachedValueHasChanged = this.valueHasChanged; this.value = this.defaultValue; this.valueHasChanged = cachedValueHasChanged; } } handleValueChange() { if (!this.valueHasChanged) { const cachedValueHasChanged = this.valueHasChanged; this.value = this.defaultValue; this.valueHasChanged = cachedValueHasChanged; } const allOptions = this.getAllOptions(); const value = Array.isArray(this.value) ? this.value : [this.value]; const valueString = value.map(String); const allSelectedOptions = allOptions.filter( (el) => valueString.includes(String(el.value)) ); this.setSelectedOptions(allSelectedOptions); } async handleOpenChange() { if (this.open && !this.disabled) { this.setCurrentOption(this.selectedOptions[0] || this.getFirstOption()); this.emit("syn-show"); this.addOpenListeners(); await stopAnimations(this); this.listbox.hidden = false; this.popup.active = true; requestAnimationFrame(() => { this.setCurrentOption(this.currentOption); }); const { keyframes, options } = getAnimation(this, "select.show", { dir: this.localize.dir() }); await animateTo(this.popup.popup, keyframes, options); if (this.currentOption) { scrollIntoView(this.currentOption, this.listbox, "vertical", "auto"); } this.emit("syn-after-show"); } else { this.emit("syn-hide"); this.removeOpenListeners(); await stopAnimations(this); const { keyframes, options } = getAnimation(this, "select.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. */ async show() { if (this.open || this.disabled) { this.open = false; return void 0; } this.open = true; return waitForEvent(this, "syn-after-show"); } /** Hides the listbox. */ async hide() { if (!this.open || this.disabled) { 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(); } render() { var _a; const hasValue = isAllowedValue(this.value); 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; const hasClearIcon = this.clearable && !this.disabled && hasValue; const isPlaceholderVisible = this.placeholder && this.value && !hasValue; return html` <div part="form-control" class=${classMap({ "form-control": true, "form-control--small": this.size === "small", "form-control--medium": this.size === "medium", "form-control--large": this.size === "large", "form-control--has-label": hasLabel, "form-control--has-help-text": hasHelpText })} > <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({ select: true, "select--standard": true, "select--open": this.open, "select--disabled": this.disabled, "select--multiple": this.multiple, "select--focused": this.hasFocus, "select--placeholder-visible": isPlaceholderVisible, "select--top": this.placement === "top", "select--bottom": this.placement === "bottom", "select--small": this.size === "small", "select--medium": this.size === "medium", "select--large": this.size === "large" })} placement=${this.placement + "-start"} strategy=${this.hoist ? "fixed" : "absolute"} flip shift sync="width" auto-size="vertical" auto-size-padding="10" exportparts="popup" > <div part="combobox" class="select__combobox" slot="anchor" @keydown=${this.handleComboboxKeyDown} @mousedown=${this.handleComboboxMouseDown} > <slot part="prefix" name="prefix" class="select__prefix"></slot> <input part="display-input" class="select__display-input" type="text" placeholder=${this.placeholder} .disabled=${this.disabled} .value=${this.displayLabel} autocomplete="off" spellcheck="false" autocapitalize="off" readonly 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} /> ${this.multiple ? html`<div part="tags" class="select__tags">${this.tags}</div>` : ""} <input class="select__value-input" type="text" ?disabled=${this.disabled} ?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="select__clear" type="button" aria-label=${this.localize.term("clearEntry")} @mousedown=${this.handleClearMouseDown} @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="select__suffix"></slot> <slot name="expand-icon" part="expand-icon" class="select__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-multiselectable=${this.multiple ? "true" : "false"} aria-labelledby="label" part="listbox" class="select__listbox" tabindex="-1" @mouseup=${this.handleOptionClick} @slotchange=${this.handleDefaultSlotChange} > <slot></slot> </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> `; } }; SynSelect.styles = [component_styles_default, form_control_styles_default, select_styles_default, form_control_custom_styles_default, select_custom_styles_default]; SynSelect.dependencies = { "syn-icon": SynIcon, "syn-popup": SynPopup, "syn-tag": SynTag }; __decorateClass([ query(".select") ], SynSelect.prototype, "popup", 2); __decorateClass([ query(".select__combobox") ], SynSelect.prototype, "combobox", 2); __decorateClass([ query(".select__display-input") ], SynSelect.prototype, "displayInput", 2); __decorateClass([ query(".select__value-input") ], SynSelect.prototype, "valueInput", 2); __decorateClass([ query(".select__listbox") ], SynSelect.prototype, "listbox", 2); __decorateClass([ query(".select__tags") ], SynSelect.prototype, "tagContainer", 2); __decorateClass([ state() ], SynSelect.prototype, "hasFocus", 2); __decorateClass([ state() ], SynSelect.prototype, "displayLabel", 2); __decorateClass([ state() ], SynSelect.prototype, "currentOption", 2); __decorateClass([ state() ], SynSelect.prototype, "selectedOptions", 2); __decorateClass([ state() ], SynSelect.prototype, "valueHasChanged", 2); __decorateClass([ property() ], SynSelect.prototype, "delimiter", 2); __decorateClass([ property() ], SynSelect.prototype, "name", 2); __decorateClass([ state() ], SynSelect.prototype, "value", 1); __decorateClass([ property({ attribute: "value" }) ], SynSelect.prototype, "defaultValue", 2); __decorateClass([ property({ reflect: true }) ], SynSelect.prototype, "size", 2); __decorateClass([ property() ], SynSelect.prototype, "placeholder", 2); __decorateClass([ property({ type: Boolean, reflect: true }) ], SynSelect.prototype, "multiple", 2); __decorateClass([ property({ attribute: "max-options-visible", type: Number }) ], SynSelect.prototype, "maxOptionsVisible", 2); __decorateClass([ property({ type: Boolean, reflect: true }) ], SynSelect.prototype, "disabled", 2); __decorateClass([ property({ type: Boolean }) ], SynSelect.prototype, "clearable", 2); __decorateClass([ property({ type: Boolean, reflect: true }) ], SynSelect.prototype, "open", 2); __decorateClass([ property({ type: Boolean }) ], SynSelect.prototype, "hoist", 2); __decorateClass([ property() ], SynSelect.prototype, "label", 2); __decorateClass([ property({ reflect: true }) ], SynSelect.prototype, "placement", 2); __decorateClass([ property({ attribute: "help-text" }) ], SynSelect.prototype, "helpText", 2); __decorateClass([ property({ reflect: true }) ], SynSelect.prototype, "form", 2); __decorateClass([ property({ type: Boolean, reflect: true }) ], SynSelect.prototype, "required", 2); __decorateClass([ property() ], SynSelect.prototype, "getTag", 2); __decorateClass([ watch("delimiter") ], SynSelect.prototype, "handleDelimiterChange", 1); __decorateClass([ watch("disabled", { waitUntilFirstUpdate: true }) ], SynSelect.prototype, "handleDisabledChange", 1); __decorateClass([ watch(["defaultValue", "value", "delimiter"], { waitUntilFirstUpdate: true }) ], SynSelect.prototype, "handleValueChange", 1); __decorateClass([ watch("open", { waitUntilFirstUpdate: true }) ], SynSelect.prototype, "handleOpenChange", 1); SynSelect = __decorateClass([ enableDefaultSettings("SynSelect") ], SynSelect); setDefaultAnimation("select.show", { keyframes: [ { opacity: 0, scale: 0.9 }, { opacity: 1, scale: 1 } ], options: { duration: 100, easing: "ease" } }); setDefaultAnimation("select.hide", { keyframes: [ { opacity: 1, scale: 1 }, { opacity: 0, scale: 0.9 } ], options: { duration: 100, easing: "ease" } }); export { SynSelect }; //# sourceMappingURL=chunk.EFQNVNFO.js.map