UNPKG

@blueprintjs/datetime

Version:

Components for interacting with dates and times

423 lines 23.2 kB
/* * Copyright 2016 Palantir Technologies, Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { __assign, __rest } from "tslib"; /** * @fileoverview This component is DEPRECATED, and the code is frozen. * All changes & bugfixes should be made to DateInput3 in the datetime2 * package instead. */ /* eslint-disable deprecation/deprecation, @blueprintjs/no-deprecated-components, react-hooks/exhaustive-deps */ import classNames from "classnames"; import * as React from "react"; import { DISPLAYNAME_PREFIX, InputGroup, mergeRefs, Popover, Tag, Utils, } from "@blueprintjs/core"; import { Classes } from "../../common"; import { getFormattedDateString } from "../../common/dateFormatProps"; import { hasMonthChanged, hasTimeChanged, isDateValid, isDayInRange } from "../../common/dateUtils"; import * as Errors from "../../common/errors"; import { getCurrentTimezone } from "../../common/getTimezone"; import { UTC_TIME } from "../../common/timezoneItems"; import { getTimezoneShortName, isValidTimezone } from "../../common/timezoneNameUtils"; import { convertLocalDateToTimezoneTime, getDateObjectFromIsoString, getIsoEquivalentWithUpdatedTimezone, } from "../../common/timezoneUtils"; import { DatePicker } from "../date-picker/datePicker"; import { DatePickerUtils } from "../date-picker/datePickerUtils"; import { TimezoneSelect } from "../timezone-select/timezoneSelect"; var timezoneSelectButtonProps = { fill: false, minimal: true, outlined: true, }; var INVALID_DATE = new Date(undefined); var DEFAULT_MAX_DATE = DatePickerUtils.getDefaultMaxDate(); var DEFAULT_MIN_DATE = DatePickerUtils.getDefaultMinDate(); /** * Date input component. * * @see https://blueprintjs.com/docs/#datetime/date-input * @deprecated use `{ DateInput3 } from "@blueprintjs/datetime2"` instead */ export var DateInput = React.memo(function _DateInput(props) { var _a, _b, _c, _d, _e; var defaultTimezone = props.defaultTimezone, defaultValue = props.defaultValue, disableTimezoneSelect = props.disableTimezoneSelect, fill = props.fill, _f = props.inputProps, inputProps = _f === void 0 ? {} : _f, // defaults duplicated here for TypeScript convenience _g = props.maxDate, // defaults duplicated here for TypeScript convenience maxDate = _g === void 0 ? DEFAULT_MAX_DATE : _g, _h = props.minDate, minDate = _h === void 0 ? DEFAULT_MIN_DATE : _h, placeholder = props.placeholder, _j = props.popoverProps, popoverProps = _j === void 0 ? {} : _j, popoverRef = props.popoverRef, showTimezoneSelect = props.showTimezoneSelect, timePrecision = props.timePrecision, timezone = props.timezone, value = props.value, datePickerProps = __rest(props, ["defaultTimezone", "defaultValue", "disableTimezoneSelect", "fill", "inputProps", "maxDate", "minDate", "placeholder", "popoverProps", "popoverRef", "showTimezoneSelect", "timePrecision", "timezone", "value"]); // Refs // ------------------------------------------------------------------------ var inputRef = React.useRef(null); var popoverContentRef = React.useRef(null); var popoverId = Utils.uniqueId("date-picker"); // State // ------------------------------------------------------------------------ var _k = React.useState(false), isOpen = _k[0], setIsOpen = _k[1]; var _l = React.useState(getInitialTimezoneValue(props)), timezoneValue = _l[0], setTimezoneValue = _l[1]; var valueFromProps = React.useMemo(function () { return getDateObjectFromIsoString(value, timezoneValue); }, [timezoneValue, value]); var isControlled = valueFromProps !== undefined; var defaultValueFromProps = React.useMemo(function () { return getDateObjectFromIsoString(defaultValue, timezoneValue); }, [defaultValue, defaultTimezone]); var _m = React.useState(isControlled ? valueFromProps : defaultValueFromProps), valueAsDate = _m[0], setValue = _m[1]; var _o = React.useState(undefined), selectedShortcutIndex = _o[0], setSelectedShortcutIndex = _o[1]; var _p = React.useState(false), isInputFocused = _p[0], setIsInputFocused = _p[1]; // rendered as the text input's value var formattedDateString = React.useMemo(function () { return valueAsDate === null ? undefined : getFormattedDateString(valueAsDate, props); }, [ valueAsDate, minDate, maxDate, // HACKHACK: ESLint false positive // eslint-disable-next-line @typescript-eslint/unbound-method props.formatDate, props.locale, props.invalidDateMessage, props.outOfRangeMessage, ]); var _q = React.useState(formattedDateString !== null && formattedDateString !== void 0 ? formattedDateString : undefined), inputValue = _q[0], setInputValue = _q[1]; var isErrorState = valueAsDate != null && (!isDateValid(valueAsDate) || !isDayInRange(valueAsDate, [minDate, maxDate])); // Effects // ------------------------------------------------------------------------ React.useEffect(function () { if (isControlled) { setValue(valueFromProps); } }, [valueFromProps]); React.useEffect(function () { // uncontrolled mode, updating initial timezone value if (defaultTimezone !== undefined && isValidTimezone(defaultTimezone)) { setTimezoneValue(defaultTimezone); } }, [defaultTimezone]); React.useEffect(function () { // controlled mode, updating timezone value if (timezone !== undefined && isValidTimezone(timezone)) { setTimezoneValue(timezone); } }, [timezone]); React.useEffect(function () { if (isControlled && !isInputFocused) { setInputValue(formattedDateString); } }, [formattedDateString]); // Popover contents (date picker) // ------------------------------------------------------------------------ var handlePopoverClose = React.useCallback(function (e) { var _a; (_a = popoverProps.onClose) === null || _a === void 0 ? void 0 : _a.call(popoverProps, e); setIsOpen(false); }, []); var handleDateChange = React.useCallback(function (newDate, isUserChange, didSubmitWithEnter) { var _a, _b; if (didSubmitWithEnter === void 0) { didSubmitWithEnter = false; } var prevDate = valueAsDate; if (newDate === null) { if (!isControlled && !didSubmitWithEnter) { // user clicked on current day in the calendar, so we should clear the input when uncontrolled setInputValue(""); } (_a = props.onChange) === null || _a === void 0 ? void 0 : _a.call(props, null, isUserChange); return; } // this change handler was triggered by a change in month, day, or (if // enabled) time. for UX purposes, we want to close the popover only if // the user explicitly clicked a day within the current month. var newIsOpen = !isUserChange || !props.closeOnSelection || (prevDate != null && (hasMonthChanged(prevDate, newDate) || (timePrecision !== undefined && hasTimeChanged(prevDate, newDate)))); // if selecting a date via click or Tab, the input will already be // blurred by now, so sync isInputFocused to false. if selecting via // Enter, setting isInputFocused to false won't do anything by itself, // plus we want the field to retain focus anyway. // (note: spelling out the ternary explicitly reads more clearly.) var newIsInputFocused = didSubmitWithEnter ? true : false; if (isControlled) { setIsInputFocused(newIsInputFocused); setIsOpen(newIsOpen); } else { var newFormattedDateString = getFormattedDateString(newDate, props); setIsInputFocused(newIsInputFocused); setIsOpen(newIsOpen); setValue(newDate); setInputValue(newFormattedDateString); } var newIsoDateString = getIsoEquivalentWithUpdatedTimezone(newDate, timezoneValue, timePrecision); (_b = props.onChange) === null || _b === void 0 ? void 0 : _b.call(props, newIsoDateString, isUserChange); }, [props.onChange, timezoneValue, timePrecision, valueAsDate]); var dayPickerProps = __assign(__assign({}, props.dayPickerProps), { onDayKeyDown: function (day, modifiers, e) { var _a, _b; (_b = (_a = props.dayPickerProps) === null || _a === void 0 ? void 0 : _a.onDayKeyDown) === null || _b === void 0 ? void 0 : _b.call(_a, day, modifiers, e); }, onMonthChange: function (month) { var _a, _b; (_b = (_a = props.dayPickerProps) === null || _a === void 0 ? void 0 : _a.onMonthChange) === null || _b === void 0 ? void 0 : _b.call(_a, month); } }); var handleShortcutChange = React.useCallback(function (_, index) { setSelectedShortcutIndex(index); }, []); var handleStartFocusBoundaryFocusIn = React.useCallback(function (e) { var _a, _b, _c; if ((_a = popoverContentRef.current) === null || _a === void 0 ? void 0 : _a.contains(getRelatedTargetWithFallback(e))) { // Not closing Popover to allow user to freely switch between manually entering a date // string in the input and selecting one via the Popover (_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.focus(); } else { (_c = getKeyboardFocusableElements(popoverContentRef).shift()) === null || _c === void 0 ? void 0 : _c.focus(); } }, []); var handleEndFocusBoundaryFocusIn = React.useCallback(function (e) { var _a, _b, _c; if ((_a = popoverContentRef.current) === null || _a === void 0 ? void 0 : _a.contains(getRelatedTargetWithFallback(e))) { (_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.focus(); handlePopoverClose(e); } else { (_c = getKeyboardFocusableElements(popoverContentRef).pop()) === null || _c === void 0 ? void 0 : _c.focus(); } }, []); // React's onFocus prop listens to the focusin browser event under the hood, so it's safe to // provide it the focusIn event handlers instead of using a ref and manually adding the // event listeners ourselves. var popoverContent = (React.createElement("div", { ref: popoverContentRef, role: "dialog", "aria-label": "date picker", id: popoverId }, React.createElement("div", { onFocus: handleStartFocusBoundaryFocusIn, tabIndex: 0 }), React.createElement(DatePicker, __assign({}, datePickerProps, { dayPickerProps: dayPickerProps, maxDate: maxDate, minDate: minDate, onChange: handleDateChange, onShortcutChange: handleShortcutChange, selectedShortcutIndex: selectedShortcutIndex, timePrecision: timePrecision, // the rest of this component handles invalid dates gracefully (to show error messages), // but DatePicker does not, so we must take care to filter those out value: isErrorState ? null : valueAsDate })), React.createElement("div", { onFocus: handleEndFocusBoundaryFocusIn, tabIndex: 0 }))); // Timezone select // ------------------------------------------------------------------------ // we need a date which is guaranteed to be non-null here; if necessary, // we use today's date and shift it to the desired/current timezone var tzSelectDate = React.useMemo(function () { return valueAsDate != null && isDateValid(valueAsDate) ? valueAsDate : convertLocalDateToTimezoneTime(new Date(), timezoneValue); }, [timezoneValue, valueAsDate]); var isTimezoneSelectHidden = timePrecision === undefined || showTimezoneSelect === false; var isTimezoneSelectDisabled = props.disabled || disableTimezoneSelect; var handleTimezoneChange = React.useCallback(function (newTimezone) { var _a, _b; if (timezone === undefined) { // uncontrolled timezone setTimezoneValue(newTimezone); } (_a = props.onTimezoneChange) === null || _a === void 0 ? void 0 : _a.call(props, newTimezone); if (valueAsDate != null) { var newDateString = getIsoEquivalentWithUpdatedTimezone(valueAsDate, newTimezone, timePrecision); (_b = props.onChange) === null || _b === void 0 ? void 0 : _b.call(props, newDateString, true); } }, [props.onChange, valueAsDate, timePrecision]); var maybeTimezonePicker = isTimezoneSelectHidden ? undefined : (React.createElement(TimezoneSelect, { buttonProps: timezoneSelectButtonProps, className: Classes.DATE_INPUT_TIMEZONE_SELECT, date: tzSelectDate, disabled: isTimezoneSelectDisabled, onChange: handleTimezoneChange, value: timezoneValue }, React.createElement(Tag, { interactive: !isTimezoneSelectDisabled, minimal: true, rightIcon: isTimezoneSelectDisabled ? undefined : "caret-down" }, getTimezoneShortName(timezoneValue, tzSelectDate)))); // Text input // ------------------------------------------------------------------------ var parseDate = React.useCallback(function (dateString) { if (dateString === props.outOfRangeMessage || dateString === props.invalidDateMessage) { return null; } var newDate = props.parseDate(dateString, props.locale); return newDate === false ? INVALID_DATE : newDate; }, // HACKHACK: ESLint false positive // eslint-disable-next-line @typescript-eslint/unbound-method [props.outOfRangeMessage, props.invalidDateMessage, props.parseDate, props.locale]); var handleInputFocus = React.useCallback(function (e) { var _a, _b; setIsInputFocused(true); setIsOpen(true); setInputValue(formattedDateString); (_b = (_a = props.inputProps) === null || _a === void 0 ? void 0 : _a.onFocus) === null || _b === void 0 ? void 0 : _b.call(_a, e); }, [formattedDateString, (_a = props.inputProps) === null || _a === void 0 ? void 0 : _a.onFocus]); var handleInputBlur = React.useCallback(function (e) { var _a, _b, _c, _d; if (inputValue == null || valueAsDate == null) { return; } var date = parseDate(inputValue); if (inputValue.length > 0 && inputValue !== formattedDateString && (!isDateValid(date) || !isDayInRange(date, [minDate, maxDate]))) { if (isControlled) { setIsInputFocused(false); } else { setIsInputFocused(false); setValue(date); setInputValue(undefined); } if (date === null) { (_a = props.onChange) === null || _a === void 0 ? void 0 : _a.call(props, null, true); } else { (_b = props.onError) === null || _b === void 0 ? void 0 : _b.call(props, date); } } else { if (inputValue.length === 0) { setIsInputFocused(false); setValue(null); setInputValue(undefined); } else { setIsInputFocused(false); } } (_d = (_c = props.inputProps) === null || _c === void 0 ? void 0 : _c.onBlur) === null || _d === void 0 ? void 0 : _d.call(_c, e); }, [ parseDate, formattedDateString, inputValue, valueAsDate, minDate, maxDate, props.onChange, props.onError, (_b = props.inputProps) === null || _b === void 0 ? void 0 : _b.onBlur, ]); var handleInputChange = React.useCallback(function (e) { var _a, _b, _c, _d; var valueString = e.target.value; var inputValueAsDate = parseDate(valueString); if (isDateValid(inputValueAsDate) && isDayInRange(inputValueAsDate, [minDate, maxDate])) { if (isControlled) { setInputValue(valueString); } else { setValue(inputValueAsDate); setInputValue(valueString); } var newIsoDateString = getIsoEquivalentWithUpdatedTimezone(inputValueAsDate, timezoneValue, timePrecision); (_a = props.onChange) === null || _a === void 0 ? void 0 : _a.call(props, newIsoDateString, true); } else { if (valueString.length === 0) { (_b = props.onChange) === null || _b === void 0 ? void 0 : _b.call(props, null, true); } setValue(inputValueAsDate); setInputValue(valueString); } (_d = (_c = props.inputProps) === null || _c === void 0 ? void 0 : _c.onChange) === null || _d === void 0 ? void 0 : _d.call(_c, e); }, [minDate, maxDate, timezoneValue, timePrecision, parseDate, props.onChange, (_c = props.inputProps) === null || _c === void 0 ? void 0 : _c.onChange]); var handleInputClick = React.useCallback(function (e) { var _a, _b; // stop propagation to the Popover's internal handleTargetClick handler; // otherwise, the popover will flicker closed as soon as it opens. e.stopPropagation(); (_b = (_a = props.inputProps) === null || _a === void 0 ? void 0 : _a.onClick) === null || _b === void 0 ? void 0 : _b.call(_a, e); }, [(_d = props.inputProps) === null || _d === void 0 ? void 0 : _d.onClick]); var handleInputKeyDown = React.useCallback(function (e) { var _a, _b, _c, _d; if (e.key === "Tab" && e.shiftKey) { // close popover on SHIFT+TAB key press handlePopoverClose(e); } else if (e.key === "Tab" && isOpen) { (_a = getKeyboardFocusableElements(popoverContentRef).shift()) === null || _a === void 0 ? void 0 : _a.focus(); // necessary to prevent focusing the second focusable element e.preventDefault(); } else if (e.key === "Escape") { setIsOpen(false); (_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.blur(); } else if (e.key === "Enter" && inputValue != null) { var nextDate = parseDate(inputValue); if (isDateValid(nextDate)) { handleDateChange(nextDate, true, true); } } (_d = (_c = props.inputProps) === null || _c === void 0 ? void 0 : _c.onKeyDown) === null || _d === void 0 ? void 0 : _d.call(_c, e); }, [inputValue, parseDate, (_e = props.inputProps) === null || _e === void 0 ? void 0 : _e.onKeyDown]); // Main render // ------------------------------------------------------------------------ var shouldShowErrorStyling = !isInputFocused || inputValue === props.outOfRangeMessage || inputValue === props.invalidDateMessage; // We use the renderTarget API to flatten the rendered DOM and make it easier to implement features like the "fill" prop. var renderTarget = React.useCallback(function (_a) { var _b, _c, _d; var targetIsOpen = _a.isOpen, ref = _a.ref, targetProps = __rest(_a, ["isOpen", "ref"]); return (React.createElement(InputGroup, __assign({ autoComplete: "off", className: classNames(targetProps.className, inputProps.className), intent: shouldShowErrorStyling && isErrorState ? "danger" : "none", placeholder: placeholder, rightElement: React.createElement(React.Fragment, null, props.rightElement, maybeTimezonePicker), tagName: popoverProps.targetTagName, type: "text", role: "combobox" }, targetProps, inputProps, { "aria-controls": popoverId, "aria-expanded": targetIsOpen, disabled: props.disabled, fill: fill, inputRef: mergeRefs(ref, inputRef, (_c = (_b = props.inputProps) === null || _b === void 0 ? void 0 : _b.inputRef) !== null && _c !== void 0 ? _c : null), onBlur: handleInputBlur, onChange: handleInputChange, onClick: handleInputClick, onFocus: handleInputFocus, onKeyDown: handleInputKeyDown, value: (_d = (isInputFocused ? inputValue : formattedDateString)) !== null && _d !== void 0 ? _d : "" }))); }, [ fill, formattedDateString, inputValue, isInputFocused, isTimezoneSelectDisabled, isTimezoneSelectHidden, placeholder, shouldShowErrorStyling, timezoneValue, props.disabled, props.inputProps, props.rightElement, ]); // N.B. no need to set `fill` since that is unused with the `renderTarget` API return (React.createElement(Popover, __assign({ isOpen: isOpen && !props.disabled }, popoverProps, { autoFocus: false, className: classNames(Classes.DATE_INPUT, popoverProps.className, props.className), content: popoverContent, enforceFocus: false, onClose: handlePopoverClose, popoverClassName: classNames(Classes.DATE_INPUT_POPOVER, popoverProps.popoverClassName), ref: popoverRef, renderTarget: renderTarget }))); }); DateInput.displayName = "".concat(DISPLAYNAME_PREFIX, ".DateInput"); DateInput.defaultProps = { closeOnSelection: true, disabled: false, invalidDateMessage: "Invalid date", maxDate: DEFAULT_MAX_DATE, minDate: DEFAULT_MIN_DATE, outOfRangeMessage: "Out of range", reverseMonthAndYearMenus: false, }; function getInitialTimezoneValue(_a) { var defaultTimezone = _a.defaultTimezone, timezone = _a.timezone; if (timezone !== undefined) { // controlled mode if (isValidTimezone(timezone)) { return timezone; } else { console.error(Errors.DATEINPUT_INVALID_TIMEZONE); return UTC_TIME.ianaCode; } } else if (defaultTimezone !== undefined) { // uncontrolled mode with initial value if (isValidTimezone(defaultTimezone)) { return defaultTimezone; } else { console.error(Errors.DATEINPUT_INVALID_DEFAULT_TIMEZONE); return UTC_TIME.ianaCode; } } else { // uncontrolled mode return getCurrentTimezone(); } } function getRelatedTargetWithFallback(e) { var _a; return ((_a = e.relatedTarget) !== null && _a !== void 0 ? _a : Utils.getActiveElement(e.currentTarget)); } function getKeyboardFocusableElements(popoverContentRef) { if (popoverContentRef.current === null) { return []; } var elements = Array.from(popoverContentRef.current.querySelectorAll("button:not([disabled]),input,[tabindex]:not([tabindex='-1'])")); // Remove focus boundary div elements elements.pop(); elements.shift(); return elements; } //# sourceMappingURL=dateInput.js.map