/*! * All material copyright ESRI, All Rights Reserved, unless otherwise specified. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details. * v1.0.0-beta.97 */ import { h, Host } from "@stencil/core"; import { guid } from "../../utils/guid"; import { formatTimeString, isValidTime, localizeTimeString } from "../../utils/time"; import { connectLabel, disconnectLabel, getLabelText } from "../../utils/label"; import { connectForm, disconnectForm, HiddenFormInputSlot, submitForm } from "../../utils/form"; import { updateHostInteraction } from "../../utils/interactive"; import { connectLocalized, disconnectLocalized, numberStringFormatter, updateEffectiveLocale } from "../../utils/locale"; import { numberKeys } from "../../utils/key"; export class InputTimePicker { constructor() { //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- /** * When `true`, the component is active. * * @deprecated Use `open` instead. */ this.active = false; /** When `true`, displays the `calcite-time-picker` component. */ this.open = false; /** When `true`, interaction is prevented and the component is displayed with lower opacity. */ this.disabled = false; /** * When `true`, the component's value can be read, but controls are not accessible and the value cannot be modified. * * @mdn [readOnly](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/readonly) */ this.readOnly = false; /** * When `true`, the component must have a value in order for the form to submit. * * @internal */ this.required = false; /** Specifies the size of the component. */ this.scale = "m"; /** * Determines the type of positioning to use for the overlaid content. * * Using `"absolute"` will work for most cases. The component will be positioned inside of overflowing parent containers and will affect the container's layout. * * `"fixed"` should be used to escape an overflowing parent container, or when the reference element's `position` CSS property is `"fixed"`. * */ this.overlayPositioning = "absolute"; /** * Determines where the popover will be positioned relative to the input. * * @see [LogicalPlacement](https://github.com/Esri/calcite-components/blob/master/src/utils/floating-ui.ts#L25) */ this.placement = "auto"; /** Specifies the granularity the component's `value` must adhere to (in seconds). */ this.step = 60; /** The component's value in UTC (always 24-hour format). */ this.value = null; /** whether the value of the input was changed as a result of user typing or not */ this.internalValueChange = false; this.previousValidValue = null; this.referenceElementId = `input-time-picker-${guid()}`; //-------------------------------------------------------------------------- // // State // //-------------------------------------------------------------------------- this.effectiveLocale = ""; //-------------------------------------------------------------------------- // // Event Listeners // //-------------------------------------------------------------------------- this.calciteInternalInputBlurHandler = () => { this.open = false; const shouldIncludeSeconds = this.shouldIncludeSeconds(); const { effectiveLocale: locale, numberingSystem, value, calciteInputEl } = this; numberStringFormatter.numberFormatOptions = { locale, numberingSystem, useGrouping: false }; const delocalizedValue = numberStringFormatter.delocalize(calciteInputEl.value); const localizedInputValue = localizeTimeString({ value: delocalizedValue, includeSeconds: shouldIncludeSeconds, locale, numberingSystem }); this.setInputValue(localizedInputValue || localizeTimeString({ value, locale, numberingSystem, includeSeconds: shouldIncludeSeconds })); }; this.calciteInternalInputFocusHandler = (event) => { if (!this.readOnly) { this.open = true; event.stopPropagation(); } }; this.calciteInputInputHandler = (event) => { const target = event.target; numberStringFormatter.numberFormatOptions = { locale: this.effectiveLocale, numberingSystem: this.numberingSystem, useGrouping: false }; const delocalizedValue = numberStringFormatter.delocalize(target.value); this.setValue({ value: delocalizedValue }); // only translate the numerals until blur const localizedValue = delocalizedValue .split("") .map((char) => numberKeys.includes(char) ? numberStringFormatter.numberFormatter.format(Number(char)) : char) .join(""); this.setInputValue(localizedValue); }; this.timePickerChangeHandler = (event) => { event.stopPropagation(); const target = event.target; const value = target.value; this.setValue({ value, origin: "time-picker" }); }; // -------------------------------------------------------------------------- // // Private Methods // // -------------------------------------------------------------------------- this.keyDownHandler = (event) => { const { defaultPrevented, key } = event; if (defaultPrevented) { return; } if (key === "Enter") { if (submitForm(this)) { event.preventDefault(); } } if (key === "Escape" && this.open) { this.open = false; event.preventDefault(); } }; this.setCalcitePopoverEl = (el) => { this.popoverEl = el; }; this.setCalciteInputEl = (el) => { this.calciteInputEl = el; }; this.setCalciteTimePickerEl = (el) => { this.calciteTimePickerEl = el; }; this.setInputValue = (newInputValue) => { if (!this.calciteInputEl) { return; } this.calciteInputEl.value = newInputValue; }; this.setValue = ({ value, origin = "input" }) => { const previousValue = this.value; const newValue = formatTimeString(value); const newLocalizedValue = localizeTimeString({ value: newValue, locale: this.effectiveLocale, numberingSystem: this.numberingSystem, includeSeconds: this.shouldIncludeSeconds() }); this.internalValueChange = origin !== "external" && origin !== "loading"; const shouldEmit = origin !== "loading" && origin !== "external" && ((value !== this.previousValidValue && !value) || !!(!this.previousValidValue && newValue) || (newValue !== this.previousValidValue && newValue)); if (value) { if (shouldEmit) { this.previousValidValue = newValue; } if (newValue && newValue !== this.value) { this.value = newValue; } this.localizedValue = newLocalizedValue; } else { this.value = value; this.localizedValue = null; } if (origin === "time-picker" || origin === "external") { this.setInputValue(newLocalizedValue); } if (shouldEmit) { const changeEvent = this.calciteInputTimePickerChange.emit(); if (changeEvent.defaultPrevented) { this.internalValueChange = false; this.value = previousValue; this.setInputValue(previousValue); this.previousValidValue = previousValue; } else { this.previousValidValue = newValue; } } }; } activeHandler(value) { this.open = value; } openHandler(value) { this.active = value; if (this.disabled || this.readOnly) { this.open = false; return; } if (value) { this.reposition(true); } } handleDisabledAndReadOnlyChange(value) { if (!value) { this.open = false; } } localeChanged() { updateEffectiveLocale(this); } valueWatcher(newValue) { if (!this.internalValueChange) { this.setValue({ value: newValue, origin: "external" }); } this.internalValueChange = false; } effectiveLocaleWatcher() { this.setInputValue(localizeTimeString({ value: this.value, locale: this.effectiveLocale, numberingSystem: this.numberingSystem, includeSeconds: this.shouldIncludeSeconds() })); } clickHandler(event) { if (event.composedPath().includes(this.calciteTimePickerEl)) { return; } this.setFocus(); } timePickerBlurHandler(event) { event.preventDefault(); event.stopPropagation(); this.open = false; } timePickerFocusHandler(event) { event.preventDefault(); event.stopPropagation(); if (!this.readOnly) { this.open = true; } } // -------------------------------------------------------------------------- // // Public Methods // // -------------------------------------------------------------------------- /** Sets focus on the component. */ async setFocus() { var _a; (_a = this.calciteInputEl) === null || _a === void 0 ? void 0 : _a.setFocus(); } /** * Updates the position of the component. * * @param delayed */ async reposition(delayed = false) { var _a; (_a = this.popoverEl) === null || _a === void 0 ? void 0 : _a.reposition(delayed); } onLabelClick() { this.setFocus(); } shouldIncludeSeconds() { return this.step < 60; } //-------------------------------------------------------------------------- // // Lifecycle // //-------------------------------------------------------------------------- connectedCallback() { connectLocalized(this); const { active, open } = this; if (this.value) { this.setValue({ value: isValidTime(this.value) ? this.value : undefined, origin: "loading" }); } connectLabel(this); connectForm(this); if (open) { this.active = open; } else if (active) { this.open = active; } } componentDidLoad() { this.setInputValue(this.localizedValue); } disconnectedCallback() { disconnectLabel(this); disconnectForm(this); disconnectLocalized(this); } componentDidRender() { updateHostInteraction(this); } // -------------------------------------------------------------------------- // // Render Methods // // -------------------------------------------------------------------------- render() { const popoverId = `${this.referenceElementId}-popover`; return (h(Host, { onKeyDown: this.keyDownHandler }, h("div", { "aria-controls": popoverId, "aria-haspopup": "dialog", "aria-label": this.name, "aria-owns": popoverId, id: this.referenceElementId, role: "combobox" }, h("calcite-input", { disabled: this.disabled, icon: "clock", label: getLabelText(this), onCalciteInputInput: this.calciteInputInputHandler, onCalciteInternalInputBlur: this.calciteInternalInputBlurHandler, onCalciteInternalInputFocus: this.calciteInternalInputFocusHandler, readOnly: this.readOnly, ref: this.setCalciteInputEl, scale: this.scale, step: this.step })), h("calcite-popover", { id: popoverId, label: "Time Picker", open: this.open, overlayPositioning: this.overlayPositioning, placement: this.placement, ref: this.setCalcitePopoverEl, referenceElement: this.referenceElementId, triggerDisabled: true }, h("calcite-time-picker", { intlHour: this.intlHour, intlHourDown: this.intlHourDown, intlHourUp: this.intlHourUp, intlMeridiem: this.intlMeridiem, intlMeridiemDown: this.intlMeridiemDown, intlMeridiemUp: this.intlMeridiemUp, intlMinute: this.intlMinute, intlMinuteDown: this.intlMinuteDown, intlMinuteUp: this.intlMinuteUp, intlSecond: this.intlSecond, intlSecondDown: this.intlSecondDown, intlSecondUp: this.intlSecondUp, lang: this.effectiveLocale, numberingSystem: this.numberingSystem, onCalciteInternalTimePickerChange: this.timePickerChangeHandler, ref: this.setCalciteTimePickerEl, scale: this.scale, step: this.step, value: this.value })), h(HiddenFormInputSlot, { component: this }))); } static get is() { return "calcite-input-time-picker"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["input-time-picker.scss"] }; } static get styleUrls() { return { "$": ["input-time-picker.css"] }; } static get properties() { return { "active": { "type": "boolean", "mutable": true, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "deprecated", "text": "Use `open` instead." }], "text": "When `true`, the component is active." }, "attribute": "active", "reflect": true, "defaultValue": "false" }, "open": { "type": "boolean", "mutable": true, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, displays the `calcite-time-picker` component." }, "attribute": "open", "reflect": true, "defaultValue": "false" }, "disabled": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, interaction is prevented and the component is displayed with lower opacity." }, "attribute": "disabled", "reflect": true, "defaultValue": "false" }, "readOnly": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "mdn", "text": "[readOnly](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/readonly)" }], "text": "When `true`, the component's value can be read, but controls are not accessible and the value cannot be modified." }, "attribute": "read-only", "reflect": true, "defaultValue": "false" }, "intlHour": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Accessible name for the component's hour input." }, "attribute": "intl-hour", "reflect": false }, "intlHourDown": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Accessible name for the component's hour down button." }, "attribute": "intl-hour-down", "reflect": false }, "intlHourUp": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Accessible name for the component's hour up button." }, "attribute": "intl-hour-up", "reflect": false }, "intlMeridiem": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Accessible name for the component's meridiem (am/pm) input." }, "attribute": "intl-meridiem", "reflect": false }, "intlMeridiemDown": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Accessible name for the component's meridiem (am/pm) down button." }, "attribute": "intl-meridiem-down", "reflect": false }, "intlMeridiemUp": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Accessible name for the component's meridiem (am/pm) up button." }, "attribute": "intl-meridiem-up", "reflect": false }, "intlMinute": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Accessible name for the component's minute input." }, "attribute": "intl-minute", "reflect": false }, "intlMinuteDown": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Accessible name for the component's minute down button." }, "attribute": "intl-minute-down", "reflect": false }, "intlMinuteUp": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Accessible name for the component's minute up button." }, "attribute": "intl-minute-up", "reflect": false }, "intlSecond": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Accessible name for the component's second input." }, "attribute": "intl-second", "reflect": false }, "intlSecondDown": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Accessible name for the component's second down button." }, "attribute": "intl-second-down", "reflect": false }, "intlSecondUp": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Accessible name for the component's second up button." }, "attribute": "intl-second-up", "reflect": false }, "locale": { "type": "string", "mutable": true, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "internal", "text": undefined }, { "name": "deprecated", "text": "set the global `lang` attribute on the element instead." }, { "name": "mdn", "text": "[lang](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang)" }], "text": "BCP 47 language tag for desired language and country format." }, "attribute": "locale", "reflect": false }, "name": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the name of the component on form submission." }, "attribute": "name", "reflect": false }, "numberingSystem": { "type": "string", "mutable": false, "complexType": { "original": "NumberingSystem", "resolved": "\"arab\" | \"arabext\" | \"bali\" | \"beng\" | \"deva\" | \"fullwide\" | \"gujr\" | \"guru\" | \"hanidec\" | \"khmr\" | \"knda\" | \"laoo\" | \"latn\" | \"limb\" | \"mlym\" | \"mong\" | \"mymr\" | \"orya\" | \"tamldec\" | \"telu\" | \"thai\" | \"tibt\"", "references": { "NumberingSystem": { "location": "import", "path": "../../utils/locale" } } }, "required": false, "optional": true, "docs": { "tags": [], "text": "Specifies the Unicode numeral system used by the component for localization." }, "attribute": "numbering-system", "reflect": false }, "required": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "When `true`, the component must have a value in order for the form to submit." }, "attribute": "required", "reflect": true, "defaultValue": "false" }, "scale": { "type": "string", "mutable": false, "complexType": { "original": "Scale", "resolved": "\"l\" | \"m\" | \"s\"", "references": { "Scale": { "location": "import", "path": "../interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the size of the component." }, "attribute": "scale", "reflect": true, "defaultValue": "\"m\"" }, "overlayPositioning": { "type": "string", "mutable": false, "complexType": { "original": "OverlayPositioning", "resolved": "\"absolute\" | \"fixed\"", "references": { "OverlayPositioning": { "location": "import", "path": "../../utils/floating-ui" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Determines the type of positioning to use for the overlaid content.\n\nUsing `\"absolute\"` will work for most cases. The component will be positioned inside of overflowing parent containers and will affect the container's layout.\n\n`\"fixed\"` should be used to escape an overflowing parent container, or when the reference element's `position` CSS property is `\"fixed\"`." }, "attribute": "overlay-positioning", "reflect": false, "defaultValue": "\"absolute\"" }, "placement": { "type": "string", "mutable": false, "complexType": { "original": "LogicalPlacement", "resolved": "Placement | VariationPlacement | AutoPlacement | DeprecatedPlacement", "references": { "LogicalPlacement": { "location": "import", "path": "../../utils/floating-ui" } } }, "required": false, "optional": false, "docs": { "tags": [{ "name": "see", "text": "[LogicalPlacement](https://github.com/Esri/calcite-components/blob/master/src/utils/floating-ui.ts#L25)" }], "text": "Determines where the popover will be positioned relative to the input." }, "attribute": "placement", "reflect": true, "defaultValue": "\"auto\"" }, "step": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the granularity the component's `value` must adhere to (in seconds)." }, "attribute": "step", "reflect": false, "defaultValue": "60" }, "value": { "type": "string", "mutable": true, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The component's value in UTC (always 24-hour format)." }, "attribute": "value", "reflect": false, "defaultValue": "null" } }; } static get states() { return { "effectiveLocale": {}, "localizedValue": {} }; } static get events() { return [{ "method": "calciteInputTimePickerChange", "name": "calciteInputTimePickerChange", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Fires when the time value is changed as a result of user input." }, "complexType": { "original": "string", "resolved": "string", "references": {} } }]; } static get methods() { return { "setFocus": { "complexType": { "signature": "() => Promise", "parameters": [], "references": { "Promise": { "location": "global" } }, "return": "Promise" }, "docs": { "text": "Sets focus on the component.", "tags": [] } }, "reposition": { "complexType": { "signature": "(delayed?: boolean) => Promise", "parameters": [{ "tags": [{ "name": "param", "text": "delayed" }], "text": "" }], "references": { "Promise": { "location": "global" } }, "return": "Promise" }, "docs": { "text": "Updates the position of the component.", "tags": [{ "name": "param", "text": "delayed" }] } } }; } static get elementRef() { return "el"; } static get watchers() { return [{ "propName": "active", "methodName": "activeHandler" }, { "propName": "open", "methodName": "openHandler" }, { "propName": "disabled", "methodName": "handleDisabledAndReadOnlyChange" }, { "propName": "readOnly", "methodName": "handleDisabledAndReadOnlyChange" }, { "propName": "locale", "methodName": "localeChanged" }, { "propName": "value", "methodName": "valueWatcher" }, { "propName": "effectiveLocale", "methodName": "effectiveLocaleWatcher" }]; } static get listeners() { return [{ "name": "click", "method": "clickHandler", "target": undefined, "capture": false, "passive": false }, { "name": "calciteInternalTimePickerBlur", "method": "timePickerBlurHandler", "target": undefined, "capture": false, "passive": false }, { "name": "calciteInternalTimePickerFocus", "method": "timePickerFocusHandler", "target": undefined, "capture": false, "passive": false }]; } }