UNPKG

13.1 kBJavaScriptView Raw
1/*
2 * Copyright 2016 Palantir Technologies, Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16/**
17 * @fileoverview This component is DEPRECATED, and the code is frozen.
18 * All changes & bugfixes should be made to DateInput2 in the datetime2
19 * package instead.
20 */
21/* eslint-disable deprecation/deprecation, @blueprintjs/no-deprecated-components */
22import classNames from "classnames";
23import * as React from "react";
24import { AbstractPureComponent2, DISPLAYNAME_PREFIX, InputGroup, Intent, Keys, Popover, refHandler, setRef, } from "@blueprintjs/core";
25import * as Classes from "./common/classes";
26import { isDateValid, isDayInRange } from "./common/dateUtils";
27import { getFormattedDateString } from "./dateFormat";
28import { DatePicker } from "./datePicker";
29import { getDefaultMaxDate, getDefaultMinDate } from "./datePickerCore";
30/**
31 * Date input component.
32 *
33 * @see https://blueprintjs.com/docs/#datetime/dateinput
34 * @deprecated use { DateInput2 } from "@blueprintjs/datetime2"
35 */
36export class DateInput extends AbstractPureComponent2 {
37 static displayName = `${DISPLAYNAME_PREFIX}.DateInput`;
38 static defaultProps = {
39 closeOnSelection: true,
40 dayPickerProps: {},
41 disabled: false,
42 invalidDateMessage: "Invalid date",
43 maxDate: getDefaultMaxDate(),
44 minDate: getDefaultMinDate(),
45 outOfRangeMessage: "Out of range",
46 reverseMonthAndYearMenus: false,
47 };
48 state = {
49 isInputFocused: false,
50 isOpen: false,
51 value: this.props.value !== undefined ? this.props.value : this.props.defaultValue,
52 valueString: null,
53 };
54 inputElement = null;
55 popoverContentElement = null;
56 handleInputRef = refHandler(this, "inputElement", this.props.inputProps?.inputRef);
57 handlePopoverContentRef = refHandler(this, "popoverContentElement");
58 render() {
59 const { value, valueString } = this.state;
60 const dateString = this.state.isInputFocused ? valueString : getFormattedDateString(value, this.props);
61 const dateValue = isDateValid(value) ? value : null;
62 const dayPickerProps = {
63 ...this.props.dayPickerProps,
64 onDayKeyDown: (day, modifiers, e) => {
65 this.props.dayPickerProps.onDayKeyDown?.(day, modifiers, e);
66 },
67 onMonthChange: (month) => {
68 this.props.dayPickerProps.onMonthChange?.(month);
69 },
70 };
71 // React's onFocus prop listens to the focusin browser event under the hood, so it's safe to
72 // provide it the focusIn event handlers instead of using a ref and manually adding the
73 // event listeners ourselves.
74 const wrappedPopoverContent = (React.createElement("div", { ref: this.handlePopoverContentRef },
75 React.createElement("div", { onFocus: this.handleStartFocusBoundaryFocusIn, tabIndex: 0 }),
76 React.createElement(DatePicker, { ...this.props, dayPickerProps: dayPickerProps, onChange: this.handleDateChange, value: dateValue, onShortcutChange: this.handleShortcutChange, selectedShortcutIndex: this.state.selectedShortcutIndex }),
77 React.createElement("div", { onFocus: this.handleEndFocusBoundaryFocusIn, tabIndex: 0 })));
78 // assign default empty object here to prevent mutation
79 const { inputProps = {}, popoverProps = {} } = this.props;
80 const isErrorState = value != null && (!isDateValid(value) || !this.isDateInRange(value));
81 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) },
82 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 })));
83 }
84 componentDidUpdate(prevProps, prevState) {
85 super.componentDidUpdate(prevProps, prevState);
86 if (prevProps.inputProps?.inputRef !== this.props.inputProps?.inputRef) {
87 setRef(prevProps.inputProps?.inputRef, null);
88 this.handleInputRef = refHandler(this, "inputElement", this.props.inputProps?.inputRef);
89 setRef(this.props.inputProps?.inputRef, this.inputElement);
90 }
91 if (prevProps.value !== this.props.value) {
92 this.setState({ value: this.props.value });
93 }
94 }
95 isDateInRange(value) {
96 return isDayInRange(value, [this.props.minDate, this.props.maxDate]);
97 }
98 handleClosePopover = (e) => {
99 const { popoverProps = {} } = this.props;
100 popoverProps.onClose?.(e);
101 this.setState({ isOpen: false });
102 };
103 handleDateChange = (newDate, isUserChange, didSubmitWithEnter = false) => {
104 const prevDate = this.state.value;
105 // this change handler was triggered by a change in month, day, or (if
106 // enabled) time. for UX purposes, we want to close the popover only if
107 // the user explicitly clicked a day within the current month.
108 const isOpen = !isUserChange ||
109 !this.props.closeOnSelection ||
110 (prevDate != null && (this.hasMonthChanged(prevDate, newDate) || this.hasTimeChanged(prevDate, newDate)));
111 // if selecting a date via click or Tab, the input will already be
112 // blurred by now, so sync isInputFocused to false. if selecting via
113 // Enter, setting isInputFocused to false won't do anything by itself,
114 // plus we want the field to retain focus anyway.
115 // (note: spelling out the ternary explicitly reads more clearly.)
116 const isInputFocused = didSubmitWithEnter ? true : false;
117 if (this.props.value === undefined) {
118 const valueString = getFormattedDateString(newDate, this.props);
119 this.setState({ isInputFocused, isOpen, value: newDate, valueString });
120 }
121 else {
122 this.setState({ isInputFocused, isOpen });
123 }
124 this.props.onChange?.(newDate, isUserChange);
125 };
126 hasMonthChanged(prevDate, nextDate) {
127 return (prevDate == null) !== (nextDate == null) || nextDate.getMonth() !== prevDate.getMonth();
128 }
129 hasTimeChanged(prevDate, nextDate) {
130 if (this.props.timePrecision == null) {
131 return false;
132 }
133 return ((prevDate == null) !== (nextDate == null) ||
134 nextDate.getHours() !== prevDate.getHours() ||
135 nextDate.getMinutes() !== prevDate.getMinutes() ||
136 nextDate.getSeconds() !== prevDate.getSeconds() ||
137 nextDate.getMilliseconds() !== prevDate.getMilliseconds());
138 }
139 handleInputFocus = (e) => {
140 const valueString = this.state.value == null ? "" : this.formatDate(this.state.value);
141 this.setState({ isInputFocused: true, isOpen: true, valueString });
142 this.safeInvokeInputProp("onFocus", e);
143 };
144 handleInputClick = (e) => {
145 // stop propagation to the Popover's internal handleTargetClick handler;
146 // otherwise, the popover will flicker closed as soon as it opens.
147 e.stopPropagation();
148 this.safeInvokeInputProp("onClick", e);
149 };
150 handleInputChange = (e) => {
151 const valueString = e.target.value;
152 const value = this.parseDate(valueString);
153 if (isDateValid(value) && this.isDateInRange(value)) {
154 if (this.props.value === undefined) {
155 this.setState({ value, valueString });
156 }
157 else {
158 this.setState({ valueString });
159 }
160 this.props.onChange?.(value, true);
161 }
162 else {
163 if (valueString.length === 0) {
164 this.props.onChange?.(null, true);
165 }
166 this.setState({ valueString });
167 }
168 this.safeInvokeInputProp("onChange", e);
169 };
170 handleInputBlur = (e) => {
171 const { valueString } = this.state;
172 const date = this.parseDate(valueString);
173 if (valueString.length > 0 &&
174 valueString !== getFormattedDateString(this.state.value, this.props) &&
175 (!isDateValid(date) || !this.isDateInRange(date))) {
176 if (this.props.value === undefined) {
177 this.setState({ isInputFocused: false, value: date, valueString: null });
178 }
179 else {
180 this.setState({ isInputFocused: false });
181 }
182 if (isNaN(date.valueOf())) {
183 this.props.onError?.(new Date(undefined));
184 }
185 else if (!this.isDateInRange(date)) {
186 this.props.onError?.(date);
187 }
188 else {
189 this.props.onChange?.(date, true);
190 }
191 }
192 else {
193 if (valueString.length === 0) {
194 this.setState({ isInputFocused: false, value: null, valueString: null });
195 }
196 else {
197 this.setState({ isInputFocused: false });
198 }
199 }
200 this.safeInvokeInputProp("onBlur", e);
201 };
202 handleInputKeyDown = (e) => {
203 // HACKHACK: https://github.com/palantir/blueprint/issues/4165
204 if (e.which === Keys.ENTER) {
205 const nextDate = this.parseDate(this.state.valueString);
206 this.handleDateChange(nextDate, true, true);
207 }
208 else if (e.which === Keys.TAB && e.shiftKey) {
209 // close popover on SHIFT+TAB key press
210 this.handleClosePopover();
211 }
212 else if (e.which === Keys.TAB && this.state.isOpen) {
213 this.getKeyboardFocusableElements().shift()?.focus();
214 // necessary to prevent focusing the second focusable element
215 e.preventDefault();
216 }
217 else if (e.which === Keys.ESCAPE) {
218 this.setState({ isOpen: false });
219 this.inputElement?.blur();
220 }
221 this.safeInvokeInputProp("onKeyDown", e);
222 };
223 getKeyboardFocusableElements = () => {
224 const elements = Array.from(this.popoverContentElement?.querySelectorAll("button:not([disabled]),input,[tabindex]:not([tabindex='-1'])"));
225 // Remove focus boundary div elements
226 elements.pop();
227 elements.shift();
228 return elements;
229 };
230 handleStartFocusBoundaryFocusIn = (e) => {
231 if (this.popoverContentElement.contains(this.getRelatedTarget(e))) {
232 // Not closing Popover to allow user to freely switch between manually entering a date
233 // string in the input and selecting one via the Popover
234 this.inputElement?.focus();
235 }
236 else {
237 this.getKeyboardFocusableElements().shift()?.focus();
238 }
239 };
240 handleEndFocusBoundaryFocusIn = (e) => {
241 if (this.popoverContentElement.contains(this.getRelatedTarget(e))) {
242 this.inputElement?.focus();
243 this.handleClosePopover();
244 }
245 else {
246 this.getKeyboardFocusableElements().pop()?.focus();
247 }
248 };
249 getRelatedTarget(e) {
250 // Support IE11 (#2924)
251 return (e.relatedTarget ?? document.activeElement);
252 }
253 handleShortcutChange = (_, selectedShortcutIndex) => {
254 this.setState({ selectedShortcutIndex });
255 };
256 /** safe wrapper around invoking input props event handler (prop defaults to undefined) */
257 safeInvokeInputProp(name, e) {
258 const { inputProps = {} } = this.props;
259 inputProps[name]?.(e);
260 }
261 parseDate(dateString) {
262 if (dateString === this.props.outOfRangeMessage || dateString === this.props.invalidDateMessage) {
263 return null;
264 }
265 const { locale, parseDate } = this.props;
266 const newDate = parseDate(dateString, locale);
267 return newDate === false ? new Date(undefined) : newDate;
268 }
269 formatDate(date) {
270 if (!isDateValid(date) || !this.isDateInRange(date)) {
271 return "";
272 }
273 const { locale, formatDate } = this.props;
274 return formatDate(date, locale);
275 }
276}
277//# sourceMappingURL=dateInput.js.map
\No newline at end of file