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