UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

423 lines (422 loc) • 21.7 kB
/*! All material copyright ESRI, All Rights Reserved, unless otherwise specified. See https://github.com/Esri/calcite-design-system/blob/dev/LICENSE.md for details. v3.2.1 */ import { c as customElement } from "../../chunks/runtime.js"; import { ref } from "lit-html/directives/ref.js"; import { repeat } from "lit-html/directives/repeat.js"; import { nothing, html } from "lit"; import { LitElement, createEvent, stringOrBoolean, safeClassMap } from "@arcgis/lumina"; import { useWatchAttributes } from "@arcgis/lumina/controllers"; import { debounce, escapeRegExp } from "lodash-es"; import { b as defaultMenuPlacement, r as reposition, c as connectFloatingUI, a as disconnectFloatingUI, F as FloatingCSS } from "../../chunks/floating-ui.js"; import { u as updateHostInteraction, I as InteractiveContainer } from "../../chunks/interactive.js"; import { o as onToggleOpenCloseComponent } from "../../chunks/openCloseComponent.js"; import { c as connectLabel, d as disconnectLabel } from "../../chunks/label.js"; import { c as connectForm, a as afterConnectDefaultValueSet, d as disconnectForm, s as submitForm, H as HiddenFormInputSlot } from "../../chunks/form.js"; import { a as slotChangeHasAssignedElement } from "../../chunks/dom.js"; import { g as guid } from "../../chunks/guid.js"; import { u as useT9n } from "../../chunks/useT9n.js"; import { V as Validation } from "../../chunks/Validation.js"; import { c as createObserver } from "../../chunks/observers.js"; import { c as componentFocusable } from "../../chunks/component.js"; import { css } from "@lit/reactive-element/css-tag.js"; const styles = css`:host([disabled]){cursor:default;-webkit-user-select:none;user-select:none;opacity:var(--calcite-opacity-disabled)}:host([disabled]) *,:host([disabled]) ::slotted(*){pointer-events:none}:host{position:relative;display:block}.input-container{position:relative;display:flex;flex:1 1 auto;flex-wrap:nowrap}.input{width:100%;--calcite-input-prefix-size: var(--calcite-autocomplete-input-prefix-size);--calcite-input-suffix-size: var(--calcite-autocomplete-input-suffix-size);--calcite-input-background-color: var(--calcite-autocomplete-input-background-color);--calcite-input-border-color: var(--calcite-autocomplete-input-border-color);--calcite-input-corner-radius: var(--calcite-autocomplete-input-corner-radius);--calcite-input-shadow: var(--calcite-autocomplete-input-shadow);--calcite-input-icon-color: var(--calcite-autocomplete-input-icon-color);--calcite-input-text-color: var(--calcite-autocomplete-input-text-color);--calcite-input-placeholder-text-color: var(--calcite-autocomplete-input-placeholder-text-color);--calcite-input-actions-background-color: var(--calcite-autocomplete-input-actions-background-color);--calcite-input-actions-background-color-hover: var(--calcite-autocomplete-input-actions-background-color-hover);--calcite-input-actions-background-color-press: var(--calcite-autocomplete-input-actions-background-color-press);--calcite-input-actions-icon-color: var(--calcite-autocomplete-input-actions-icon-color);--calcite-input-actions-icon-color-hover: var(--calcite-autocomplete-input-actions-icon-color-hover);--calcite-input-actions-icon-color-press: var(--calcite-autocomplete-input-actions-icon-color-press);--calcite-input-loading-background-color: var(--calcite-autocomplete-input-loading-background-color);--calcite-input-loading-fill-color: var(--calcite-autocomplete-input-loading-fill-color);--calcite-input-prefix-background-color: var(--calcite-autocomplete-input-prefix-background-color);--calcite-input-prefix-text-color: var(--calcite-autocomplete-input-prefix-text-color);--calcite-input-suffix-background-color: var(--calcite-autocomplete-input-suffix-background-color);--calcite-input-suffix-text-color: var(--calcite-autocomplete-input-suffix-text-color)}:host([disabled]) ::slotted([calcite-hydrated][disabled]),:host([disabled]) [calcite-hydrated][disabled]{opacity:1}.interaction-container{display:contents}.content-container{box-sizing:border-box;width:100%}.floating-ui-container{--calcite-floating-ui-z-index: var(--calcite-z-index-dropdown);inline-size:max-content;display:none;max-inline-size:100vw;max-block-size:100vh;inset-block-start:0;left:0;z-index:var(--calcite-floating-ui-z-index)}.floating-ui-container .calcite-floating-ui-anim{position:relative;transition:var(--calcite-floating-ui-transition);transition-property:inset,left,opacity;opacity:0;box-shadow:0 0 16px #00000029;z-index:var(--calcite-z-index);border-radius:.25rem}.floating-ui-container[data-placement^=bottom] .calcite-floating-ui-anim{inset-block-start:-5px}.floating-ui-container[data-placement^=top] .calcite-floating-ui-anim{inset-block-start:5px}.floating-ui-container[data-placement^=left] .calcite-floating-ui-anim{left:5px}.floating-ui-container[data-placement^=right] .calcite-floating-ui-anim{left:-5px}.floating-ui-container[data-placement] .calcite-floating-ui-anim--active{opacity:1;inset-block-start:0;left:0}.content-container .calcite-floating-ui-anim{max-height:45vh;width:100%;overflow-y:auto;color:var(--calcite-autocomplete-text-color, var(--calcite-color-text-1));background-color:var(--calcite-autocomplete-background-color, var(--calcite-color-foreground-1));border-radius:var(--calcite-autocomplete-corner-radius, var(--calcite-corner-radius-round))}.content--hidden{display:none}@media (forced-colors: active){.floating-ui-container--active{border:1px solid canvasText}}.screen-readers-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.validation-container{display:flex;flex-direction:column;align-items:flex-start;align-self:stretch}:host([scale=m]) .validation-container,:host([scale=l]) .validation-container{padding-block-start:.5rem}:host([scale=s]) .validation-container{padding-block-start:.25rem}::slotted(input[slot=hidden-form-input]){margin:0!important;opacity:0!important;outline:none!important;padding:0!important;position:absolute!important;inset:0!important;transform:none!important;-webkit-appearance:none!important;z-index:-1!important}:host([hidden]){display:none}[hidden]{display:none}`; const SLOTS = { contentBottom: "content-bottom", contentTop: "content-top" }; const CSS = { inputContainer: "input-container", input: "input", contentContainer: "content-container", contentAnimation: "content-animation", content: "content", contentHidden: "content--hidden", floatingUIContainer: "floating-ui-container", floatingUIContainerActive: "floating-ui-container--active", screenReadersOnly: "screen-readers-only" }; const IDS = { validationMessage: "autocompleteValidationMessage" }; const groupItemSelector = "calcite-autocomplete-item-group"; const itemSelector = "calcite-autocomplete-item"; class Autocomplete extends LitElement { constructor() { super(); this.guid = guid(); this.attributeWatch = useWatchAttributes(["autofocus", "enterkeyhint", "inputmode"], this.handleGlobalAttributesChanged); this.inputId = `autocomplete-input-${this.guid}`; this.listId = `autocomplete-list-${this.guid}`; this.messages = useT9n(); this.transitionProp = "opacity"; this.mutationObserver = createObserver("mutation", () => this.getAllItemsDebounced()); this.resizeObserver = createObserver("resize", () => { this.setFloatingElSize(); }); this.getAllItemsDebounced = debounce(this.getAllItems, 0); this.activeDescendant = ""; this.activeIndex = -1; this.hasContentBottom = false; this.hasContentTop = false; this.items = []; this.groups = []; this.alignment = "start"; this.disabled = false; this.iconFlipRtl = false; this.loading = false; this.open = false; this.overlayPositioning = "absolute"; this.placement = defaultMenuPlacement; this.readOnly = false; this.required = false; this.scale = "m"; this.status = "idle"; this.validity = { valid: false, badInput: false, customError: false, patternMismatch: false, rangeOverflow: false, rangeUnderflow: false, stepMismatch: false, tooLong: false, tooShort: false, typeMismatch: false, valueMissing: false }; this.value = ""; this.calciteAutocompleteBeforeClose = createEvent({ cancelable: false }); this.calciteAutocompleteBeforeOpen = createEvent({ cancelable: false }); this.calciteAutocompleteChange = createEvent({ cancelable: false }); this.calciteAutocompleteClose = createEvent({ cancelable: false }); this.calciteAutocompleteOpen = createEvent({ cancelable: false }); this.calciteAutocompleteTextChange = createEvent({ cancelable: false }); this.calciteAutocompleteTextInput = createEvent({ cancelable: false }); this.listenOn(document, "click", this.documentClickHandler); this.listen("calciteInternalAutocompleteItemSelect", this.handleInternalAutocompleteItemSelect); } static { this.properties = { activeDescendant: [16, {}, { state: true }], activeIndex: [16, {}, { state: true }], hasContentBottom: [16, {}, { state: true }], hasContentTop: [16, {}, { state: true }], items: [16, {}, { state: true }], groups: [16, {}, { state: true }], isOpen: [16, {}, { state: true }], enabledItems: [16, {}, { state: true }], alignment: [3, {}, { reflect: true }], autocomplete: [0, {}, { attribute: false }], disabled: [7, {}, { reflect: true, type: Boolean }], flipPlacements: [0, {}, { attribute: false }], form: [3, {}, { reflect: true }], icon: [3, { converter: stringOrBoolean }, { reflect: true }], iconFlipRtl: [7, {}, { reflect: true, type: Boolean }], inputValue: 1, label: 1, loading: [7, {}, { reflect: true, type: Boolean }], maxLength: [11, {}, { reflect: true, type: Number }], messageOverrides: [0, {}, { attribute: false }], minLength: [11, {}, { reflect: true, type: Number }], name: [3, {}, { reflect: true }], open: [7, {}, { reflect: true, type: Boolean }], overlayPositioning: [3, {}, { reflect: true }], pattern: 1, placeholder: 1, placement: [3, {}, { reflect: true }], prefixText: 1, readOnly: [7, {}, { reflect: true, type: Boolean }], required: [7, {}, { reflect: true, type: Boolean }], scale: [3, {}, { reflect: true }], status: [3, {}, { reflect: true }], suffixText: 1, validationIcon: [3, { converter: stringOrBoolean }, { reflect: true }], validationMessage: 1, validity: [0, {}, { attribute: false }], value: 1 }; } static { this.styles = styles; } get isOpen() { return this.open && (this.hasContentTop || this.hasContentBottom || this.items.length > 0); } get enabledItems() { return this.items.filter((item) => !item.disabled); } async reposition(delayed = false) { const { floatingEl, referenceEl, placement, overlayPositioning, flipPlacements } = this; return reposition(this, { floatingEl, referenceEl, overlayPositioning, placement, flipPlacements, type: "menu" }, delayed); } async scrollContentTo(options) { this.transitionEl?.scrollTo(options); } async selectText() { return this.referenceEl.selectText(); } async setFocus() { await componentFocusable(this); return this.referenceEl.setFocus(); } connectedCallback() { super.connectedCallback(); this.mutationObserver?.observe(this.el, { childList: true, subtree: true }); connectLabel(this); connectForm(this); this.defaultInputValue = this.inputValue || ""; this.getAllItemsDebounced(); connectFloatingUI(this); } async load() { this.getAllItemsDebounced(); } willUpdate(changes) { if (changes.has("disabled") && (this.hasUpdated || this.disabled !== false)) { this.handleDisabledChange(this.disabled); } if (changes.has("flipPlacements")) { this.reposition(true); } if (changes.has("open") && (this.hasUpdated || this.open !== false)) { this.openHandler(); } if (changes.has("overlayPositioning") && (this.hasUpdated || this.overlayPositioning !== "absolute")) { this.reposition(true); } if (changes.has("placement") && (this.hasUpdated || this.placement !== defaultMenuPlacement)) { this.reposition(true); } let itemsAndGroupsUpdated = false; if (changes.has("inputValue") && (this.hasUpdated || this.inputValue)) { this.inputValueMatchPattern = this.inputValue && new RegExp(`(${escapeRegExp(this.inputValue)})`, "i"); this.updateItems(); this.updateGroups(); itemsAndGroupsUpdated = true; } if (!itemsAndGroupsUpdated && changes.has("scale") && (this.hasUpdated || this.scale !== "m")) { this.updateItems(); this.updateGroups(); itemsAndGroupsUpdated = true; } if (!itemsAndGroupsUpdated && changes.has("activeIndex") && (this.hasUpdated || this.activeIndex !== -1)) { this.updateItems(); } } updated() { updateHostInteraction(this); } loaded() { afterConnectDefaultValueSet(this, this.value || ""); this.defaultInputValue = this.inputValue || ""; connectFloatingUI(this); } disconnectedCallback() { super.disconnectedCallback(); this.mutationObserver?.disconnect(); this.resizeObserver?.disconnect(); disconnectLabel(this); disconnectForm(this); disconnectFloatingUI(this); } setFloatingElSize() { const { referenceEl, floatingEl } = this; if (!referenceEl || !floatingEl) { return; } floatingEl.style.inlineSize = `${referenceEl.clientWidth}px`; } handleGlobalAttributesChanged() { this.requestUpdate(); } handleDisabledChange(value) { if (!value) { this.open = false; } } openHandler() { onToggleOpenCloseComponent(this); if (!this.open) { this.activeIndex = -1; } if (this.disabled) { this.open = false; return; } this.setFloatingElSize(); this.reposition(true); } async documentClickHandler(event) { if (this.disabled || event.composedPath().includes(this.el)) { return; } this.open = false; } async handleInternalAutocompleteItemSelect(event) { this.value = event.target.value; event.stopPropagation(); this.emitChange(); await this.setFocus(); this.open = false; } onLabelClick() { this.setFocus(); } onFormReset() { this.inputValue = this.defaultInputValue; } onBeforeOpen() { this.calciteAutocompleteBeforeOpen.emit(); } onOpen() { this.calciteAutocompleteOpen.emit(); } onBeforeClose() { this.calciteAutocompleteBeforeClose.emit(); } onClose() { this.calciteAutocompleteClose.emit(); } emitChange() { this.calciteAutocompleteChange.emit(); } updateGroups() { this.groups.forEach((group, index, items) => { group.scale = this.scale; if (index === 0) { group.disableSpacing = true; } const nextGroupItem = items[index + 1]; if (nextGroupItem) { nextGroupItem.disableSpacing = group.children.length === 0; } }); } updateItems() { let activeDescendant = null; this.items.forEach((item) => { item.scale = this.scale; item.inputValueMatchPattern = this.inputValueMatchPattern; }); this.enabledItems.forEach((item, index) => { const isActive = index === this.activeIndex; if (isActive) { activeDescendant = item.guid; } item.active = isActive; }); this.activeDescendant = activeDescendant; } handleInputFocus() { this.open = true; } handleContentTopSlotChange(event) { this.hasContentTop = slotChangeHasAssignedElement(event); } handleContentBottomSlotChange(event) { this.hasContentBottom = slotChangeHasAssignedElement(event); } getAllItems() { const { el } = this; this.groups = Array.from(el.querySelectorAll(groupItemSelector)); this.items = Array.from(el.querySelectorAll(itemSelector)); this.updateItems(); this.updateGroups(); } setReferenceEl(el) { this.referenceEl = el; if (!el) { return; } this.resizeObserver?.observe(el); connectFloatingUI(this); } keyDownHandler(event) { const { defaultPrevented, key } = event; if (defaultPrevented) { return; } const { open, activeIndex, enabledItems } = this; const activeItem = enabledItems.length && activeIndex > -1 ? enabledItems[activeIndex] : null; switch (key) { case "Escape": if (open) { this.open = false; event.preventDefault(); } break; case "Tab": this.open = false; break; case "Enter": if (open && activeItem) { this.value = activeItem.value; this.emitChange(); this.open = false; event.preventDefault(); } else if (!event.defaultPrevented) { if (submitForm(this)) { event.preventDefault(); } } break; case "ArrowDown": if (enabledItems.length) { this.open = true; this.activeIndex = activeIndex !== -1 ? Math.min(activeIndex + 1, enabledItems.length - 1) : 0; this.scrollToActiveItem(); event.preventDefault(); } break; case "ArrowUp": if (enabledItems.length) { this.open = true; this.activeIndex = activeIndex !== -1 ? Math.max(activeIndex - 1, 0) : enabledItems.length - 1; this.scrollToActiveItem(); event.preventDefault(); } break; case "Home": if (enabledItems.length) { this.open = true; this.activeIndex = 0; this.scrollToActiveItem(); event.preventDefault(); } break; case "End": if (enabledItems.length) { this.open = true; this.activeIndex = enabledItems.length - 1; this.scrollToActiveItem(); event.preventDefault(); } break; } } scrollToActiveItem() { this.enabledItems[this.activeIndex]?.scrollIntoView({ block: "nearest" }); } changeHandler(event) { event.stopPropagation(); this.inputValue = event.target.value; this.calciteAutocompleteTextChange.emit(); } inputClickHandler(event) { if (event.defaultPrevented) { return; } this.open = true; } inputHandler(event) { event.stopPropagation(); this.inputValue = event.target.value; this.open = this.inputValue?.length > 0; this.calciteAutocompleteTextInput.emit(); } setFloatingEl(el) { this.floatingEl = el; connectFloatingUI(this); } setTransitionEl(el) { if (!el) { return; } this.transitionEl = el; } render() { const { disabled, listId, inputId, isOpen } = this; const autofocus = this.el.autofocus; const enterKeyHint = this.el.enterKeyHint; const inputMode = this.el.inputMode; return InteractiveContainer({ disabled, children: html`<div class=${safeClassMap(CSS.inputContainer)}><calcite-input .alignment=${this.alignment} aria-activedescendant=${this.activeDescendant ?? nothing} aria-controls=${listId ?? nothing} aria-owns=${listId ?? nothing} aria-autocomplete=list .ariaExpanded=${isOpen} aria-haspopup=listbox .autocomplete=${this.autocomplete} .autofocus=${autofocus} class=${safeClassMap(CSS.input)} clearable .disabled=${disabled} enterkeyhint=${enterKeyHint ?? nothing} .form=${this.form} .icon=${this.icon ?? true} .iconFlipRtl=${this.iconFlipRtl} id=${inputId ?? nothing} inputmode=${inputMode ?? nothing} .label=${this.label} .loading=${this.loading} .maxLength=${this.maxLength} .messageOverrides=${this.messages} .minLength=${this.minLength} .name=${this.name} @click=${this.inputClickHandler} @keydown=${this.keyDownHandler} @calciteInputChange=${this.changeHandler} @calciteInputInput=${this.inputHandler} @calciteInternalInputFocus=${this.handleInputFocus} .pattern=${this.pattern} .placeholder=${this.placeholder} .prefixText=${this.prefixText} .readOnly=${this.readOnly} role=combobox .scale=${this.scale} .status=${this.status} .suffixText=${this.suffixText} type=search .value=${this.inputValue} ${ref(this.setReferenceEl)}></calcite-input>${this.renderListBox()}<div class=${safeClassMap({ [CSS.contentContainer]: true, [CSS.floatingUIContainer]: true, [CSS.floatingUIContainerActive]: isOpen })} ${ref(this.setFloatingEl)}><div class=${safeClassMap({ [CSS.contentAnimation]: true, [FloatingCSS.animation]: true, [FloatingCSS.animationActive]: isOpen })} ${ref(this.setTransitionEl)}><div class=${safeClassMap({ [CSS.content]: true, [CSS.contentHidden]: !isOpen })}><slot name=${SLOTS.contentTop} @slotchange=${this.handleContentTopSlotChange}></slot><slot aria-hidden=true></slot><slot name=${SLOTS.contentBottom} @slotchange=${this.handleContentBottomSlotChange}></slot></div></div></div></div>${HiddenFormInputSlot({ component: this })}${this.validationMessage && this.status === "invalid" ? Validation({ icon: this.validationIcon, id: IDS.validationMessage, message: this.validationMessage, scale: this.scale, status: this.status }) : null}` }); } renderListBox() { return html`<ul aria-labelledby=${this.inputId ?? nothing} class=${safeClassMap(CSS.screenReadersOnly)} id=${this.listId ?? nothing} role=listbox tabindex=-1>${this.renderListBoxOptions()}</ul>`; } renderListBoxOptions() { return repeat(this.items.filter((item) => !!(item.label || item.heading)), (item) => item.guid, (item) => html`<li .ariaDisabled=${item.disabled} .ariaLabel=${item.label} id=${item.guid ?? nothing} role=option tabindex=-1>${item.heading}${item.description}</li>`); } } customElement("calcite-autocomplete", Autocomplete); export { Autocomplete };