@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
423 lines (422 loc) • 21.7 kB
JavaScript
/*! 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} (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} =${this.inputClickHandler} =${this.keyDownHandler} =${this.changeHandler} =${this.inputHandler} =${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} =${this.handleContentTopSlotChange}></slot><slot aria-hidden=true></slot><slot name=${SLOTS.contentBottom} =${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
};