@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
419 lines (418 loc) • 24.2 kB
JavaScript
/* COPYRIGHT Esri - https://js.arcgis.com/5.0/LICENSE.txt */
import { c as customElement } from "../../chunks/runtime.js";
import { repeat } from "lit/directives/repeat.js";
import { css, nothing, html } from "lit";
import { LitElement, createEvent, stringOrBoolean, safeClassMap } from "@arcgis/lumina";
import { useWatchAttributes } from "@arcgis/lumina/controllers";
import { debounce } from "es-toolkit";
import { escapeRegExp } from "es-toolkit/compat";
import { createRef, ref } from "lit/directives/ref.js";
import { d as defaultMenuPlacement, r as reposition, c as connectFloatingUI, a as disconnectFloatingUI, F as FloatingCSS } from "../../chunks/floating-ui.js";
import { c as connectLabel, d as disconnectLabel, g as getLabelText } 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 { s as slotChangeHasAssignedElement } from "../../chunks/dom.js";
import { g as guid } from "../../chunks/guid.js";
import { u as useT9n } from "../../chunks/useT9n.js";
import { u as useCancelable } from "../../chunks/useCancelable.js";
import { I as InternalLabel } from "../../chunks/InternalLabel.js";
import { V as Validation } from "../../chunks/Validation.js";
import { c as createObserver, u as updateRefObserver } from "../../chunks/observers.js";
import { u as useSetFocus } from "../../chunks/useSetFocus.js";
import { u as useInteractive } from "../../chunks/useInteractive.js";
import { t as toggleOpenClose } from "../../chunks/openCloseComponent.js";
import { u as useTopLayer } from "../../chunks/useTopLayer.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-text-color: var(--calcite-autocomplete-input-prefix-text-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{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)}@starting-style{.floating-ui-container{opacity:0;inset-block-start:0;left:0}}:host([top-layer-disabled]) .floating-ui-container{--calcite-floating-ui-z-index: var(--calcite-z-index-dropdown)}.floating-ui-container[popover]{padding:0;margin:0;border:none;background-color:transparent;overflow:visible;display:none}.floating-ui-container:popover-open{display:block}.floating-ui-container .calcite-floating-ui-anim{position:relative;transition-duration:var(--calcite-floating-ui-transition);transition-property:inset-block-start,left,opacity,display;transition-behavior:allow-discrete;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}@starting-style{.floating-ui-container[data-placement] .calcite-floating-ui-anim--active{opacity:0}}.content-container .calcite-floating-ui-anim{width:100%;overflow-y:auto;max-block-size:var(--calcite-autocomplete-menu-max-size-y, 45vh);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}.internal-label-alignment--center{align-items:center}.internal-label-alignment--end{align-items:end}.internal-label--container{display:flex;justify-content:space-between;color:var(--calcite-color-text-1)}.internal-label-required--indicator{font-weight:var(--calcite-font-weight-medium);color:var(--calcite-color-status-danger);padding-inline:var(--calcite-spacing-base)}.internal-label-required--indicator:hover{cursor:help}.internal-label--text{line-height:1}:host([scale=s]) .internal-label-spacing--bottom{margin-block-end:var(--calcite-spacing-xxs)}:host([scale=s]) .internal-label-spacing-inline--end{margin-inline-end:var(--calcite-spacing-sm)}:host([scale=s]) .internal-label-spacing-inline--start{margin-inline-start:var(--calcite-spacing-sm)}:host([scale=s]) .internal-label--text{font-size:var(--calcite-font-size--2)}:host([scale=m]) .internal-label-spacing--bottom{margin-block-end:var(--calcite-spacing-sm)}:host([scale=m]) .internal-label-spacing-inline--end{margin-inline-end:var(--calcite-spacing-sm)}:host([scale=m]) .internal-label-spacing-inline--start{margin-inline-start:var(--calcite-spacing-sm)}:host([scale=m]) .internal-label--text{font-size:var(--calcite-font-size--1)}:host([scale=l]) .internal-label-spacing--bottom{margin-block-end:var(--calcite-spacing-sm)}:host([scale=l]) .internal-label-spacing-inline--end{margin-inline-end:var(--calcite-spacing-md)}:host([scale=l]) .internal-label-spacing-inline--start{margin-inline-start:var(--calcite-spacing-md)}:host([scale=l]) .internal-label--text{font-size:var(--calcite-font-size-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;opacity:0;outline:none;padding:0;position:absolute;inset:0;transform:none;-webkit-appearance:none;z-index:-1}: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 idPrefix = "autocomplete";
const IDS = {
validationMessage: "autocompleteValidationMessage",
input: (id) => `${idPrefix}-input-${id}`,
list: (id) => `${idPrefix}-list-${id}`
};
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 = IDS.input(this.guid);
this.listId = IDS.list(this.guid);
this.messages = useT9n();
this.transitionProp = "opacity";
this.transitionRef = createRef();
this.mutationObserver = createObserver("mutation", () => this.getAllItemsDebounced());
this.focusSetter = useSetFocus()(this);
this.resizeObserver = createObserver("resize", () => {
this.setFloatingElSize();
});
this.cancelable = useCancelable()(this);
this.getAllItemsDebounced = debounce(this.getAllItems, 0);
this.interactiveContainer = useInteractive(this);
this.topLayer = useTopLayer({
target: () => this.floatingEl
})(this);
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.topLayerDisabled = false;
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("calciteAutocompleteItemSelect", this.handleAutocompleteItemSelect);
}
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 }], alignment: [3, {}, { reflect: true }], autocomplete: 1, disabled: [7, {}, { reflect: true, type: Boolean }], flipPlacements: [0, {}, { attribute: false }], form: [3, {}, { reflect: true }], icon: [3, { converter: stringOrBoolean, type: String }, { reflect: true }], iconFlipRtl: [7, {}, { reflect: true, type: Boolean }], inputValue: 1, label: 1, labelText: 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, topLayerDisabled: [7, {}, { reflect: true, type: Boolean }], validationIcon: [3, { converter: stringOrBoolean, type: String }, { 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.transitionRef.value?.scrollTo(options);
}
async selectText() {
return this.referenceEl.selectText();
}
async setFocus(options) {
return this.focusSetter(() => this.referenceEl, options);
}
connectedCallback() {
super.connectedCallback();
this.mutationObserver?.observe(this.el, { childList: true, subtree: true });
connectLabel(this);
connectForm(this);
this.defaultInputValue = this.inputValue || "";
this.getAllItemsDebounced();
connectFloatingUI(this);
this.cancelable.add(this.getAllItemsDebounced);
}
async load() {
this.getAllItemsDebounced();
}
willUpdate(changes) {
if (changes.has("disabled") && (this.hasUpdated || this.disabled !== false)) {
this.handleDisabledChange(this.disabled);
}
if (changes.has("open") && (this.hasUpdated || this.open !== false)) {
this.openHandler();
}
if (changes.has("flipPlacements") || changes.has("overlayPositioning") && (this.hasUpdated || this.overlayPositioning !== "absolute") || 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();
}
}
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() {
if (this.disabled) {
this.open = false;
return;
}
if (!this.open) {
this.activeIndex = -1;
}
toggleOpenClose(this);
this.setFloatingElSize();
this.reposition(true);
}
async documentClickHandler(event) {
if (this.disabled || event.composedPath().includes(this.el)) {
return;
}
this.open = false;
}
async handleAutocompleteItemSelect(event) {
this.value = event.target.value;
this.emitChange();
await this.setFocus();
this.open = false;
}
onLabelClick() {
this.setFocus();
}
onFormReset() {
this.inputValue = this.defaultInputValue;
}
onBeforeOpen() {
this.calciteAutocompleteBeforeOpen.emit();
this.topLayer.show();
}
onOpen() {
this.calciteAutocompleteOpen.emit();
}
onBeforeClose() {
this.calciteAutocompleteBeforeClose.emit();
}
onClose() {
this.calciteAutocompleteClose.emit();
this.topLayer.hide();
}
emitChange() {
this.calciteAutocompleteChange.emit();
}
updateGroups() {
this.groups.forEach((group, index, items) => {
group.scale = this.scale;
group.position = index;
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) {
updateRefObserver(this.resizeObserver, this.referenceEl, el);
this.referenceEl = 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;
activeItem.emitSelectEvent();
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);
}
render() {
const { disabled, listId, inputId, isOpen } = this;
const autofocus = this.el.autofocus;
const enterKeyHint = this.el.enterKeyHint;
const inputMode = this.el.inputMode;
return this.interactiveContainer({ disabled, children: html`${this.labelText && InternalLabel({ labelText: this.labelText, onClick: this.onLabelClick, required: this.required, tooltipText: this.messages.required }) || ""}<div class=${safeClassMap(CSS.inputContainer)}><calcite-input .alignment=${this.alignment} aria-activedescendant=${this.activeDescendant ?? nothing} aria-controls=${listId ?? nothing} aria-label=${getLabelText(this) ?? 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} .required=${this.required} 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
})} popover=manual ${ref(this.setFloatingEl)}><div class=${safeClassMap({
[CSS.contentAnimation]: true,
[FloatingCSS.animation]: true,
[FloatingCSS.animationActive]: isOpen
})} ${ref(this.transitionRef)}><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} aria-live=polite 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
};