@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
433 lines (432 loc) • 19 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 { 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} =${this.onComboboxBeforeClose} =${this.onComboboxBeforeOpen} =${this.onComboboxChange} =${this.onComboboxClose} =${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
};