This repository has been archived by the owner on May 24, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 72
/
DatePicker.jsx
503 lines (446 loc) · 16.6 KB
/
DatePicker.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames/bind';
import ResponsiveElement from 'terra-responsive-element';
import { injectIntl, intlShape } from 'react-intl';
/* eslint-disable-next-line */
import ReactDatePicker from './react-datepicker';
import DateInput from './DateInput';
import DateUtil from './DateUtil';
import styles from './DatePicker.module.scss';
const cx = classNames.bind(styles);
const propTypes = {
/**
* @private Whether or not to disable focus on the calendar button when the calendar picker dismisses.
*/
disableButtonFocusOnClose: PropTypes.bool,
/**
* Whether the date input should be disabled.
*/
disabled: PropTypes.bool,
/**
* An array of ISO 8601 string representation of the dates to disable in the picker. The values must be in the `YYYY-MM-DD` format.
*/
excludeDates: PropTypes.arrayOf(PropTypes.string),
/**
* A function that gets called for each date in the picker to evaluate which date should be disabled.
* A return value of true will be enabled and false will be disabled.
*/
filterDate: PropTypes.func,
/**
* An array of ISO 8601 string representation of the dates to enable in the picker. All Other dates will be disabled. The values must be in the `YYYY-MM-DD` format.
*/
includeDates: PropTypes.arrayOf(PropTypes.string),
/**
* Custom input attributes to apply to the date input. Use the name prop to set the name for the input.
* Do not set the name in inputAttribute as it will be ignored.
*/
// eslint-disable-next-line react/forbid-prop-types
inputAttributes: PropTypes.object,
/**
* @private
* intl object programmatically imported through injectIntl from react-intl.
* */
intl: intlShape.isRequired,
/**
* Whether the input displays as Incomplete. Use when no value has been provided. _(usage note: `required` must also be set)_.
*/
isIncomplete: PropTypes.bool,
/**
* Whether the input displays as Invalid. Use when value does not meet validation pattern.
*/
isInvalid: PropTypes.bool,
/**
* An ISO 8601 string representation of the maximum date that can be selected. The value must be in the `YYYY-MM-DD` format.
*/
maxDate: PropTypes.string,
/**
* An ISO 8601 string representation of the minimum date that can be selected. The value must be in the `YYYY-MM-DD` format.
*/
minDate: PropTypes.string,
/**
* Name of the date input. The name should be unique.
*/
name: PropTypes.string.isRequired,
/**
* A callback function triggered when the date picker component loses focus.
* This event does not get triggered when the focus is moved from the date input to the calendar button since the focus is still within the main date picker component.
*/
onBlur: PropTypes.func,
/**
* A callback function to execute when a valid date is selected or entered.
* The first parameter is the event. The second parameter is the changed date value.
*/
onChange: PropTypes.func,
/**
* A callback function to execute when a change is made in the date input.
* The first parameter is the event. The second parameter is the changed date value.
*/
onChangeRaw: PropTypes.func,
/**
* A callback function to execute when clicking outside of the picker to dismiss it.
*/
onClickOutside: PropTypes.func,
/**
* A callback function triggered when the date picker component receives focus.
* This event does not get triggered when the focus is moved from the date input to the calendar button since the focus is still within the main date picker component.
*/
onFocus: PropTypes.func,
/**
* A callback function to execute when a date is selected from within the picker.
*/
onSelect: PropTypes.func,
/**
* Whether or not the date is required.
*/
required: PropTypes.bool,
/**
* An ISO 8601 string representation of the default value to show in the date input. The value must be in the `YYYY-MM-DD` format.
* This is analogous to defaultvalue in a form input field.
*/
selectedDate: PropTypes.string,
/**
* The date value. This prop should only be used for a controlled date picker.
* When this prop is set a handler is needed for both the `onChange` and `onChangeRaw` props to manage the date value.
* If both `selectedDate` and this prop are set, then `selectedDate` will have no effect.
* The value must be in the `YYYY-MM-DD` format or the all-numeric date format based on the locale.
*/
value: PropTypes.string,
};
const defaultProps = {
disabled: false,
excludeDates: undefined,
filterDate: undefined,
includeDates: undefined,
inputAttributes: undefined,
isIncomplete: false,
isInvalid: false,
maxDate: undefined,
minDate: undefined,
onBlur: undefined,
onChange: undefined,
onChangeRaw: undefined,
onClickOutside: undefined,
onFocus: undefined,
onSelect: undefined,
required: false,
disableButtonFocusOnClose: false,
selectedDate: undefined,
};
class DatePicker extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedDate: DateUtil.defaultValue(props),
prevPropsSelectedDate: props.value || props.selectedDate,
};
this.datePickerContainer = React.createRef();
this.isDefaultDateAcceptable = false;
this.containerHasFocus = false;
this.handleBlur = this.handleBlur.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleChangeRaw = this.handleChangeRaw.bind(this);
this.handleFilterDate = this.handleFilterDate.bind(this);
this.handleOnSelect = this.handleOnSelect.bind(this);
this.handleOnClickOutside = this.handleOnClickOutside.bind(this);
this.handleOnInputFocus = this.handleOnInputFocus.bind(this);
this.handleFocus = this.handleFocus.bind(this);
this.handleOnCalendarButtonClick = this.handleOnCalendarButtonClick.bind(this);
}
static getDerivedStateFromProps(nextProps, prevState) {
const { selectedDate, value } = nextProps;
let nextDateValue = selectedDate;
// Use the value for a controlled component if one is provided.
if (value !== undefined && value !== null) {
nextDateValue = value;
}
if (nextDateValue !== prevState.prevPropsSelectedDate) {
const nextSelectedDate = DateUtil.createSafeDate(nextDateValue);
if (nextSelectedDate) {
return {
selectedDate: nextSelectedDate,
prevPropsSelectedDate: nextDateValue,
};
}
return {
prevPropsSelectedDate: nextDateValue,
};
}
return null;
}
componentDidMount() {
this.dateValue = DateUtil.formatMomentDate(this.state.selectedDate, DateUtil.getFormatByLocale(this.props.intl.locale)) || '';
this.isDefaultDateAcceptable = this.validateDefaultDate();
}
handleFilterDate(date) {
if (this.props.filterDate) {
return this.props.filterDate(date && date.isValid() ? date.format(DateUtil.ISO_EXTENDED_DATE_FORMAT) : '');
}
return true;
}
handleOnSelect(selectedDate, event) {
// onSelect should only be invoked when selecting a date from the picker.
// react-datepicker has an issue where onSelect is invoked both when selecting a date from the picker
// as well as manually entering a valid date or clearing the date,
// Until a fix is made, we need to return if the event type is 'change' indicating that onSelect was
// invoked from a manual change. See https://github.com/Hacker0x01/react-datepicker/issues/990
if (event.type === 'change' || !selectedDate || !selectedDate.isValid()) {
return;
}
this.dateValue = DateUtil.formatISODate(selectedDate, DateUtil.getFormatByLocale(this.props.intl.locale));
this.isDefaultDateAcceptable = true;
if (this.props.onSelect) {
this.props.onSelect(event, selectedDate.format(DateUtil.ISO_EXTENDED_DATE_FORMAT));
}
if (!this.props.disableButtonFocusOnClose) {
// Allows time for focus-trap to release focus on the picker before returning focus to the calendar button.
setTimeout(() => {
this.calendarButton.focus();
}, 100);
}
}
handleOnClickOutside(event) {
if (this.props.onClickOutside) {
this.props.onClickOutside(event);
}
}
handleBlur(event) {
// Modern browsers support event.relatedTarget but event.relatedTarget returns null in IE 10 / IE 11.
// IE 11 sets document.activeElement to the next focused element before the blur event is called.
const activeTarget = event.relatedTarget ? event.relatedTarget : document.activeElement;
// Handle blur only if focus has moved out of the entire date picker component.
if (!this.datePickerContainer.current.contains(activeTarget)) {
if (this.props.onBlur) {
const format = DateUtil.getFormatByLocale(this.props.intl.locale);
const isCompleteDate = DateUtil.isValidDate(this.dateValue, format);
const iSOString = isCompleteDate ? DateUtil.convertToISO8601(this.dateValue, format) : '';
let isValidDate = false;
if (this.dateValue === '' || (isCompleteDate && this.isDateWithinRange(DateUtil.createSafeDate(iSOString)))) {
isValidDate = true;
}
const options = {
iSO: iSOString,
inputValue: this.dateValue,
isCompleteValue: isCompleteDate,
isValidValue: isValidDate,
};
this.props.onBlur(event, options);
}
this.containerHasFocus = false;
}
}
handleChange(date, event) {
if (event.type === 'change') {
this.dateValue = event.target.value;
}
this.setState({
selectedDate: date,
});
if (this.props.onChange) {
this.props.onChange(event, date && date.isValid() ? date.format(DateUtil.ISO_EXTENDED_DATE_FORMAT) : '');
}
}
handleChangeRaw(event) {
this.dateValue = event.target.value;
if (this.props.onChangeRaw) {
this.props.onChangeRaw(event, event.target.value);
}
}
handleOnInputFocus(event) {
this.handleFocus(event);
if (!this.isDefaultDateAcceptable) {
this.dateValue = '';
this.handleChange(null, event);
this.isDefaultDateAcceptable = true;
}
}
handleFocus(event) {
// Handle focus only if focus is gained from outside of the entire date picker component.
// For IE 10/11 we cannot rely on event.relatedTarget since it is always null. Need to also check if containerHasFocus is false to
// determine if the date-picker component did not have focus but will now gain focus.
if (this.props.onFocus && !this.containerHasFocus && !this.datePickerContainer.current.contains(event.relatedTarget)) {
this.props.onFocus(event);
this.containerHasFocus = true;
}
}
handleOnCalendarButtonClick(event, onClick) {
if (this.onCalendarButtonClick) {
this.onCalendarButtonClick(event);
}
if (!this.isDefaultDateAcceptable && !this.validateDefaultDate()) {
this.dateValue = '';
this.handleChange(null, event);
} else if (onClick) {
// This onClick function is the onInputClick function coming from https://github.com/Hacker0x01/react-datepicker/blob/master/src/index.jsx#L326.
// It does not take any parameter so there is not a need to pass in the event.
onClick();
this.isDefaultDateAcceptable = true;
}
}
validateDefaultDate() {
return this.isDateWithinRange(this.state.selectedDate);
}
isDateWithinRange(date) {
let isAcceptable = true;
if (DateUtil.isDateOutOfRange(date, DateUtil.createSafeDate(this.props.minDate), DateUtil.createSafeDate(this.props.maxDate))) {
isAcceptable = false;
}
if (DateUtil.isDateExcluded(date, this.props.excludeDates)) {
isAcceptable = false;
}
return isAcceptable;
}
render() {
const {
disableButtonFocusOnClose,
inputAttributes,
excludeDates,
filterDate,
includeDates,
intl,
isIncomplete,
isInvalid,
maxDate,
minDate,
name,
onBlur,
onChange,
onChangeRaw,
onClickOutside,
onFocus,
onSelect,
required,
selectedDate,
value,
...customProps
} = this.props;
this.onCalendarButtonClick = customProps.onCalendarButtonClick;
delete customProps.onCalendarButtonClick;
const todayString = intl.formatMessage({ id: 'Terra.datePicker.today' });
const dateFormat = DateUtil.getFormatByLocale(intl.locale);
const placeholderText = intl.formatMessage({ id: 'Terra.datePicker.dateFormat' });
const excludeMomentDates = DateUtil.filterInvalidDates(excludeDates);
const includeMomentDates = DateUtil.filterInvalidDates(includeDates);
const maxMomentDate = DateUtil.createSafeDate(maxDate);
const minMomentDate = DateUtil.createSafeDate(minDate);
let formattedValue = DateUtil.strictFormatISODate(value, dateFormat);
if (!formattedValue) {
formattedValue = value;
}
let selectedDateInPicker;
// If using this as a controlled component.
if (value !== undefined) {
// If value is empty, let selectedDateInPicker be undefined as in clearing the value.
if (value !== '') {
selectedDateInPicker = DateUtil.createSafeDate(DateUtil.convertToISO8601(value, dateFormat));
// If value is not a valid date, keep the previous selected date in the picker.
if (selectedDateInPicker === undefined) {
selectedDateInPicker = this.state.selectedDate;
}
}
} else {
selectedDateInPicker = this.state.selectedDate;
}
const portalPicker = (
<ReactDatePicker
{...customProps}
selected={selectedDateInPicker}
value={formattedValue}
onBlur={this.handleBlur}
onChange={this.handleChange}
onChangeRaw={this.handleChangeRaw}
onClickOutside={this.handleOnClickOutside}
onFocus={this.handleOnInputFocus}
onSelect={this.handleOnSelect}
required={required}
customInput={(
<DateInput
onCalendarButtonClick={this.handleOnCalendarButtonClick}
inputAttributes={inputAttributes}
isIncomplete={isIncomplete}
isInvalid={isInvalid}
required={required}
shouldShowPicker={!this.isDefaultDateAcceptable && this.state.selectedDate === null}
onButtonFocus={this.handleFocus}
buttonRefCallback={(buttonRef) => { this.calendarButton = buttonRef; }}
/>
)}
excludeDates={excludeMomentDates}
filterDate={this.handleFilterDate}
includeDates={includeMomentDates}
maxDate={maxMomentDate}
minDate={minMomentDate}
todayButton={todayString}
withPortal
dateFormatCalendar=" "
dateFormat={dateFormat}
fixedHeight
locale={intl.locale}
placeholderText={placeholderText}
dropdownMode="select"
showMonthDropdown
showYearDropdown
preventOpenOnFocus
name={name}
allowSameDay
/>
);
const popupPicker = (
<ReactDatePicker
{...customProps}
selected={selectedDateInPicker}
value={formattedValue}
onBlur={this.handleBlur}
onChange={this.handleChange}
onChangeRaw={this.handleChangeRaw}
onClickOutside={this.handleOnClickOutside}
onFocus={this.handleOnInputFocus}
onSelect={this.handleOnSelect}
required={required}
customInput={(
<DateInput
onCalendarButtonClick={this.handleOnCalendarButtonClick}
inputAttributes={inputAttributes}
isIncomplete={isIncomplete}
isInvalid={isInvalid}
shouldShowPicker={!this.isDefaultDateAcceptable && this.state.selectedDate === null}
onButtonFocus={this.handleFocus}
buttonRefCallback={(buttonRef) => { this.calendarButton = buttonRef; }}
/>
)}
excludeDates={excludeMomentDates}
filterDate={this.handleFilterDate}
includeDates={includeMomentDates}
maxDate={maxMomentDate}
minDate={minMomentDate}
todayButton={todayString}
dateFormatCalendar=" "
dateFormat={dateFormat}
fixedHeight
locale={intl.locale}
placeholderText={placeholderText}
dropdownMode="select"
showMonthDropdown
showYearDropdown
preventOpenOnFocus
name={name}
allowSameDay
/>
);
return (
<div
className={cx('date-picker')}
ref={this.datePickerContainer}
>
<ResponsiveElement
responsiveTo="window"
tiny={portalPicker}
medium={popupPicker}
/>
</div>
);
}
}
DatePicker.propTypes = propTypes;
DatePicker.defaultProps = defaultProps;
export default injectIntl(DatePicker);