@blueprintjs/datetime
Version:
Components for interacting with dates and times
277 lines • 13.1 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.
*/
/**
* @fileoverview This component is DEPRECATED, and the code is frozen.
* All changes & bugfixes should be made to DateInput2 in the datetime2
* package instead.
*/
/* eslint-disable deprecation/deprecation, @blueprintjs/no-deprecated-components */
import classNames from "classnames";
import * as React from "react";
import { AbstractPureComponent2, DISPLAYNAME_PREFIX, InputGroup, Intent, Keys, Popover, refHandler, setRef, } from "@blueprintjs/core";
import * as Classes from "./common/classes";
import { isDateValid, isDayInRange } from "./common/dateUtils";
import { getFormattedDateString } from "./dateFormat";
import { DatePicker } from "./datePicker";
import { getDefaultMaxDate, getDefaultMinDate } from "./datePickerCore";
/**
* Date input component.
*
* @see https://blueprintjs.com/docs/#datetime/dateinput
* @deprecated use { DateInput2 } from "@blueprintjs/datetime2"
*/
export class DateInput extends AbstractPureComponent2 {
static displayName = `${DISPLAYNAME_PREFIX}.DateInput`;
static defaultProps = {
closeOnSelection: true,
dayPickerProps: {},
disabled: false,
invalidDateMessage: "Invalid date",
maxDate: getDefaultMaxDate(),
minDate: getDefaultMinDate(),
outOfRangeMessage: "Out of range",
reverseMonthAndYearMenus: false,
};
state = {
isInputFocused: false,
isOpen: false,
value: this.props.value !== undefined ? this.props.value : this.props.defaultValue,
valueString: null,
};
inputElement = null;
popoverContentElement = null;
handleInputRef = refHandler(this, "inputElement", this.props.inputProps?.inputRef);
handlePopoverContentRef = refHandler(this, "popoverContentElement");
render() {
const { value, valueString } = this.state;
const dateString = this.state.isInputFocused ? valueString : getFormattedDateString(value, this.props);
const dateValue = isDateValid(value) ? value : null;
const dayPickerProps = {
...this.props.dayPickerProps,
onDayKeyDown: (day, modifiers, e) => {
this.props.dayPickerProps.onDayKeyDown?.(day, modifiers, e);
},
onMonthChange: (month) => {
this.props.dayPickerProps.onMonthChange?.(month);
},
};
// 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 wrappedPopoverContent = (React.createElement("div", { ref: this.handlePopoverContentRef },
React.createElement("div", { onFocus: this.handleStartFocusBoundaryFocusIn, tabIndex: 0 }),
React.createElement(DatePicker, { ...this.props, dayPickerProps: dayPickerProps, onChange: this.handleDateChange, value: dateValue, onShortcutChange: this.handleShortcutChange, selectedShortcutIndex: this.state.selectedShortcutIndex }),
React.createElement("div", { onFocus: this.handleEndFocusBoundaryFocusIn, tabIndex: 0 })));
// assign default empty object here to prevent mutation
const { inputProps = {}, popoverProps = {} } = this.props;
const isErrorState = value != null && (!isDateValid(value) || !this.isDateInRange(value));
return (React.createElement(Popover, { isOpen: this.state.isOpen && !this.props.disabled, fill: this.props.fill, ...popoverProps, autoFocus: false, className: classNames(popoverProps.className, this.props.className), content: wrappedPopoverContent, enforceFocus: false, onClose: this.handleClosePopover, popoverClassName: classNames(Classes.DATEINPUT_POPOVER, popoverProps.popoverClassName) },
React.createElement(InputGroup, { autoComplete: "off", intent: isErrorState ? Intent.DANGER : Intent.NONE, placeholder: this.props.placeholder, rightElement: this.props.rightElement, type: "text", ...inputProps, disabled: this.props.disabled, inputRef: this.handleInputRef, onBlur: this.handleInputBlur, onChange: this.handleInputChange, onClick: this.handleInputClick, onFocus: this.handleInputFocus, onKeyDown: this.handleInputKeyDown, value: dateString })));
}
componentDidUpdate(prevProps, prevState) {
super.componentDidUpdate(prevProps, prevState);
if (prevProps.inputProps?.inputRef !== this.props.inputProps?.inputRef) {
setRef(prevProps.inputProps?.inputRef, null);
this.handleInputRef = refHandler(this, "inputElement", this.props.inputProps?.inputRef);
setRef(this.props.inputProps?.inputRef, this.inputElement);
}
if (prevProps.value !== this.props.value) {
this.setState({ value: this.props.value });
}
}
isDateInRange(value) {
return isDayInRange(value, [this.props.minDate, this.props.maxDate]);
}
handleClosePopover = (e) => {
const { popoverProps = {} } = this.props;
popoverProps.onClose?.(e);
this.setState({ isOpen: false });
};
handleDateChange = (newDate, isUserChange, didSubmitWithEnter = false) => {
const prevDate = this.state.value;
// 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 isOpen = !isUserChange ||
!this.props.closeOnSelection ||
(prevDate != null && (this.hasMonthChanged(prevDate, newDate) || this.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 isInputFocused = didSubmitWithEnter ? true : false;
if (this.props.value === undefined) {
const valueString = getFormattedDateString(newDate, this.props);
this.setState({ isInputFocused, isOpen, value: newDate, valueString });
}
else {
this.setState({ isInputFocused, isOpen });
}
this.props.onChange?.(newDate, isUserChange);
};
hasMonthChanged(prevDate, nextDate) {
return (prevDate == null) !== (nextDate == null) || nextDate.getMonth() !== prevDate.getMonth();
}
hasTimeChanged(prevDate, nextDate) {
if (this.props.timePrecision == null) {
return false;
}
return ((prevDate == null) !== (nextDate == null) ||
nextDate.getHours() !== prevDate.getHours() ||
nextDate.getMinutes() !== prevDate.getMinutes() ||
nextDate.getSeconds() !== prevDate.getSeconds() ||
nextDate.getMilliseconds() !== prevDate.getMilliseconds());
}
handleInputFocus = (e) => {
const valueString = this.state.value == null ? "" : this.formatDate(this.state.value);
this.setState({ isInputFocused: true, isOpen: true, valueString });
this.safeInvokeInputProp("onFocus", e);
};
handleInputClick = (e) => {
// stop propagation to the Popover's internal handleTargetClick handler;
// otherwise, the popover will flicker closed as soon as it opens.
e.stopPropagation();
this.safeInvokeInputProp("onClick", e);
};
handleInputChange = (e) => {
const valueString = e.target.value;
const value = this.parseDate(valueString);
if (isDateValid(value) && this.isDateInRange(value)) {
if (this.props.value === undefined) {
this.setState({ value, valueString });
}
else {
this.setState({ valueString });
}
this.props.onChange?.(value, true);
}
else {
if (valueString.length === 0) {
this.props.onChange?.(null, true);
}
this.setState({ valueString });
}
this.safeInvokeInputProp("onChange", e);
};
handleInputBlur = (e) => {
const { valueString } = this.state;
const date = this.parseDate(valueString);
if (valueString.length > 0 &&
valueString !== getFormattedDateString(this.state.value, this.props) &&
(!isDateValid(date) || !this.isDateInRange(date))) {
if (this.props.value === undefined) {
this.setState({ isInputFocused: false, value: date, valueString: null });
}
else {
this.setState({ isInputFocused: false });
}
if (isNaN(date.valueOf())) {
this.props.onError?.(new Date(undefined));
}
else if (!this.isDateInRange(date)) {
this.props.onError?.(date);
}
else {
this.props.onChange?.(date, true);
}
}
else {
if (valueString.length === 0) {
this.setState({ isInputFocused: false, value: null, valueString: null });
}
else {
this.setState({ isInputFocused: false });
}
}
this.safeInvokeInputProp("onBlur", e);
};
handleInputKeyDown = (e) => {
// HACKHACK: https://github.com/palantir/blueprint/issues/4165
if (e.which === Keys.ENTER) {
const nextDate = this.parseDate(this.state.valueString);
this.handleDateChange(nextDate, true, true);
}
else if (e.which === Keys.TAB && e.shiftKey) {
// close popover on SHIFT+TAB key press
this.handleClosePopover();
}
else if (e.which === Keys.TAB && this.state.isOpen) {
this.getKeyboardFocusableElements().shift()?.focus();
// necessary to prevent focusing the second focusable element
e.preventDefault();
}
else if (e.which === Keys.ESCAPE) {
this.setState({ isOpen: false });
this.inputElement?.blur();
}
this.safeInvokeInputProp("onKeyDown", e);
};
getKeyboardFocusableElements = () => {
const elements = Array.from(this.popoverContentElement?.querySelectorAll("button:not([disabled]),input,[tabindex]:not([tabindex='-1'])"));
// Remove focus boundary div elements
elements.pop();
elements.shift();
return elements;
};
handleStartFocusBoundaryFocusIn = (e) => {
if (this.popoverContentElement.contains(this.getRelatedTarget(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
this.inputElement?.focus();
}
else {
this.getKeyboardFocusableElements().shift()?.focus();
}
};
handleEndFocusBoundaryFocusIn = (e) => {
if (this.popoverContentElement.contains(this.getRelatedTarget(e))) {
this.inputElement?.focus();
this.handleClosePopover();
}
else {
this.getKeyboardFocusableElements().pop()?.focus();
}
};
getRelatedTarget(e) {
// Support IE11 (#2924)
return (e.relatedTarget ?? document.activeElement);
}
handleShortcutChange = (_, selectedShortcutIndex) => {
this.setState({ selectedShortcutIndex });
};
/** safe wrapper around invoking input props event handler (prop defaults to undefined) */
safeInvokeInputProp(name, e) {
const { inputProps = {} } = this.props;
inputProps[name]?.(e);
}
parseDate(dateString) {
if (dateString === this.props.outOfRangeMessage || dateString === this.props.invalidDateMessage) {
return null;
}
const { locale, parseDate } = this.props;
const newDate = parseDate(dateString, locale);
return newDate === false ? new Date(undefined) : newDate;
}
formatDate(date) {
if (!isDateValid(date) || !this.isDateInRange(date)) {
return "";
}
const { locale, formatDate } = this.props;
return formatDate(date, locale);
}
}
//# sourceMappingURL=dateInput.js.map