UNPKG

@blueprintjs/datetime

Version:

Components for interacting with dates and times

411 lines 22.6 kB
"use strict"; /* * Copyright 2023 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. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.DateInput = exports.DATEINPUT_DEFAULT_PROPS = void 0; const tslib_1 = require("tslib"); const classnames_1 = tslib_1.__importDefault(require("classnames")); const React = tslib_1.__importStar(require("react")); const core_1 = require("@blueprintjs/core"); const common_1 = require("../../common"); const dateFnsFormatUtils_1 = require("../../common/dateFnsFormatUtils"); const dateFnsLocaleUtils_1 = require("../../common/dateFnsLocaleUtils"); const datePicker_1 = require("../date-picker/datePicker"); const datePickerUtils_1 = require("../date-picker/datePickerUtils"); const timezoneSelect_1 = require("../timezone-select/timezoneSelect"); const useDateFormatter_1 = require("./useDateFormatter"); const useDateParser_1 = require("./useDateParser"); const timezoneSelectButtonProps = { fill: false, minimal: true, outlined: true, }; exports.DATEINPUT_DEFAULT_PROPS = { closeOnSelection: true, disabled: false, invalidDateMessage: "Invalid date", locale: "en-US", maxDate: datePickerUtils_1.DatePickerUtils.getDefaultMaxDate(), minDate: datePickerUtils_1.DatePickerUtils.getDefaultMinDate(), outOfRangeMessage: "Out of range", reverseMonthAndYearMenus: false, }; /** * Date input component. * * @see https://blueprintjs.com/docs/#datetime/date-input */ exports.DateInput = React.memo(function DateInput(props) { const { closeOnSelection, dateFnsFormat, dateFnsLocaleLoader, defaultTimezone, defaultValue, disabled, disableTimezoneSelect, fill, inputProps = {}, invalidDateMessage, locale: localeOrCode, maxDate, minDate, onChange, onError, onTimezoneChange, outOfRangeMessage, popoverProps = {}, popoverRef, rightElement, showTimezoneSelect, timePrecision, timezone: controlledTimezone, value, ...datePickerProps } = props; const locale = (0, dateFnsLocaleUtils_1.useDateFnsLocale)(localeOrCode, dateFnsLocaleLoader); const placeholder = getPlaceholder(props); const formatDateString = (0, useDateFormatter_1.useDateFormatter)(props, locale); const parseDateString = (0, useDateParser_1.useDateParser)(props, locale); // Refs // ------------------------------------------------------------------------ const inputRef = React.useRef(null); const popoverContentRef = React.useRef(null); const popoverId = core_1.Utils.uniqueId("date-picker"); // State // ------------------------------------------------------------------------ const [isOpen, setIsOpen] = React.useState(false); const [timezoneValue, setTimezoneValue] = React.useState(getInitialTimezoneValue(props)); const valueFromProps = React.useMemo(() => common_1.TimezoneUtils.getDateObjectFromIsoString(value, timezoneValue), [timezoneValue, value]); const isControlled = valueFromProps !== undefined; const defaultValueFromProps = React.useMemo(() => common_1.TimezoneUtils.getDateObjectFromIsoString(defaultValue, timezoneValue), [defaultValue, timezoneValue]); const [valueAsDate, setValue] = React.useState(isControlled ? valueFromProps : defaultValueFromProps); const [selectedShortcutIndex, setSelectedShortcutIndex] = React.useState(undefined); const [isInputFocused, setIsInputFocused] = React.useState(false); // rendered as the text input's value const formattedDateString = React.useMemo(() => (valueAsDate === null ? undefined : formatDateString(valueAsDate)), [valueAsDate, formatDateString]); const [inputValue, setInputValue] = React.useState(formattedDateString !== null && formattedDateString !== void 0 ? formattedDateString : undefined); const isErrorState = valueAsDate != null && (!common_1.DateUtils.isDateValid(valueAsDate) || !common_1.DateUtils.isDayInRange(valueAsDate, [minDate, maxDate])); // Effects // ------------------------------------------------------------------------ React.useEffect(() => { if (isControlled) { setValue(valueFromProps); } }, [isControlled, valueFromProps]); React.useEffect(() => { // uncontrolled mode, updating initial timezone value if (defaultTimezone !== undefined && common_1.TimezoneNameUtils.isValidTimezone(defaultTimezone)) { setTimezoneValue(defaultTimezone); } }, [defaultTimezone]); React.useEffect(() => { // controlled mode, updating timezone value if (controlledTimezone !== undefined && common_1.TimezoneNameUtils.isValidTimezone(controlledTimezone)) { setTimezoneValue(controlledTimezone); } }, [controlledTimezone]); React.useEffect(() => { if (isControlled && !isInputFocused) { setInputValue(formattedDateString); } }, [isControlled, isInputFocused, formattedDateString]); // Popover contents (date picker) // ------------------------------------------------------------------------ const handlePopoverClose = React.useCallback((e) => { var _a; (_a = popoverProps.onClose) === null || _a === void 0 ? void 0 : _a.call(popoverProps, e); setIsOpen(false); }, [popoverProps]); const handleDateChange = React.useCallback((newDate, isUserChange, didSubmitWithEnter = false) => { const 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(""); } onChange === null || onChange === void 0 ? void 0 : onChange(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. const newIsOpen = !isUserChange || !closeOnSelection || (prevDate != null && (common_1.DateUtils.hasMonthChanged(prevDate, newDate) || (timePrecision !== undefined && common_1.DateUtils.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.) const newIsInputFocused = didSubmitWithEnter ? true : false; if (isControlled) { setIsInputFocused(newIsInputFocused); setIsOpen(newIsOpen); } else { const newFormattedDateString = formatDateString(newDate); setIsInputFocused(newIsInputFocused); setIsOpen(newIsOpen); setValue(newDate); setInputValue(newFormattedDateString); } const newIsoDateString = common_1.TimezoneUtils.getIsoEquivalentWithUpdatedTimezone(newDate, timezoneValue, timePrecision); onChange === null || onChange === void 0 ? void 0 : onChange(newIsoDateString, isUserChange); }, [closeOnSelection, isControlled, formatDateString, onChange, timezoneValue, timePrecision, valueAsDate]); const dayPickerProps = { ...props.dayPickerProps, onDayKeyDown: (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: (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); }, }; const handleShortcutChange = React.useCallback((_, index) => { setSelectedShortcutIndex(index); }, []); const handleStartFocusBoundaryFocusIn = React.useCallback((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(); } }, []); const handleEndFocusBoundaryFocusIn = React.useCallback((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(); } }, [handlePopoverClose]); // 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. const popoverContent = (React.createElement("div", { ref: popoverContentRef, role: "dialog", "aria-label": "date picker", id: popoverId }, React.createElement("div", { onFocus: handleStartFocusBoundaryFocusIn, tabIndex: 0 }), React.createElement(datePicker_1.DatePicker, { ...datePickerProps, dateFnsLocaleLoader: dateFnsLocaleLoader, dayPickerProps: dayPickerProps, locale: locale, maxDate: maxDate, minDate: minDate, onChange: handleDateChange, onShortcutChange: handleShortcutChange, selectedShortcutIndex: selectedShortcutIndex, timePrecision: timePrecision, timezone: timezoneValue, // 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 const tzSelectDate = React.useMemo(() => valueAsDate != null && common_1.DateUtils.isDateValid(valueAsDate) ? valueAsDate : common_1.TimezoneUtils.convertLocalDateToTimezoneTime(new Date(), timezoneValue), [timezoneValue, valueAsDate]); const isTimezoneSelectHidden = timePrecision === undefined || showTimezoneSelect === false; const isTimezoneSelectDisabled = disabled || disableTimezoneSelect; const handleTimezoneChange = React.useCallback((newTimezone) => { if (controlledTimezone === undefined) { // uncontrolled timezone setTimezoneValue(newTimezone); } onTimezoneChange === null || onTimezoneChange === void 0 ? void 0 : onTimezoneChange(newTimezone); if (valueAsDate != null) { const newDateString = common_1.TimezoneUtils.getIsoEquivalentWithUpdatedTimezone(valueAsDate, newTimezone, timePrecision); onChange === null || onChange === void 0 ? void 0 : onChange(newDateString, true); } }, [onChange, onTimezoneChange, valueAsDate, timePrecision, controlledTimezone]); const maybeTimezonePicker = React.useMemo(() => isTimezoneSelectHidden ? undefined : (React.createElement(timezoneSelect_1.TimezoneSelect, { buttonProps: timezoneSelectButtonProps, className: common_1.Classes.DATE_INPUT_TIMEZONE_SELECT, date: tzSelectDate, disabled: isTimezoneSelectDisabled, onChange: handleTimezoneChange, value: timezoneValue }, React.createElement(core_1.Tag, { endIcon: isTimezoneSelectDisabled ? undefined : "caret-down", interactive: !isTimezoneSelectDisabled, minimal: true }, common_1.TimezoneNameUtils.getTimezoneShortName(timezoneValue, tzSelectDate)))), [handleTimezoneChange, isTimezoneSelectDisabled, isTimezoneSelectHidden, timezoneValue, tzSelectDate]); // Text input // ------------------------------------------------------------------------ const handleInputFocus = React.useCallback((e) => { var _a; setIsInputFocused(true); setIsOpen(true); setInputValue(formattedDateString); (_a = inputProps === null || inputProps === void 0 ? void 0 : inputProps.onFocus) === null || _a === void 0 ? void 0 : _a.call(inputProps, e); }, [formattedDateString, inputProps]); const handleInputBlur = React.useCallback((e) => { var _a; if (inputValue == null || valueAsDate == null) { setIsInputFocused(false); return; } const date = parseDateString(inputValue); if (inputValue.length > 0 && inputValue !== formattedDateString && (!common_1.DateUtils.isDateValid(date) || !common_1.DateUtils.isDayInRange(date, [minDate, maxDate]))) { if (isControlled) { setIsInputFocused(false); } else { setIsInputFocused(false); setValue(date); setInputValue(undefined); } if (date === null) { onChange === null || onChange === void 0 ? void 0 : onChange(null, true); } else { onError === null || onError === void 0 ? void 0 : onError(date); } } else { if (inputValue.length === 0) { setIsInputFocused(false); setValue(null); setInputValue(undefined); } else { setIsInputFocused(false); } } (_a = inputProps === null || inputProps === void 0 ? void 0 : inputProps.onBlur) === null || _a === void 0 ? void 0 : _a.call(inputProps, e); }, [ formattedDateString, inputProps, inputValue, isControlled, maxDate, minDate, onChange, onError, parseDateString, valueAsDate, ]); const handleInputChange = React.useCallback((e) => { var _a; const valueString = e.target.value; const inputValueAsDate = parseDateString(valueString); if (common_1.DateUtils.isDateValid(inputValueAsDate) && common_1.DateUtils.isDayInRange(inputValueAsDate, [minDate, maxDate])) { if (isControlled) { setInputValue(valueString); } else { setValue(inputValueAsDate); setInputValue(valueString); } const newIsoDateString = common_1.TimezoneUtils.getIsoEquivalentWithUpdatedTimezone(inputValueAsDate, timezoneValue, timePrecision); onChange === null || onChange === void 0 ? void 0 : onChange(newIsoDateString, true); } else { if (valueString.length === 0) { onChange === null || onChange === void 0 ? void 0 : onChange(null, true); } setValue(inputValueAsDate); setInputValue(valueString); } (_a = inputProps === null || inputProps === void 0 ? void 0 : inputProps.onChange) === null || _a === void 0 ? void 0 : _a.call(inputProps, e); }, [isControlled, minDate, maxDate, timezoneValue, timePrecision, parseDateString, onChange, inputProps]); const handleInputClick = React.useCallback((e) => { var _a; // stop propagation to the Popover's internal handleTargetClick handler; // otherwise, the popover will flicker closed as soon as it opens. e.stopPropagation(); (_a = inputProps === null || inputProps === void 0 ? void 0 : inputProps.onClick) === null || _a === void 0 ? void 0 : _a.call(inputProps, e); }, [inputProps]); const handleInputKeyDown = React.useCallback((e) => { var _a, _b, _c; 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) { const nextDate = parseDateString(inputValue); if (common_1.DateUtils.isDateValid(nextDate)) { handleDateChange(nextDate, true, true); } } (_c = inputProps === null || inputProps === void 0 ? void 0 : inputProps.onKeyDown) === null || _c === void 0 ? void 0 : _c.call(inputProps, e); }, [handleDateChange, handlePopoverClose, inputProps, inputValue, isOpen, parseDateString]); // Main render // ------------------------------------------------------------------------ const shouldShowErrorStyling = !isInputFocused || inputValue === outOfRangeMessage || inputValue === invalidDateMessage; // We use the renderTarget API to flatten the rendered DOM and make it easier to implement features like the "fill" prop. const renderTarget = React.useCallback(({ isOpen: targetIsOpen, ref, ...targetProps }) => { var _a; return (React.createElement(core_1.InputGroup, { autoComplete: "off", className: (0, classnames_1.default)(targetProps.className, inputProps.className), intent: shouldShowErrorStyling && isErrorState ? core_1.Intent.DANGER : core_1.Intent.NONE, placeholder: placeholder, rightElement: React.createElement(React.Fragment, null, rightElement, maybeTimezonePicker), tagName: popoverProps.targetTagName, type: "text", role: "combobox", ...targetProps, ...inputProps, "aria-controls": popoverId, "aria-expanded": targetIsOpen, disabled: disabled, fill: fill, inputRef: (0, core_1.mergeRefs)(ref, inputRef, inputProps === null || inputProps === void 0 ? void 0 : inputProps.inputRef), onBlur: handleInputBlur, onChange: handleInputChange, onClick: handleInputClick, onFocus: handleInputFocus, onKeyDown: handleInputKeyDown, value: (_a = (isInputFocused ? inputValue : formattedDateString)) !== null && _a !== void 0 ? _a : "" })); }, [ disabled, fill, formattedDateString, handleInputBlur, handleInputChange, handleInputClick, handleInputFocus, handleInputKeyDown, inputProps, inputValue, isErrorState, isInputFocused, maybeTimezonePicker, placeholder, popoverId, popoverProps.targetTagName, rightElement, shouldShowErrorStyling, ]); // N.B. no need to set `fill` since that is unused with the `renderTarget` API return (React.createElement(core_1.Popover, { isOpen: isOpen && !disabled, ...popoverProps, autoFocus: false, className: (0, classnames_1.default)(common_1.Classes.DATE_INPUT, popoverProps.className, props.className), content: popoverContent, enforceFocus: false, onClose: handlePopoverClose, popoverClassName: (0, classnames_1.default)(common_1.Classes.DATE_INPUT_POPOVER, popoverProps.popoverClassName), ref: popoverRef, renderTarget: renderTarget })); }); exports.DateInput.displayName = `${core_1.DISPLAYNAME_PREFIX}.DateInput`; // TODO: Removing `defaultProps` here breaks tests. Investigate why. // eslint-disable-next-line @typescript-eslint/no-deprecated exports.DateInput.defaultProps = exports.DATEINPUT_DEFAULT_PROPS; /** Gets the input `placeholder` value from props, using default values if undefined */ function getPlaceholder(props) { var _a; if (props.placeholder !== undefined || (props.formatDate !== undefined && props.parseDate !== undefined)) { return props.placeholder; } else { return (_a = props.dateFnsFormat) !== null && _a !== void 0 ? _a : (0, dateFnsFormatUtils_1.getDefaultDateFnsFormat)(props); } } function getInitialTimezoneValue({ defaultTimezone, timezone }) { if (timezone !== undefined) { // controlled mode if (common_1.TimezoneNameUtils.isValidTimezone(timezone)) { return timezone; } else { console.error(common_1.Errors.DATEINPUT_INVALID_TIMEZONE); return common_1.TimezoneUtils.UTC_TIME.ianaCode; } } else if (defaultTimezone !== undefined) { // uncontrolled mode with initial value if (common_1.TimezoneNameUtils.isValidTimezone(defaultTimezone)) { return defaultTimezone; } else { console.error(common_1.Errors.DATEINPUT_INVALID_DEFAULT_TIMEZONE); return common_1.TimezoneUtils.UTC_TIME.ianaCode; } } else { // uncontrolled mode return common_1.TimezoneUtils.getCurrentTimezone(); } } function getRelatedTargetWithFallback(e) { var _a; return (_a = e.relatedTarget) !== null && _a !== void 0 ? _a : core_1.Utils.getActiveElement(e.currentTarget); } function getKeyboardFocusableElements(popoverContentRef) { if (popoverContentRef.current === null) { return []; } const elements = core_1.Utils.getFocusableElements(popoverContentRef.current); // Remove focus boundary div elements elements.pop(); elements.shift(); return elements; } //# sourceMappingURL=dateInput.js.map