@blueprintjs/datetime
Version:
Components for interacting with dates and times
423 lines • 23.2 kB
JavaScript
/*
* 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