UNPKG

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