UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

433 lines (432 loc) 19 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 { keyed } from "lit-html/directives/keyed.js"; import { nothing, html } from "lit"; import { LitElement, createEvent, stringOrBoolean, safeClassMap } from "@arcgis/lumina"; import { c as connectLabel, d as disconnectLabel } from "../../chunks/label.js"; import { u as updateHostInteraction, I as InteractiveContainer } from "../../chunks/interactive.js"; import { c as componentFocusable } from "../../chunks/component.js"; import { c as connectForm, a as afterConnectDefaultValueSet, d as disconnectForm, H as HiddenFormInputSlot } from "../../chunks/form.js"; import { u as useT9n } from "../../chunks/useT9n.js"; import { g as getDateTimeFormat } from "../../chunks/locale.js"; import { css } from "@lit/reactive-element/css-tag.js"; const CSS = { offset: "offset" }; const hourToMinutes = 60; function timeZoneOffsetToDecimal(shortOffsetTimeZoneName) { const minusSign = "−"; const hyphen = "-"; return shortOffsetTimeZoneName.replace(":15", ".25").replace(":30", ".5").replace(":45", ".75").replace(minusSign, hyphen); } function toOffsetValue(timeZoneName, referenceDateInMs) { const offset = getTimeZoneShortOffset(timeZoneName, "en-US", referenceDateInMs).replace("GMT", ""); if (offset === "") { return 0; } return Number(timeZoneOffsetToDecimal(offset)) * hourToMinutes; } function getUserTimeZoneOffset() { const localDate = /* @__PURE__ */ new Date(); return localDate.getTimezoneOffset() * -1; } function getUserTimeZoneName() { const dateFormatter = new Intl.DateTimeFormat(); return dateFormatter.resolvedOptions().timeZone; } async function getNormalizer(mode) { if (mode === "offset") { return (timeZone) => timeZone; } const { normalize } = await import("timezone-groups/utils/time-zones"); return normalize; } async function createTimeZoneItems(locale, messages, mode, referenceDate, standardTime) { if (mode === "name") { const { groupByName } = await import("timezone-groups/groupByName"); const groups2 = await groupByName(); return groups2.map(({ label: timeZone }) => { const label = timeZone; const value = timeZone; return { label, value, metadata: { filterValue: timeZone } }; }).filter((group) => !!group).sort(); } const effectiveLocale = standardTime === "user" ? locale : ( // we use locales that will always yield a short offset that matches `standardTime` standardTime === "utc" ? "fr" : "en-GB" ); const referenceDateInMs = referenceDate.getTime(); if (mode === "region") { const [{ groupByRegion }, { getCountry, global: globalLabel }] = await Promise.all([ import("timezone-groups/groupByRegion"), import("timezone-groups/utils/region") ]); const groups2 = await groupByRegion(); return groups2.map(({ label: region, tzs }) => { tzs.sort((timeZoneA, timeZoneB) => { const labeledTimeZoneA = getTimeZoneLabel(timeZoneA, messages); const labeledTimeZoneB = getTimeZoneLabel(timeZoneB, messages); const gmtTimeZoneString = "Etc/GMT"; if (timeZoneA.startsWith(gmtTimeZoneString) && timeZoneB.startsWith(gmtTimeZoneString)) { const offsetStringA = timeZoneA.substring(gmtTimeZoneString.length); const offsetStringB = timeZoneB.substring(gmtTimeZoneString.length); const offsetA = offsetStringA === "" ? 0 : parseInt(offsetStringA); const offsetB = offsetStringB === "" ? 0 : parseInt(offsetStringB); return offsetB - offsetA; } return labeledTimeZoneA.localeCompare(labeledTimeZoneB); }); return { label: getMessageOrKeyFallback(messages, region), items: tzs.map((timeZone) => { const decimalOffset = timeZoneOffsetToDecimal( getTimeZoneShortOffset(timeZone, effectiveLocale, referenceDateInMs) ); const label = getTimeZoneLabel(timeZone, messages); const filterValue = region === globalLabel ? ( // we rely on the label for search since GMT items have their signs inverted (see https://en.wikipedia.org/wiki/Tz_database#Area) // in addition to the label we also add "Global" and "Etc" to allow searching for these items `${getTimeZoneLabel(globalLabel, messages)} Etc` ) : toUserFriendlyName(timeZone); const countryCode = getCountry(timeZone); const country = getMessageOrKeyFallback(messages, countryCode); return { label, value: timeZone, metadata: { country: country === label ? void 0 : country, filterValue, offset: decimalOffset } }; }) }; }).sort( (groupA, groupB) => groupA.label === globalLabel ? -1 : groupB.label === globalLabel ? 1 : groupA.label.localeCompare(groupB.label) ); } const [{ groupByOffset }, { DateEngine }] = await Promise.all([ import("timezone-groups/groupByOffset"), import("timezone-groups/groupByOffset/strategy/native") ]); const groups = await groupByOffset({ dateEngine: new DateEngine(), groupDateRange: 1, startDate: new Date(referenceDateInMs).toISOString() }); const listFormatter = new Intl.ListFormat(locale, { style: "long", type: "conjunction" }); const offsetTimeZoneNameBlockList = ["Factory", "Etc/UTC"]; groups.forEach((group) => { const indexOffsets = []; let removedSoFar = 0; group.tzs.forEach((tz, index) => { if (offsetTimeZoneNameBlockList.includes(tz)) { removedSoFar++; } indexOffsets[index] = removedSoFar; }); group.tzs = group.tzs.filter((tz) => !offsetTimeZoneNameBlockList.includes(tz)); group.labelTzIdx = group.labelTzIdx.map((index) => index - indexOffsets[index]).filter((index) => index >= 0 && index < group.tzs.length); }); return groups.map(({ labelTzIdx, tzs }) => { const groupRepTz = tzs[0]; const decimalOffset = timeZoneOffsetToDecimal( getTimeZoneShortOffset(groupRepTz, effectiveLocale, referenceDateInMs) ); const value = toOffsetValue(groupRepTz, referenceDateInMs); const tzLabels = labelTzIdx.map((index) => getTimeZoneLabel(tzs[index], messages)); const label = createTimeZoneOffsetLabel(messages, decimalOffset, listFormatter.format(tzLabels)); return { label, value, metadata: { filterValue: tzs.map((tz) => toUserFriendlyName(tz)) } }; }).filter((group) => !!group).sort((groupA, groupB) => groupA.value - groupB.value); } function getTimeZoneLabel(timeZone, messages) { return messages[timeZone] || getCity(timeZone); } function getSelectedRegionTimeZoneLabel(city, country, messages) { const template = messages.timeZoneRegionLabel; return template.replace("{city}", city).replace("{country}", getMessageOrKeyFallback(messages, country)); } function getMessageOrKeyFallback(messages, key) { return messages[key] || key; } function getCity(timeZone) { return timeZone.split("/").pop(); } function toUserFriendlyName(timeZoneName) { return timeZoneName.replace(/_/g, " "); } function createTimeZoneOffsetLabel(messages, offsetLabel, groupLabel) { return messages.timeZoneLabel.replace("{offset}", offsetLabel).replace("{cities}", groupLabel); } function getTimeZoneShortOffset(timeZone, locale, referenceDateInMs = Date.now()) { if (timeZone === "Factory") { timeZone = "Etc/GMT"; } const dateTimeFormat = getDateTimeFormat(locale, { timeZone, timeZoneName: "shortOffset" }); const parts = dateTimeFormat.formatToParts(referenceDateInMs); return parts.find(({ type }) => type === "timeZoneName").value; } function hasGroups(items) { return items[0].items !== void 0; } function flattenTimeZoneItems(timeZoneItems) { return hasGroups(timeZoneItems) ? timeZoneItems.flatMap((item) => item.items) : timeZoneItems; } function findTimeZoneItemByProp(timeZoneItems, prop, valueToMatch) { return valueToMatch == null ? null : flattenTimeZoneItems(timeZoneItems).find( (item) => ( // intentional == to match string to number item[prop] == valueToMatch ) ); } 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{display:block}.offset{white-space:nowrap}:host([hidden]){display:none}[hidden]{display:none}:host([disabled]) ::slotted([calcite-hydrated][disabled]),:host([disabled]) [calcite-hydrated][disabled]{opacity:1}.interaction-container{display:contents}::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}`; class InputTimeZone extends LitElement { constructor() { super(...arguments); this.messages = useT9n({ blocking: true }); this.clearable = false; this.disabled = false; this.maxItems = 0; this.mode = "offset"; this.offsetStyle = "user"; this.open = false; this.overlayPositioning = "absolute"; 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.calciteInputTimeZoneBeforeClose = createEvent({ cancelable: false }); this.calciteInputTimeZoneBeforeOpen = createEvent({ cancelable: false }); this.calciteInputTimeZoneChange = createEvent({ cancelable: false }); this.calciteInputTimeZoneClose = createEvent({ cancelable: false }); this.calciteInputTimeZoneOpen = createEvent({ cancelable: false }); } static { this.properties = { clearable: [7, {}, { reflect: true, type: Boolean }], disabled: [7, {}, { reflect: true, type: Boolean }], form: [3, {}, { reflect: true }], maxItems: [11, {}, { reflect: true, type: Number }], messageOverrides: [0, {}, { attribute: false }], mode: [3, {}, { reflect: true }], name: [3, {}, { reflect: true }], offsetStyle: [3, {}, { reflect: true }], open: [7, {}, { reflect: true, type: Boolean }], overlayPositioning: [3, {}, { reflect: true }], readOnly: [7, {}, { reflect: true, type: Boolean }], referenceDate: 1, required: [7, {}, { reflect: true, type: Boolean }], scale: [3, {}, { reflect: true }], status: [3, {}, { reflect: true }], validationIcon: [3, { converter: stringOrBoolean }, { reflect: true }], validationMessage: 1, validity: [0, {}, { attribute: false }], value: 1 }; } static { this.shadowRootOptions = { mode: "open", delegatesFocus: true }; } static { this.styles = styles; } get value() { return this._value; } set value(value) { this._value = value; } async setFocus() { await componentFocusable(this); await this.comboboxEl.setFocus(); } connectedCallback() { super.connectedCallback(); connectForm(this); connectLabel(this); } async load() { this.normalizer = await getNormalizer(this.mode); await this.updateTimeZoneItems(); const initialValue = this.value; const normalized = this.normalizeValue(initialValue); this.value = normalized || (initialValue === "" ? normalized : void 0); this.updateTimeZoneSelection(); const selectedValue = this.selectedTimeZoneItem ? `${this.selectedTimeZoneItem.value}` : ""; afterConnectDefaultValueSet(this, selectedValue); this.value = selectedValue; } willUpdate(changes) { if (changes.has("value") && this.hasUpdated) { this.handleValueChange(this.value, changes.get("value")); } if (changes.has("messages") || changes.has("mode") && (this.hasUpdated || this.mode !== "offset") || changes.has("referenceDate")) { this.handleTimeZoneItemPropsChange(); } if (changes.has("open") && (this.hasUpdated || this.open !== false)) { this.openChanged(); } } updated() { updateHostInteraction(this); } loaded() { this.overrideSelectedLabelForRegion(this.open); this.openChanged(); } disconnectedCallback() { super.disconnectedCallback(); disconnectForm(this); disconnectLabel(this); } async handleTimeZoneItemPropsChange() { if (!this.timeZoneItems || !this.hasUpdated) { return; } await this.updateTimeZoneItems(); this.updateTimeZoneSelection(); } openChanged() { if (this.comboboxEl) { this.comboboxEl.open = this.open; } } async handleValueChange(value, oldValue) { const normalized = this.normalizeValue(value); if (!normalized) { if (this.clearable) { this._value = normalized; this.selectedTimeZoneItem = null; return; } this._value = oldValue; this.selectedTimeZoneItem = this.findTimeZoneItem(oldValue); return; } const timeZoneItem = this.findTimeZoneItem(normalized); if (!timeZoneItem) { this._value = oldValue; return; } this._value = normalized; this.selectedTimeZoneItem = timeZoneItem; if (normalized !== value) { await this.updateComplete; this.overrideSelectedLabelForRegion(this.open); } } onLabelClick() { this.setFocus(); } setComboboxRef(el) { this.comboboxEl = el; } overrideSelectedLabelForRegion(open) { if (this.mode !== "region" || !this.selectedTimeZoneItem) { return; } const { label, metadata } = this.selectedTimeZoneItem; this.comboboxEl.selectedItems[0].textLabel = !metadata.country || open ? label : getSelectedRegionTimeZoneLabel(label, metadata.country, this.messages); } onComboboxBeforeClose(event) { event.stopPropagation(); this.overrideSelectedLabelForRegion(false); this.calciteInputTimeZoneBeforeClose.emit(); } onComboboxBeforeOpen(event) { event.stopPropagation(); this.overrideSelectedLabelForRegion(true); this.calciteInputTimeZoneBeforeOpen.emit(); } onComboboxChange(event) { event.stopPropagation(); const combobox = event.target; const selectedItem = combobox.selectedItems[0]; if (!selectedItem) { this._value = ""; this.selectedTimeZoneItem = null; this.calciteInputTimeZoneChange.emit(); return; } const selected = this.findTimeZoneItemByLabel(selectedItem.getAttribute("data-label")); const selectedValue = `${selected.value}`; if (this.value === selectedValue && selected.label === this.selectedTimeZoneItem.label) { return; } this._value = selectedValue; this.selectedTimeZoneItem = selected; this.calciteInputTimeZoneChange.emit(); } onComboboxClose(event) { event.stopPropagation(); this.open = false; this.calciteInputTimeZoneClose.emit(); } onComboboxOpen(event) { this.open = true; event.stopPropagation(); this.calciteInputTimeZoneOpen.emit(); } findTimeZoneItem(value) { return findTimeZoneItemByProp(this.timeZoneItems, "value", value); } findTimeZoneItemByLabel(label) { return findTimeZoneItemByProp(this.timeZoneItems, "label", label); } async updateTimeZoneItems() { this.timeZoneItems = await this.createTimeZoneItems(); } updateTimeZoneSelection() { if (this.value === "" && this.clearable) { this.selectedTimeZoneItem = null; return; } const fallbackValue = this.mode === "offset" ? getUserTimeZoneOffset() : getUserTimeZoneName(); const valueToMatch = this.value === "" || !this.value ? fallbackValue : this.value; this.selectedTimeZoneItem = this.findTimeZoneItem(valueToMatch) || this.findTimeZoneItem(fallbackValue); } async createTimeZoneItems() { if (!this.messages._lang || !this.messages) { return []; } return createTimeZoneItems(this.messages._lang, this.messages, this.mode, this.referenceDate instanceof Date ? this.referenceDate : new Date(this.referenceDate ?? Date.now()), this.offsetStyle); } normalizeValue(value) { value = value === void 0 ? "" : value; return value ? this.normalizer(value) : value; } render() { return InteractiveContainer({ disabled: this.disabled, children: html`<calcite-combobox .clearDisabled=${!this.clearable} .disabled=${this.disabled} .label=${this.messages.chooseTimeZone} lang=${this.messages._lang ?? nothing} .maxItems=${this.maxItems} @calciteComboboxBeforeClose=${this.onComboboxBeforeClose} @calciteComboboxBeforeOpen=${this.onComboboxBeforeOpen} @calciteComboboxChange=${this.onComboboxChange} @calciteComboboxClose=${this.onComboboxClose} @calciteComboboxOpen=${this.onComboboxOpen} .overlayPositioning=${this.overlayPositioning} .placeholder=${this.mode === "name" ? this.messages.namePlaceholder : this.mode === "offset" ? this.messages.offsetPlaceholder : this.messages.regionPlaceholder} placeholder-icon=search .readOnly=${this.readOnly} .scale=${this.scale} .selectionMode=${this.clearable ? "single" : "single-persist"} .status=${this.status} .validationIcon=${this.validationIcon} .validationMessage=${this.validationMessage} ${ref(this.setComboboxRef)}>${this.renderItems()}</calcite-combobox>${HiddenFormInputSlot({ component: this })}` }); } renderItems() { if (this.mode === "region") { return this.renderRegionItems(); } return repeat(this.timeZoneItems, ({ label }) => label, (group) => { const selected = this.selectedTimeZoneItem === group; const { label, metadata, value } = group; return html`<calcite-combobox-item data-label=${label ?? nothing} .metadata=${metadata} .selected=${selected} .textLabel=${label} .value=${value}></calcite-combobox-item>`; }); } renderRegionItems() { return this.timeZoneItems.flatMap(({ label, items }) => keyed(label, html`<calcite-combobox-item-group .label=${label}>${repeat(items, ({ label: label2 }) => label2, (item) => { const selected = this.selectedTimeZoneItem === item; const { label: label2, metadata, value } = item; return html`<calcite-combobox-item data-label=${label2 ?? nothing} .description=${metadata.country} .metadata=${metadata} .selected=${selected} .textLabel=${label2} .value=${value}><span class=${safeClassMap(CSS.offset)} slot=content-end>${metadata.offset}</span></calcite-combobox-item>`; })}</calcite-combobox-item-group>`)); } } customElement("calcite-input-time-zone", InputTimeZone); export { InputTimeZone };