UNPKG

@blueprintjs/datetime

Version:

Components for interacting with dates and times

852 lines 45.6 kB
/* * 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. */ import classNames from "classnames"; import { isSameDay, isValid } from "date-fns"; import * as React from "react"; import { Boundary, Classes as CoreClasses, DISPLAYNAME_PREFIX, InputGroup, Intent, Popover, refHandler, setRef, Utils, } from "@blueprintjs/core"; import { Classes, DateUtils, Errors } from "../../common"; import { getDateFnsFormatter, getDateFnsParser, getDefaultDateFnsFormat } from "../../common/dateFnsFormatUtils"; import { getLocaleCodeFromProps } from "../../common/dateFnsLocaleProps"; import { DatePickerUtils } from "../date-picker/datePickerUtils"; import { DateRangePicker } from "../date-range-picker/dateRangePicker"; import { DateFnsLocalizedComponent } from "../dateFnsLocalizedComponent"; import { clampDate, getTodayAtMidnight, isEntireInputSelected, shiftDateByArrowKey, shiftDateByDays, } from "./dateRangeInputUilts"; /** * Date range input component. * * @see https://blueprintjs.com/docs/#datetime/date-range-input */ export class DateRangeInput extends DateFnsLocalizedComponent { constructor(props) { var _a, _b; super(props); this.startInputElement = null; this.endInputElement = null; this.handleStartInputRef = refHandler(this, "startInputElement", (_a = this.props.startInputProps) === null || _a === void 0 ? void 0 : _a.inputRef); this.handleEndInputRef = refHandler(this, "endInputElement", (_b = this.props.endInputProps) === null || _b === void 0 ? void 0 : _b.inputRef); // We use the renderTarget API to flatten the rendered DOM. this.renderTarget = // N.B. pull out `isOpen` so that it's not forwarded to the DOM. ({ isOpen, ...targetProps }) => { const { fill, popoverProps = {} } = this.props; const { targetTagName = "div" } = popoverProps; return React.createElement(targetTagName, { ...targetProps, className: classNames(CoreClasses.CONTROL_GROUP, targetProps.className, { [CoreClasses.FILL]: fill, }), }, this.renderInputGroup(Boundary.START), this.renderInputGroup(Boundary.END)); }; this.renderInputGroup = (boundary) => { var _a; const inputProps = this.getInputProps(boundary); const handleInputEvent = boundary === Boundary.START ? this.handleStartInputEvent : this.handleEndInputEvent; return (React.createElement(InputGroup, { autoComplete: "off", disabled: (_a = inputProps === null || inputProps === void 0 ? void 0 : inputProps.disabled) !== null && _a !== void 0 ? _a : this.props.disabled, fill: this.props.fill, ...inputProps, intent: this.isInputInErrorState(boundary) ? Intent.DANGER : inputProps === null || inputProps === void 0 ? void 0 : inputProps.intent, inputRef: this.getInputRef(boundary), onBlur: handleInputEvent, onChange: handleInputEvent, onClick: handleInputEvent, onFocus: handleInputEvent, onKeyDown: handleInputEvent, onMouseDown: handleInputEvent, placeholder: this.getInputPlaceholderString(boundary), value: this.getInputDisplayString(boundary) })); }; // Callbacks - DateRangePicker // =========================== this.handleDateRangePickerChange = (selectedRange, didSubmitWithEnter = false) => { var _a, _b; // ignore mouse events in the date-range picker if the popover is animating closed. if (!this.state.isOpen) { return; } const [selectedStart, selectedEnd] = selectedRange; let isOpen = true; let isStartInputFocused; let isEndInputFocused; let startHoverString; let endHoverString; let boundaryToModify; if (selectedStart == null) { // focus the start field by default or if only an end date is specified if (this.props.timePrecision == null) { isStartInputFocused = true; isEndInputFocused = false; } else { isStartInputFocused = false; isEndInputFocused = false; boundaryToModify = Boundary.START; } // for clarity, hide the hover string until the mouse moves over a different date startHoverString = null; } else if (selectedEnd == null) { // focus the end field if a start date is specified if (this.props.timePrecision == null) { isStartInputFocused = false; isEndInputFocused = true; } else { isStartInputFocused = false; isEndInputFocused = false; boundaryToModify = Boundary.END; } endHoverString = null; } else if (this.props.closeOnSelection) { isOpen = this.getIsOpenValueWhenDateChanges(selectedStart, selectedEnd); isStartInputFocused = false; if (this.props.timePrecision == null && didSubmitWithEnter) { // if we submit via click or Tab, the focus will have moved already. // it we submit with Enter, the focus won't have moved, and setting // the flag to false won't have an effect anyway, so leave it true. isEndInputFocused = true; } else { isEndInputFocused = false; boundaryToModify = Boundary.END; } } else if (this.state.lastFocusedField === Boundary.START) { // keep the start field focused if (this.props.timePrecision == null) { isStartInputFocused = true; isEndInputFocused = false; } else { isStartInputFocused = false; isEndInputFocused = false; boundaryToModify = Boundary.START; } } else if (this.props.timePrecision == null) { // keep the end field focused isStartInputFocused = false; isEndInputFocused = true; } else { isStartInputFocused = false; isEndInputFocused = false; boundaryToModify = Boundary.END; } const baseStateChange = { boundaryToModify, endHoverString, endInputString: this.formatDate(selectedEnd), isEndInputFocused, isOpen, isStartInputFocused, selectedShortcutIndex: -1, startHoverString, startInputString: this.formatDate(selectedStart), wasLastFocusChangeDueToHover: false, }; if (this.isControlled()) { this.setState(baseStateChange); } else { this.setState({ ...baseStateChange, selectedEnd, selectedStart }); } (_b = (_a = this.props).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, selectedRange); }; this.handleShortcutChange = (_, selectedShortcutIndex) => { this.setState({ selectedShortcutIndex }); }; this.handleDateRangePickerHoverChange = (hoveredRange, _hoveredDay, hoveredBoundary) => { // ignore mouse events in the date-range picker if the popover is animating closed. if (!this.state.isOpen) { return; } if (hoveredRange == null) { // undo whatever focus changes we made while hovering over various calendar dates const isEndInputFocused = this.state.boundaryToModify === Boundary.END; this.setState({ endHoverString: null, isEndInputFocused, isStartInputFocused: !isEndInputFocused, lastFocusedField: this.state.boundaryToModify, startHoverString: null, }); } else { const [hoveredStart, hoveredEnd] = hoveredRange; const isStartInputFocused = hoveredBoundary != null ? hoveredBoundary === Boundary.START : this.state.isStartInputFocused; const isEndInputFocused = hoveredBoundary != null ? hoveredBoundary === Boundary.END : this.state.isEndInputFocused; this.setState({ endHoverString: this.formatDate(hoveredEnd), isEndInputFocused, isStartInputFocused, lastFocusedField: isStartInputFocused ? Boundary.START : Boundary.END, shouldSelectAfterUpdate: this.props.selectAllOnFocus, startHoverString: this.formatDate(hoveredStart), wasLastFocusChangeDueToHover: true, }); } }; // Callbacks - Input // ================= // instantiate these two functions once so we don't have to for each callback on each render. this.handleStartInputEvent = (e) => { this.handleInputEvent(e, Boundary.START); }; this.handleEndInputEvent = (e) => { this.handleInputEvent(e, Boundary.END); }; this.handleInputEvent = (e, boundary) => { var _a, _b, _c, _d, _f, _g; const inputProps = this.getInputProps(boundary); switch (e.type) { case "blur": this.handleInputBlur(e, boundary); (_a = inputProps === null || inputProps === void 0 ? void 0 : inputProps.onBlur) === null || _a === void 0 ? void 0 : _a.call(inputProps, e); break; case "change": this.handleInputChange(e, boundary); (_b = inputProps === null || inputProps === void 0 ? void 0 : inputProps.onChange) === null || _b === void 0 ? void 0 : _b.call(inputProps, e); break; case "click": e = e; this.handleInputClick(e); (_c = inputProps === null || inputProps === void 0 ? void 0 : inputProps.onClick) === null || _c === void 0 ? void 0 : _c.call(inputProps, e); break; case "focus": this.handleInputFocus(e, boundary); (_d = inputProps === null || inputProps === void 0 ? void 0 : inputProps.onFocus) === null || _d === void 0 ? void 0 : _d.call(inputProps, e); break; case "keydown": e = e; this.handleInputKeyDown(e, boundary); (_f = inputProps === null || inputProps === void 0 ? void 0 : inputProps.onKeyDown) === null || _f === void 0 ? void 0 : _f.call(inputProps, e); break; case "mousedown": e = e; this.handleInputMouseDown(); (_g = inputProps === null || inputProps === void 0 ? void 0 : inputProps.onMouseDown) === null || _g === void 0 ? void 0 : _g.call(inputProps, e); break; default: break; } }; // add a keydown listener to persistently change focus when tabbing: // - if focused in start field, Tab moves focus to end field // - if focused in end field, Shift+Tab moves focus to start field this.handleInputKeyDown = (e, boundary) => { var _a, _b; const isArrowKeyPresssed = e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "ArrowLeft" || e.key === "ArrowRight"; const isTabPressed = e.key === "Tab"; const isEnterPressed = e.key === "Enter"; const isEscapeKeyPressed = e.key === "Escape"; const isShiftPressed = e.shiftKey; const { selectedStart, selectedEnd } = this.state; if (isArrowKeyPresssed) { this.handleInputArrowKeyDown(e, boundary); return; } if (isEscapeKeyPressed) { (_a = this.startInputElement) === null || _a === void 0 ? void 0 : _a.blur(); (_b = this.endInputElement) === null || _b === void 0 ? void 0 : _b.blur(); this.setState({ isEndInputFocused: false, isOpen: false, isStartInputFocused: false }); return; } // order of JS events is our enemy here. when tabbing between fields, // this handler will fire in the middle of a focus exchange when no // field is currently focused. we work around this by referring to the // most recently focused field, rather than the currently focused field. const wasStartFieldFocused = this.state.lastFocusedField === Boundary.START; const wasEndFieldFocused = this.state.lastFocusedField === Boundary.END; // move focus to the other field if (isTabPressed) { let isEndInputFocused; let isStartInputFocused; let isOpen = true; if (wasStartFieldFocused && !isShiftPressed) { isStartInputFocused = false; isEndInputFocused = true; // prevent the default focus-change behavior to avoid race conditions; // we'll handle the focus change ourselves in componentDidUpdate. e.preventDefault(); } else if (wasEndFieldFocused && isShiftPressed) { isStartInputFocused = true; isEndInputFocused = false; e.preventDefault(); } else { // don't prevent default here, otherwise Tab won't do anything. isStartInputFocused = false; isEndInputFocused = false; isOpen = false; } this.setState({ isEndInputFocused, isOpen, isStartInputFocused, wasLastFocusChangeDueToHover: false, }); } else if (wasStartFieldFocused && isEnterPressed) { const nextStartDate = this.parseDate(this.state.startInputString); this.handleDateRangePickerChange([nextStartDate, selectedEnd], true); } else if (wasEndFieldFocused && isEnterPressed) { const nextEndDate = this.parseDate(this.state.endInputString); this.handleDateRangePickerChange([selectedStart, nextEndDate], true); } else { // let the default keystroke happen without side effects return; } }; this.handleInputArrowKeyDown = (e, boundary) => { var _a, _b, _c; const { minDate, maxDate } = this.props; const { isOpen } = this.state; const inputElement = boundary === Boundary.START ? this.startInputElement : this.endInputElement; if (!isOpen || !isEntireInputSelected(inputElement)) { return; } const shiftedDate = (_a = this.getNextDateForArrowKeyNavigation(e.key, boundary)) !== null && _a !== void 0 ? _a : this.getDefaultDateForArrowKeyNavigation(e.key, boundary); if (shiftedDate == null) { return; } // We've commited to moving the selection, prevent the default arrow key interactions e.preventDefault(); const clampedDate = clampDate(shiftedDate, minDate, maxDate); const { keys } = this.getStateKeysAndValuesForBoundary(boundary); const nextState = { [keys.inputString]: this.formatDate(clampedDate), selectedShortcutIndex: -1, shouldSelectAfterUpdate: true, }; if (!this.isControlled()) { nextState[keys.selectedValue] = clampedDate; } (_c = (_b = this.props).onChange) === null || _c === void 0 ? void 0 : _c.call(_b, this.getDateRangeForCallback(clampedDate, boundary)); this.setState(nextState); }; this.handleInputMouseDown = () => { // clicking in the field constitutes an explicit focus change. we update // the flag on "mousedown" instead of on "click", because it needs to be // set before onFocus is called ("click" triggers after "focus"). this.setState({ wasLastFocusChangeDueToHover: false }); }; this.handleInputClick = (e) => { // unless we stop propagation on this event, a click within an input // will close the popover almost as soon as it opens. e.stopPropagation(); }; this.handleInputFocus = (_e, boundary) => { const { keys, values } = this.getStateKeysAndValuesForBoundary(boundary); const isValueControlled = this.isControlled(); // We may be reacting to a programmatic focus triggered by componentDidUpdate() at a point when // values.selectedValue may not have been updated yet in controlled mode, so we must use values.controlledValue // in that case. const inputString = formatDateString(isValueControlled ? values.controlledValue : values.selectedValue, this.props, this.state.locale, true); // change the boundary only if the user explicitly focused in the field. // focus changes from hovering don't count; they're just temporary. const boundaryToModify = this.state.wasLastFocusChangeDueToHover ? this.state.boundaryToModify : boundary; this.setState({ [keys.inputString]: inputString, [keys.isInputFocused]: true, boundaryToModify, isOpen: true, lastFocusedField: boundary, shouldSelectAfterUpdate: this.props.selectAllOnFocus, wasLastFocusChangeDueToHover: false, }); }; this.handleInputBlur = (_e, boundary) => { var _a, _b; const { keys, values } = this.getStateKeysAndValuesForBoundary(boundary); const maybeNextDate = this.parseDate(values.inputString); const isValueControlled = this.isControlled(); let nextState = { [keys.isInputFocused]: false, shouldSelectAfterUpdate: false, }; if (this.isInputEmpty(values.inputString)) { if (isValueControlled) { nextState = { ...nextState, [keys.inputString]: formatDateString(values.controlledValue, this.props, this.state.locale), }; } else { nextState = { ...nextState, [keys.inputString]: null, [keys.selectedValue]: null, }; } } else if (!this.isNextDateRangeValid(maybeNextDate, boundary)) { if (!isValueControlled) { nextState = { ...nextState, [keys.inputString]: null, [keys.selectedValue]: maybeNextDate, }; } (_b = (_a = this.props).onError) === null || _b === void 0 ? void 0 : _b.call(_a, this.getDateRangeForCallback(maybeNextDate, boundary)); } this.setState(nextState); }; this.handleInputChange = (e, boundary) => { var _a, _b, _c, _d; const inputString = e.target.value; const { keys } = this.getStateKeysAndValuesForBoundary(boundary); const maybeNextDate = this.parseDate(inputString); const isValueControlled = this.isControlled(); let nextState = { shouldSelectAfterUpdate: false }; if (inputString.length === 0) { // this case will be relevant when we start showing the hovered range in the input // fields. goal is to show an empty field for clarity until the mouse moves over a // different date. const baseState = { ...nextState, [keys.inputString]: "" }; if (isValueControlled) { nextState = baseState; } else { nextState = { ...baseState, [keys.selectedValue]: null }; } (_b = (_a = this.props).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, this.getDateRangeForCallback(null, boundary)); } else if (this.isDateValidAndInRange(maybeNextDate)) { // note that error cases that depend on both fields (e.g. overlapping dates) should fall // through into this block so that the UI can update immediately, possibly with an error // message on the other field. // also, clear the hover string to ensure the most recent keystroke appears. const baseState = { ...nextState, [keys.hoverString]: null, [keys.inputString]: inputString, }; if (isValueControlled) { nextState = baseState; } else { nextState = { ...baseState, [keys.selectedValue]: maybeNextDate }; } if (this.isNextDateRangeValid(maybeNextDate, boundary)) { (_d = (_c = this.props).onChange) === null || _d === void 0 ? void 0 : _d.call(_c, this.getDateRangeForCallback(maybeNextDate, boundary)); } } else { // again, clear the hover string to ensure the most recent keystroke appears nextState = { ...nextState, [keys.inputString]: inputString, [keys.hoverString]: null }; } this.setState(nextState); }; // Callbacks - Popover // =================== this.handlePopoverClose = (event) => { var _a, _b; this.setState({ isOpen: false }); (_b = (_a = this.props.popoverProps) === null || _a === void 0 ? void 0 : _a.onClose) === null || _b === void 0 ? void 0 : _b.call(_a, event); }; this.getIsOpenValueWhenDateChanges = (nextSelectedStart, nextSelectedEnd) => { if (this.props.closeOnSelection) { // trivial case when TimePicker is not shown if (this.props.timePrecision == null) { return false; } const fallbackDate = getTodayAtMidnight(); const [selectedStart, selectedEnd] = this.getSelectedRange([fallbackDate, fallbackDate]); // case to check if the user has changed TimePicker values if (DateUtils.isSameTime(selectedStart, nextSelectedStart) && DateUtils.isSameTime(selectedEnd, nextSelectedEnd)) { return false; } return true; } return true; }; this.getInitialRange = (props = this.props) => { const { defaultValue, value } = props; if (value != null) { return value; } else if (defaultValue != null) { return defaultValue; } else { return [null, null]; } }; this.getSelectedRange = (fallbackRange) => { let selectedStart; let selectedEnd; if (this.isControlled()) { [selectedStart, selectedEnd] = this.props.value; } else { selectedStart = this.state.selectedStart; selectedEnd = this.state.selectedEnd; } // this helper function checks if the provided boundary date *would* overlap the selected // other boundary date. providing the already-selected start date simply tells us if we're // currently in an overlapping state. const doBoundaryDatesOverlap = this.doBoundaryDatesOverlap(selectedStart, Boundary.START); const dateRange = [selectedStart, doBoundaryDatesOverlap ? undefined : selectedEnd]; return dateRange.map((selectedBound, index) => { const fallbackDate = fallbackRange != null ? fallbackRange[index] : undefined; return this.isDateValidAndInRange(selectedBound !== null && selectedBound !== void 0 ? selectedBound : null) ? selectedBound : fallbackDate; }); }; this.getInputDisplayString = (boundary) => { const { values } = this.getStateKeysAndValuesForBoundary(boundary); const { isInputFocused, inputString, selectedValue, hoverString } = values; if (hoverString != null) { return hoverString; } else if (isInputFocused) { return inputString == null ? "" : inputString; } else if (selectedValue == null) { return ""; } else if (this.doesEndBoundaryOverlapStartBoundary(selectedValue, boundary)) { return this.props.overlappingDatesMessage; } else { return formatDateString(selectedValue, this.props, this.state.locale); } }; this.getInputPlaceholderString = (boundary) => { const isStartBoundary = boundary === Boundary.START; const isEndBoundary = boundary === Boundary.END; const inputProps = this.getInputProps(boundary); const { isInputFocused } = this.getStateKeysAndValuesForBoundary(boundary).values; // use the custom placeholder text for the input, if providied if ((inputProps === null || inputProps === void 0 ? void 0 : inputProps.placeholder) != null) { return inputProps.placeholder; } else if (isStartBoundary) { return isInputFocused ? this.state.formattedMinDateString : "Start date"; } else if (isEndBoundary) { return isInputFocused ? this.state.formattedMaxDateString : "End date"; } else { return ""; } }; this.getInputProps = (boundary) => { return boundary === Boundary.START ? this.props.startInputProps : this.props.endInputProps; }; this.getInputRef = (boundary) => { return boundary === Boundary.START ? this.handleStartInputRef : this.handleEndInputRef; }; this.getStateKeysAndValuesForBoundary = (boundary) => { const controlledRange = this.props.value; if (boundary === Boundary.START) { return { keys: { hoverString: "startHoverString", inputString: "startInputString", isInputFocused: "isStartInputFocused", selectedValue: "selectedStart", }, values: { controlledValue: controlledRange != null ? controlledRange[0] : undefined, hoverString: this.state.startHoverString, inputString: this.state.startInputString, isInputFocused: this.state.isStartInputFocused, selectedValue: this.state.selectedStart, }, }; } else { return { keys: { hoverString: "endHoverString", inputString: "endInputString", isInputFocused: "isEndInputFocused", selectedValue: "selectedEnd", }, values: { controlledValue: controlledRange != null ? controlledRange[1] : undefined, hoverString: this.state.endHoverString, inputString: this.state.endInputString, isInputFocused: this.state.isEndInputFocused, selectedValue: this.state.selectedEnd, }, }; } }; this.getDateRangeForCallback = (currDate, currBoundary) => { const otherBoundary = this.getOtherBoundary(currBoundary); const otherDate = this.getStateKeysAndValuesForBoundary(otherBoundary).values.selectedValue; return currBoundary === Boundary.START ? [currDate, otherDate] : [otherDate, currDate]; }; this.getOtherBoundary = (boundary) => { return boundary === Boundary.START ? Boundary.END : Boundary.START; }; this.doBoundaryDatesOverlap = (date, boundary) => { const { allowSingleDayRange } = this.props; const otherBoundary = this.getOtherBoundary(boundary); const otherBoundaryDate = this.getStateKeysAndValuesForBoundary(otherBoundary).values.selectedValue; if (date == null || otherBoundaryDate == null) { return false; } if (boundary === Boundary.START) { const isAfter = date > otherBoundaryDate; return isAfter || (!allowSingleDayRange && isSameDay(date, otherBoundaryDate)); } else { const isBefore = date < otherBoundaryDate; return isBefore || (!allowSingleDayRange && isSameDay(date, otherBoundaryDate)); } }; /** * Returns true if the provided boundary is an END boundary overlapping the * selected start date. (If the boundaries overlap, we consider the END * boundary to be erroneous.) */ this.doesEndBoundaryOverlapStartBoundary = (boundaryDate, boundary) => { return boundary === Boundary.START ? false : this.doBoundaryDatesOverlap(boundaryDate, boundary); }; this.isControlled = () => this.props.value !== undefined; this.isInputEmpty = (inputString) => inputString == null || inputString.length === 0; this.isInputInErrorState = (boundary) => { const values = this.getStateKeysAndValuesForBoundary(boundary).values; const { isInputFocused, hoverString, inputString, selectedValue } = values; if (hoverString != null || this.isInputEmpty(inputString)) { // don't show an error state while we're hovering over a valid date. return false; } const boundaryValue = isInputFocused ? this.parseDate(inputString) : selectedValue; return (boundaryValue != null && (!this.isDateValidAndInRange(boundaryValue) || this.doesEndBoundaryOverlapStartBoundary(boundaryValue, boundary))); }; this.isDateValidAndInRange = (date) => { // min/max dates defined in defaultProps return isValid(date) && DateUtils.isDayInRange(date, [this.props.minDate, this.props.maxDate]); }; // this is a slightly kludgy function, but it saves us a good amount of repeated code between // the constructor and componentDidUpdate. this.formatMinMaxDateString = (props, propName) => { var _a, _b; const date = props[propName]; // N.B. default values are applied only if a prop is strictly `undefined` // See: https://facebook.github.io/react/docs/react-component.html#defaultprops const defaultDate = DateRangeInput.defaultProps[propName]; // N.B. this.state will be undefined in the constructor, so we need a fallback in that case const maybeLocale = ((_b = (_a = this.state) === null || _a === void 0 ? void 0 : _a.locale) !== null && _b !== void 0 ? _b : typeof props.locale === "string") ? undefined : props.locale; return formatDateString(date !== null && date !== void 0 ? date : defaultDate, this.props, maybeLocale); }; this.parseDate = (dateString) => { var _a; if (dateString === undefined || dateString === this.props.outOfRangeMessage || dateString === this.props.invalidDateMessage) { return null; } // HACKHACK: this code below is largely copied from the `useDateParser()` hook, which is the preferred // implementation that we can migrate to once DateRangeInput is a function component. const { dateFnsFormat, locale: localeFromProps, parseDate, timePickerProps, timePrecision } = this.props; const { locale } = this.state; let newDate = null; if (parseDate !== undefined) { // user-provided date parser newDate = parseDate(dateString, (_a = locale === null || locale === void 0 ? void 0 : locale.code) !== null && _a !== void 0 ? _a : getLocaleCodeFromProps(localeFromProps)); } else { // use user-provided date-fns format or one of the default formats inferred from time picker props const format = dateFnsFormat !== null && dateFnsFormat !== void 0 ? dateFnsFormat : getDefaultDateFnsFormat({ timePickerProps, timePrecision }); newDate = getDateFnsParser(format, locale)(dateString); } return newDate === false ? getTodayAtMidnight() : newDate; }; // called on date hover & selection this.formatDate = (date) => { var _a; if (!this.isDateValidAndInRange(date)) { return ""; } // HACKHACK: the code below is largely copied from the `useDateFormatter()` hook, which is the preferred // implementation that we can migrate to once DateRangeInput is a function component. const { dateFnsFormat, formatDate, locale: localeFromProps, timePickerProps, timePrecision } = this.props; const { locale } = this.state; if (formatDate !== undefined) { // user-provided date formatter return formatDate(date, (_a = locale === null || locale === void 0 ? void 0 : locale.code) !== null && _a !== void 0 ? _a : getLocaleCodeFromProps(localeFromProps)); } else { // use user-provided date-fns format or one of the default formats inferred from time picker props const format = dateFnsFormat !== null && dateFnsFormat !== void 0 ? dateFnsFormat : getDefaultDateFnsFormat({ timePickerProps, timePrecision }); return getDateFnsFormatter(format, locale)(date); } }; const [selectedStart, selectedEnd] = this.getInitialRange(); this.state = { formattedMaxDateString: this.formatMinMaxDateString(props, "maxDate"), formattedMinDateString: this.formatMinMaxDateString(props, "minDate"), isEndInputFocused: false, isOpen: false, isStartInputFocused: false, locale: undefined, selectedEnd, selectedShortcutIndex: -1, selectedStart, }; } async componentDidMount() { await super.componentDidMount(); } async componentDidUpdate(prevProps) { var _a, _b, _c, _d, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q; super.componentDidUpdate(prevProps); const { isStartInputFocused, isEndInputFocused, shouldSelectAfterUpdate } = this.state; if (((_a = prevProps.startInputProps) === null || _a === void 0 ? void 0 : _a.inputRef) !== ((_b = this.props.startInputProps) === null || _b === void 0 ? void 0 : _b.inputRef)) { setRef((_c = prevProps.startInputProps) === null || _c === void 0 ? void 0 : _c.inputRef, null); this.handleStartInputRef = refHandler(this, "startInputElement", (_d = this.props.startInputProps) === null || _d === void 0 ? void 0 : _d.inputRef); setRef((_f = this.props.startInputProps) === null || _f === void 0 ? void 0 : _f.inputRef, this.startInputElement); } if (((_g = prevProps.endInputProps) === null || _g === void 0 ? void 0 : _g.inputRef) !== ((_h = this.props.endInputProps) === null || _h === void 0 ? void 0 : _h.inputRef)) { setRef((_j = prevProps.endInputProps) === null || _j === void 0 ? void 0 : _j.inputRef, null); this.handleEndInputRef = refHandler(this, "endInputElement", (_k = this.props.endInputProps) === null || _k === void 0 ? void 0 : _k.inputRef); setRef((_l = this.props.endInputProps) === null || _l === void 0 ? void 0 : _l.inputRef, this.endInputElement); } const shouldFocusStartInput = this.shouldFocusInputRef(isStartInputFocused, this.startInputElement); const shouldFocusEndInput = this.shouldFocusInputRef(isEndInputFocused, this.endInputElement); if (shouldFocusStartInput) { (_m = this.startInputElement) === null || _m === void 0 ? void 0 : _m.focus(); } else if (shouldFocusEndInput) { (_o = this.endInputElement) === null || _o === void 0 ? void 0 : _o.focus(); } if (isStartInputFocused && shouldSelectAfterUpdate) { (_p = this.startInputElement) === null || _p === void 0 ? void 0 : _p.select(); } else if (isEndInputFocused && shouldSelectAfterUpdate) { (_q = this.endInputElement) === null || _q === void 0 ? void 0 : _q.select(); } let nextState = {}; if (this.props.value !== prevProps.value) { const [selectedStart, selectedEnd] = this.getInitialRange(this.props); nextState = { ...nextState, selectedEnd, selectedStart, }; } // cache the formatted date strings to avoid computing on each render. if (this.props.minDate !== prevProps.minDate) { const formattedMinDateString = this.formatMinMaxDateString(this.props, "minDate"); nextState = { ...nextState, formattedMinDateString }; } if (this.props.maxDate !== prevProps.maxDate) { const formattedMaxDateString = this.formatMinMaxDateString(this.props, "maxDate"); nextState = { ...nextState, formattedMaxDateString }; } this.setState(nextState); } render() { const { locale, selectedShortcutIndex } = this.state; const { popoverProps = {}, popoverRef } = this.props; const popoverContent = (React.createElement(DateRangePicker, { ...this.props, boundaryToModify: this.state.boundaryToModify, locale: locale !== null && locale !== void 0 ? locale : this.props.locale, onChange: this.handleDateRangePickerChange, onHoverChange: this.handleDateRangePickerHoverChange, onShortcutChange: this.handleShortcutChange, selectedShortcutIndex: selectedShortcutIndex, value: this.getSelectedRange() })); // allow custom props for the popover and each input group, but pass them in an order that // guarantees only some props are overridable. return (React.createElement(Popover, { isOpen: this.state.isOpen, placement: "bottom-start", ...popoverProps, autoFocus: false, className: classNames(Classes.DATE_RANGE_INPUT, popoverProps.className, this.props.className), content: popoverContent, enforceFocus: false, onClose: this.handlePopoverClose, popoverClassName: classNames(Classes.DATE_RANGE_INPUT_POPOVER, popoverProps.popoverClassName), ref: popoverRef, renderTarget: this.renderTarget })); } validateProps(props) { if (props.value === null) { // throw a blocking error here because we don't handle a null value gracefully across this component // (it's not allowed by TS under strict null checks anyway) throw new Error(Errors.DATERANGEINPUT_NULL_VALUE); } } getNextDateForArrowKeyNavigation(arrowKey, boundary) { const { allowSingleDayRange } = this.props; const [selectedStart, selectedEnd] = this.getSelectedRange(); const initialDate = boundary === Boundary.START ? selectedStart : selectedEnd; if (initialDate == null) { return undefined; } const relativeDate = shiftDateByArrowKey(initialDate, arrowKey); // Ensure that we don't move onto a single day range selection if that is disallowed const adjustedStart = selectedStart == null || allowSingleDayRange ? selectedStart : shiftDateByDays(selectedStart, 1); const adjustedEnd = selectedEnd == null || allowSingleDayRange ? selectedEnd : shiftDateByDays(selectedEnd, -1); return boundary === Boundary.START ? clampDate(relativeDate, undefined, adjustedEnd) : clampDate(relativeDate, adjustedStart, undefined); } getDefaultDateForArrowKeyNavigation(arrowKey, boundary) { const [selectedStart, selectedEnd] = this.getSelectedRange(); const otherBoundary = boundary === Boundary.START ? selectedEnd : selectedStart; if (otherBoundary == null) { return getTodayAtMidnight(); } const isForwardArrowKey = arrowKey === "ArrowRight" || arrowKey === "ArrowDown"; // If the arrow key direction is in the same direction as the boundary, then moving that way will not create an // overlapping date range if (isForwardArrowKey === (boundary === Boundary.END)) { return shiftDateByArrowKey(otherBoundary, arrowKey); } return undefined; } // Helpers // ======= shouldFocusInputRef(isFocused, inputRef) { return isFocused && inputRef != null && Utils.getActiveElement(this.startInputElement) !== inputRef; } isNextDateRangeValid(nextDate, boundary) { return this.isDateValidAndInRange(nextDate) && !this.doBoundaryDatesOverlap(nextDate, boundary); } } DateRangeInput.defaultProps = { allowSingleDayRange: false, closeOnSelection: true, contiguousCalendarMonths: true, dayPickerProps: {}, disabled: false, endInputProps: {}, invalidDateMessage: "Invalid date", locale: "en-US", maxDate: DatePickerUtils.getDefaultMaxDate(), minDate: DatePickerUtils.getDefaultMinDate(), outOfRangeMessage: "Out of range", overlappingDatesMessage: "Overlapping dates", popoverProps: {}, selectAllOnFocus: false, shortcuts: true, singleMonthOnly: false, startInputProps: {}, }; DateRangeInput.displayName = `${DISPLAYNAME_PREFIX}.DateRangeInput`; // called on initial construction, input focus & blur, and the standard input render path function formatDateString(date, props, locale, ignoreRange = false) { var _a; const { invalidDateMessage, maxDate, minDate, outOfRangeMessage } = props; if (date == null) { return ""; } else if (!DateUtils.isDateValid(date)) { return invalidDateMessage; } else if (ignoreRange || DateUtils.isDayInRange(date, [minDate, maxDate])) { // HACKHACK: the code below is largely copied from the `useDateFormatter()` hook, which is the preferred // implementation that we can migrate to once DateRangeInput is a function component. const { dateFnsFormat, formatDate, locale: localeFromProps, timePickerProps, timePrecision } = props; if (formatDate !== undefined) { // user-provided date formatter return formatDate(date, (_a = locale === null || locale === void 0 ? void 0 : locale.code) !== null && _a !== void 0 ? _a : getLocaleCodeFromProps(localeFromProps)); } else { // use user-provided date-fns format or one of the default formats inferred from time picker props const format = dateFnsFormat !== null && dateFnsFormat !== void 0 ? dateFnsFormat : getDefaultDateFnsFormat({ timePickerProps, timePrecision }); return getDateFnsFormatter(format, locale)(date); } } else { return outOfRangeMessage; } } //# sourceMappingURL=dateRangeInput.js.map