@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
354 lines (353 loc) • 24 kB
JavaScript
/* COPYRIGHT Esri - https://js.arcgis.com/5.0/LICENSE.txt */
import { c as customElement } from "../../chunks/runtime.js";
import { css, nothing, html } from "lit";
import { LitElement, createEvent, stringOrBoolean, safeClassMap } from "@arcgis/lumina";
import { createRef, ref } from "lit/directives/ref.js";
import { c as connectForm, d as disconnectForm, s as submitForm, H as HiddenFormInputSlot } from "../../chunks/form.js";
import { c as connectLabel, d as disconnectLabel, g as getLabelText } from "../../chunks/label.js";
import { d as decimalPlaces } from "../../chunks/math.js";
import { g as getIconScale } from "../../chunks/component.js";
import { I as InternalLabel } from "../../chunks/InternalLabel.js";
import { V as Validation } from "../../chunks/Validation.js";
import { g as getElementDir } from "../../chunks/dom.js";
import { s as syncHiddenFormInput } from "../../chunks/input.js";
import { u as useT9n } from "../../chunks/useT9n.js";
import { i as isValidNumber } from "../../chunks/locale.js";
import { u as useSetFocus } from "../../chunks/useSetFocus.js";
import { u as useTime } from "../../chunks/useTime.js";
import { u as useInteractive } from "../../chunks/useInteractive.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{display:inline-block}: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;opacity:0;outline:none;padding:0;position:absolute;inset:0;transform:none;-webkit-appearance:none;z-index:-1}calcite-time-picker{--calcite-time-picker-color: var(--calcite-input-time-picker-digit-text-color);--calcite-time-picker-icon-color: var(--calcite-input-time-picker-digit-icon-color);--calcite-time-picker-border-color: var(--calcite-input-time-picker-border-color);--calcite-time-picker-button-background-color-hover: var(--calcite-input-time-picker-action-background-color-hover);--calcite-time-picker-button-background-color-press: var(--calcite-input-time-picker-action-background-color-press);--calcite-time-picker-input-border-color-hover: var(--calcite-input-time-picker-digit-border-color-hover);--calcite-time-picker-input-border-color-press: var(--calcite-input-time-picker-digit-border-color-press)}.container{--calcite-icon-color: var( --calcite-input-time-picker-icon-color, var(--calcite-ui-icon-color, var(--calcite-color-text-3)) );align-items:center;background-color:var(--calcite-input-time-picker-input-background-color, var(--calcite-color-foreground-1));border:1px solid var(--calcite-input-time-picker-input-border-color, var(--calcite-color-border-input));border-radius:var(--calcite-input-time-picker-input-corner-radius, var(--calcite-corner-radius-none));box-shadow:var(--calcite-input-time-picker-input-shadow, var(--calcite-shadow-none));box-sizing:border-box;display:flex;color:var(--calcite-input-time-picker-input-text-color, var(--calcite-color-text-1));flex-wrap:nowrap;font-weight:var(--calcite-font-weight-normal);inline-size:100%;padding-block:var(--calcite-spacing-base);-webkit-user-select:none;user-select:none}.container:focus-within{border-color:var(--calcite-color-brand);outline:var(--calcite-border-width-md) solid var(--calcite-color-focus, var(--calcite-ui-focus-color, var(--calcite-color-brand)));outline-offset:calc(calc(-1 * var(--calcite-spacing-base)) * calc(1 - (2*clamp(0,var(--calcite-offset-invert-focus),1))))}.container.read-only{background-color:var(--calcite-color-background);font-weight:var(--calcite-font-weight-medium)}.clock-icon{--calcite-icon-color: var( --calcite-input-time-picker-icon-color, var(--calcite-ui-icon-color, var(--calcite-color-text-3)) )}.hour-suffix,.minute-suffix,.second-suffix{white-space:pre}.input-container{display:flex;flex-grow:1}.input{align-items:center;display:flex;block-size:100%;justify-content:center;min-inline-size:max-content}.input.empty{inline-size:var(--calcite-spacing-xl)}.input:focus,.input:hover:focus{background-color:Highlight;color:HighlightText;outline:2px solid transparent;outline-offset:2px}.toggle-icon{--calcite-icon-color: var(--calcite-input-time-picker-icon-color, var(--calcite-color-text-3));align-items:center;block-size:24px;cursor:pointer;display:flex;inline-size:24px;justify-content:center}.toggle-icon:hover{--calcite-icon-color: var(--calcite-input-time-picker-icon-color-hover, var(--calcite-color-text-1))}:host([scale=s]) .container{block-size:1.5rem;font-size:var(--calcite-font-size-sm);gap:var(--calcite-spacing-sm);padding-inline-start:var(--calcite-spacing-sm);padding-inline-end:var(--calcite-spacing-xxs)}:host([scale=s]) .input-container{line-height:1rem}:host([scale=m]) .container{block-size:2rem;font-size:var(--calcite-font-size);gap:var(--calcite-spacing-md);padding-inline-start:var(--calcite-spacing-md);padding-inline-end:var(--calcite-spacing-sm)}:host([scale=m]) .input-container{line-height:1.5rem}:host([scale=l]) .container{block-size:2.75rem;font-size:var(--calcite-font-size-md);gap:var(--calcite-spacing-lg);padding-inline:var(--calcite-spacing-lg)}:host([scale=l]) .input-container{line-height:2.25rem}:host([status=invalid]) .container{border-color:var(--calcite-color-status-danger)}:host([status=invalid]) .container:focus-within{outline:var(--calcite-border-width-md) solid var(--calcite-color-status-danger);outline-offset:calc(calc(-1 * var(--calcite-spacing-base)) * calc(1 - (2*clamp(0,var(--calcite-offset-invert-focus),1))))}calcite-time-picker{--calcite-time-picker-background-color: var(--calcite-input-time-picker-background-color);--calcite-time-picker-border-color: var(--calcite-input-time-picker-border-color, transparent);--calcite-time-picker-corner-radius: var( --calcite-input-time-picker-corner-radius, var(--calcite-corner-radius-round) )}calcite-popover{--calcite-popover-corner-radius: var(--calcite-input-time-picker-corner-radius, var(--calcite-corner-radius-round))}.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}:host([hidden]){display:none}[hidden]{display:none}`;
const CSS = {
clockIcon: "clock-icon",
container: "container",
decimalSeparator: "decimal-separator",
empty: "empty",
fractionalSecond: "fractional-second",
hour: "hour",
hourSuffix: "hour-suffix",
input: "input",
inputContainer: "input-container",
meridiem: "meridiem",
minute: "minute",
minuteSuffix: "minute-suffix",
readOnly: "read-only",
second: "second",
secondSuffix: "second-suffix",
toggleIcon: "toggle-icon"
};
const IDS = {
inputContainer: "inputContainer",
validationMessage: "inputTimePickerValidationMessage"
};
const ICONS = {
clock: "clock",
chevronUp: "chevron-up",
chevronDown: "chevron-down"
};
class InputTimePicker extends LitElement {
constructor() {
super();
this.messages = useT9n();
this.containerRef = createRef();
this.focusSetter = useSetFocus()(this);
this.fractionalSecondRef = createRef();
this.hourRef = createRef();
this.meridiemRef = createRef();
this.minuteRef = createRef();
this.secondRef = createRef();
this.time = useTime(this);
this.interactiveContainer = useInteractive(this);
this.timePickerRef = createRef();
this.disabled = false;
this.focusTrapDisabled = false;
this.hourFormat = "user";
this.open = false;
this.overlayPositioning = "absolute";
this.placement = "auto";
this.readOnly = false;
this.required = false;
this.scale = "m";
this.status = "idle";
this.step = 60;
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.calciteInputTimePickerBeforeClose = createEvent({ cancelable: false });
this.calciteInputTimePickerBeforeOpen = createEvent({ cancelable: false });
this.calciteInputTimePickerChange = createEvent();
this.calciteInputTimePickerClose = createEvent({ cancelable: false });
this.calciteInputTimePickerOpen = createEvent({ cancelable: false });
this.listen("blur", this.blurHandler);
this.listen("keydown", this.keyDownHandler);
this.listen("calciteTimeChange", this.timeChangeHandler);
}
static {
this.properties = { disabled: [7, {}, { reflect: true, type: Boolean }], focusTrapDisabled: [7, {}, { reflect: true, type: Boolean }], form: [3, {}, { reflect: true }], hourFormat: [3, {}, { reflect: true }], label: 1, labelText: 1, max: [3, {}, { reflect: true }], messageOverrides: [0, {}, { attribute: false }], min: [3, {}, { reflect: true }], name: 1, numberingSystem: [3, {}, { reflect: true }], open: [7, {}, { reflect: true, type: Boolean }], overlayPositioning: 1, placement: [3, {}, { reflect: true }], readOnly: [7, {}, { reflect: true, type: Boolean }], required: [7, {}, { reflect: true, type: Boolean }], scale: [3, {}, { reflect: true }], status: [3, {}, { reflect: true }], step: [11, {}, { reflect: true, type: Number }], validationIcon: [3, { converter: stringOrBoolean, type: String }, { reflect: true }], validationMessage: 1, validity: [0, {}, { attribute: false }], value: 1 };
}
static {
this.shadowRootOptions = { mode: "open", delegatesFocus: true };
}
static {
this.styles = styles;
}
async reposition(delayed = false) {
this.popoverEl?.reposition(delayed);
}
async setFocus(options) {
return this.focusSetter(() => this.el, options);
}
connectedCallback() {
super.connectedCallback();
connectLabel(this);
connectForm(this);
}
willUpdate(changes) {
if (changes.has("open") && (this.hasUpdated || this.open !== false)) {
this.openHandler();
}
if (changes.has("disabled") && (this.hasUpdated || this.disabled !== false)) {
if (!this.disabled) {
this.open = false;
}
}
if (changes.has("readOnly") && (this.hasUpdated || this.readOnly !== false)) {
if (!this.readOnly) {
this.open = false;
}
}
if (changes.has("value")) {
if (this.hasUpdated) {
if (!this.time.userChangedValue) {
this.previousEmittedValue = this.value;
}
this.time.setValue(this.value);
this.requestTimePickerUpdate();
} else {
this.previousEmittedValue = this.value;
}
}
}
disconnectedCallback() {
super.disconnectedCallback();
disconnectLabel(this);
disconnectForm(this);
}
blurHandler() {
this.changeEventHandler();
}
changeEventHandler() {
const { previousEmittedValue, value } = this;
if (previousEmittedValue !== value) {
const changeEvent = this.calciteInputTimePickerChange.emit();
if (changeEvent.defaultPrevented) {
this.time.setValue(this.previousEmittedValue);
} else {
this.previousEmittedValue = value;
}
}
}
keyDownHandler(event) {
const { defaultPrevented, key } = event;
const { hourFormat, meridiemOrder } = this.time;
if (defaultPrevented) {
return;
}
if (key === "Enter") {
if (submitForm(this)) {
event.preventDefault();
}
this.changeEventHandler();
} else if (this.open && key === "Escape") {
this.open = false;
event.preventDefault();
} else {
const showFractionalSecond = decimalPlaces(this.step) > 0;
const showSecond = this.step < 60;
switch (this.activeEl) {
case this.hourRef.value:
if (key === "ArrowRight") {
this.setFocusPart("minute");
} else if (key === "ArrowLeft" && hourFormat === "12" && meridiemOrder === 0) {
this.setFocusPart("meridiem");
}
break;
case this.minuteRef.value:
switch (key) {
case "ArrowLeft":
this.setFocusPart("hour");
break;
case "ArrowRight":
if (this.step !== 60) {
this.setFocusPart("second");
} else if (hourFormat === "12") {
this.setFocusPart("meridiem");
}
break;
}
break;
case this.secondRef.value:
switch (key) {
case "ArrowLeft":
this.setFocusPart("minute");
break;
case "ArrowRight":
if (decimalPlaces(this.step) > 0) {
this.setFocusPart("fractionalSecond");
} else if (hourFormat === "12") {
this.setFocusPart("meridiem");
}
break;
}
break;
case this.fractionalSecondRef.value:
switch (key) {
case "ArrowLeft":
this.setFocusPart("second");
break;
case "ArrowRight":
if (hourFormat === "12" && meridiemOrder !== 0) {
this.setFocusPart("meridiem");
}
break;
}
break;
case this.meridiemRef.value:
if (key === "ArrowLeft" && meridiemOrder !== 0) {
if (showFractionalSecond) {
this.setFocusPart("fractionalSecond");
} else if (showSecond) {
this.setFocusPart("second");
} else {
this.setFocusPart("minute");
}
} else if (key === "ArrowRight" && meridiemOrder === 0) {
this.setFocusPart("hour");
}
break;
}
}
}
onLabelClick() {
this.setFocus();
}
openHandler() {
if (this.disabled || this.readOnly) {
return;
}
if (this.popoverEl) {
this.popoverEl.open = this.open;
}
}
popoverBeforeOpenHandler(event) {
event.stopPropagation();
this.calciteInputTimePickerBeforeOpen.emit();
}
popoverOpenHandler(event) {
event.stopPropagation();
this.calciteInputTimePickerOpen.emit();
}
popoverBeforeCloseHandler(event) {
event.stopPropagation();
this.calciteInputTimePickerBeforeClose.emit();
}
popoverCloseHandler(event) {
event.stopPropagation();
this.calciteInputTimePickerClose.emit();
this.open = false;
}
requestTimePickerUpdate() {
this.timePickerRef.value.manager?.component.requestUpdate();
}
setCalcitePopoverEl(el) {
this.popoverEl = el;
this.openHandler();
}
async setFocusPart(target) {
const ref2 = target === "hour" ? this.hourRef : target === "minute" ? this.minuteRef : target === "second" ? this.secondRef : target === "fractionalSecond" ? this.fractionalSecondRef : this.meridiemRef;
ref2.value?.focus();
}
syncHiddenFormInput(input) {
syncHiddenFormInput("time", this, input);
}
timeChangeHandler(event) {
event.stopPropagation();
if (this.disabled) {
return;
}
const newValue = event.detail;
if (newValue !== this.value) {
this.value = newValue;
} else {
this.requestTimePickerUpdate();
}
}
timePartFocusHandler(event) {
this.activeEl = event.currentTarget;
}
timePickerChangeHandler(event) {
event.stopPropagation();
}
toggleIconClickHandler() {
this.open = !this.open;
}
render() {
const { messages, readOnly, scale } = this;
const { fractionalSecond, handleHourKeyDownEvent, handleMinuteKeyDownEvent, handleSecondKeyDownEvent, handleFractionalSecondKeyDownEvent, hour, hourFormat, localizedDecimalSeparator, localizedFractionalSecond, localizedHour, localizedHourSuffix, localizedMinute, localizedMinuteSuffix, localizedSecond, localizedSecondSuffix, meridiemOrder, minute, second } = this.time;
const emptyPlaceholder = "--";
const fractionalSecondIsNumber = isValidNumber(fractionalSecond);
const hourIsNumber = isValidNumber(hour);
const minuteIsNumber = isValidNumber(minute);
const secondIsNumber = isValidNumber(second);
const showFractionalSecond = decimalPlaces(this.step) > 0;
const showMeridiem = hourFormat === "12";
const showSecond = this.step < 60;
const meridiemStart = meridiemOrder === 0 || getElementDir(this.el) === "rtl";
const isInteractive = !this.disabled && !this.readOnly;
return this.interactiveContainer({ disabled: this.disabled, children: html`${this.labelText && InternalLabel({ labelText: this.labelText, onClick: this.onLabelClick, required: this.required, tooltipText: this.messages.required }) || ""}<div aria-controls=${IDS.inputContainer} aria-labelledby=${IDS.inputContainer} class=${safeClassMap({
[CSS.container]: true,
[CSS.readOnly]: readOnly
})} role=combobox ${ref(this.containerRef)}><calcite-icon class=${safeClassMap(CSS.clockIcon)} .icon=${ICONS.clock} .scale=${scale === "l" ? "m" : "s"}></calcite-icon><div aria-label=${getLabelText(this) ?? nothing} .ariaRequired=${this.required} class=${safeClassMap(CSS.inputContainer)} dir=ltr id=${IDS.inputContainer} role=group>${showMeridiem && meridiemStart && this.renderMeridiem() || ""}<span aria-label=${this.messages.hour ?? nothing} aria-valuemax=23 aria-valuemin=1 aria-valuenow=${(hourIsNumber && parseInt(hour) || "0") ?? nothing} aria-valuetext=${hour ?? nothing} class=${safeClassMap({
[CSS.empty]: !localizedHour,
[CSS.hour]: true,
[CSS.input]: true
})} @focus=${this.timePartFocusHandler} @keydown=${isInteractive ? handleHourKeyDownEvent : void 0} role=spinbutton tabindex=0 ${ref(this.hourRef)}>${localizedHour || emptyPlaceholder}</span><span class=${safeClassMap(CSS.hourSuffix)}>${localizedHourSuffix}</span><span aria-label=${this.messages.minute ?? nothing} aria-valuemax=12 aria-valuemin=1 aria-valuenow=${(minuteIsNumber && parseInt(minute) || "0") ?? nothing} aria-valuetext=${minute ?? nothing} class=${safeClassMap({
[CSS.empty]: !localizedMinute,
[CSS.input]: true,
[CSS.minute]: true
})} @focus=${this.timePartFocusHandler} @keydown=${isInteractive ? handleMinuteKeyDownEvent : void 0} role=spinbutton tabindex=0 ${ref(this.minuteRef)}>${localizedMinute || emptyPlaceholder}</span><span class=${safeClassMap(CSS.minuteSuffix)}>${localizedMinuteSuffix}</span>${showSecond && html`<span aria-label=${this.messages.second ?? nothing} aria-valuemax=59 aria-valuemin=0 aria-valuenow=${(secondIsNumber && parseInt(second) || "0") ?? nothing} aria-valuetext=${second ?? nothing} class=${safeClassMap({
[CSS.empty]: !localizedSecond,
[CSS.input]: true,
[CSS.second]: true
})} @focus=${this.timePartFocusHandler} @keydown=${isInteractive ? handleSecondKeyDownEvent : void 0} role=spinbutton tabindex=0 ${ref(this.secondRef)}>${localizedSecond || emptyPlaceholder}</span>` || ""}${showFractionalSecond && html`<span class=${safeClassMap(CSS.decimalSeparator)}>${localizedDecimalSeparator}</span>` || ""}${showFractionalSecond && html`<span aria-label=${this.messages.fractionalSecond ?? nothing} aria-valuemax=999 aria-valuemin=1 aria-valuenow=${(fractionalSecondIsNumber && parseInt(fractionalSecond) || "0") ?? nothing} aria-valuetext=${localizedFractionalSecond ?? nothing} class=${safeClassMap({
[CSS.empty]: !localizedFractionalSecond,
[CSS.fractionalSecond]: true,
[CSS.input]: true
})} @focus=${this.timePartFocusHandler} @keydown=${isInteractive ? handleFractionalSecondKeyDownEvent : void 0} role=spinbutton tabindex=0 ${ref(this.fractionalSecondRef)}>${localizedFractionalSecond || "".padStart(decimalPlaces(this.step), "-")}</span>` || ""}${localizedSecondSuffix && html`<span class=${safeClassMap(CSS.secondSuffix)}>${localizedSecondSuffix}</span>` || ""}${showMeridiem && !meridiemStart && this.renderMeridiem() || ""}</div>${!this.readOnly && this.renderToggleIcon(this.open) || ""}</div><calcite-popover auto-close .focusTrapDisabled=${this.focusTrapDisabled} .focusTrapOptions=${{ initialFocus: false }} .label=${messages.chooseTime} lang=${this.messages._lang ?? nothing} offset-distance=0 @calcitePopoverBeforeClose=${this.popoverBeforeCloseHandler} @calcitePopoverBeforeOpen=${this.popoverBeforeOpenHandler} @calcitePopoverClose=${this.popoverCloseHandler} @calcitePopoverOpen=${this.popoverOpenHandler} .overlayPositioning=${this.overlayPositioning} .placement=${this.placement} pointer-disabled .referenceElement=${this.containerRef.value} trigger-disabled ${ref(this.setCalcitePopoverEl)}><calcite-time-picker .hourFormat=${this.time.hourFormat} lang=${this.messages._lang ?? nothing} .messageOverrides=${this.messageOverrides} .numberingSystem=${this.numberingSystem} @calciteTimePickerChange=${this.timePickerChangeHandler} .scale=${this.scale} .step=${this.step} tabindex=${(this.open ? void 0 : -1) ?? nothing} .time=${this.time} .value=${this.value} ${ref(this.timePickerRef)}></calcite-time-picker></calcite-popover>${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}` });
}
renderMeridiem() {
const { handleMeridiemKeyDownEvent, localizedMeridiem, meridiem } = this.time;
const isInteractive = !this.disabled && !this.readOnly;
return html`<span aria-label=${this.messages.meridiem ?? nothing} aria-valuemax=2 aria-valuemin=1 aria-valuenow=${meridiem === "PM" && "2" || "1"} aria-valuetext=${meridiem ?? nothing} class=${safeClassMap({
[CSS.empty]: !localizedMeridiem,
[CSS.input]: true,
[CSS.meridiem]: true
})} @focus=${this.timePartFocusHandler} @keydown=${isInteractive ? handleMeridiemKeyDownEvent : void 0} role=spinbutton tabindex=0 ${ref(this.meridiemRef)}>${localizedMeridiem || "--"}</span>`;
}
renderToggleIcon(open) {
return html`<span class=${safeClassMap(CSS.toggleIcon)} @click=${this.toggleIconClickHandler}><calcite-icon .icon=${open ? ICONS.chevronUp : ICONS.chevronDown} .scale=${getIconScale(this.scale)}></calcite-icon></span>`;
}
}
customElement("calcite-input-time-picker", InputTimePicker);
export {
InputTimePicker
};