UNPKG

31.6 kBTypeScriptView 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
17import classNames from "classnames";
18import * as React from "react";
19import DayPicker, { CaptionElementProps, DayModifiers, DayPickerProps, NavbarElementProps } from "react-day-picker";
20
21import { AbstractPureComponent2, Boundary, DISPLAYNAME_PREFIX, Divider, Props } from "@blueprintjs/core";
22
23import * as DateClasses from "./common/classes";
24import { DateRange } from "./common/dateRange";
25import * as DateUtils from "./common/dateUtils";
26import * as Errors from "./common/errors";
27import { MonthAndYear } from "./common/monthAndYear";
28import { DatePickerCaption } from "./datePickerCaption";
29import {
30 combineModifiers,
31 DatePickerModifiers,
32 getDefaultMaxDate,
33 getDefaultMinDate,
34 HOVERED_RANGE_MODIFIER,
35 IDatePickerBaseProps,
36 SELECTED_RANGE_MODIFIER,
37} from "./datePickerCore";
38import { DatePickerNavbar } from "./datePickerNavbar";
39import { DateRangeSelectionStrategy } from "./dateRangeSelectionStrategy";
40import { DateRangeShortcut, Shortcuts } from "./shortcuts";
41import { TimePicker } from "./timePicker";
42
43// eslint-disable-next-line deprecation/deprecation
44export type DateRangePickerProps = IDateRangePickerProps;
45/** @deprecated use DateRangePickerProps */
46export interface IDateRangePickerProps extends IDatePickerBaseProps, Props {
47 /**
48 * Whether the start and end dates of the range can be the same day.
49 * If `true`, clicking a selected date will create a one-day range.
50 * If `false`, clicking a selected date will clear the selection.
51 *
52 * @default false
53 */
54 allowSingleDayRange?: boolean;
55
56 /**
57 * The date-range boundary that the next click should modify.
58 * This will be honored unless the next click would overlap the other boundary date.
59 * In that case, the two boundary dates will be auto-swapped to keep them in chronological order.
60 * If `undefined`, the picker will revert to its default selection behavior.
61 */
62 boundaryToModify?: Boundary;
63
64 /**
65 * Whether displayed months in the calendar are contiguous.
66 * If false, each side of the calendar can move independently to non-contiguous months.
67 *
68 * @default true
69 */
70 contiguousCalendarMonths?: boolean;
71
72 /**
73 * Initial `DateRange` the calendar will display as selected.
74 * This should not be set if `value` is set.
75 */
76 defaultValue?: DateRange;
77
78 /**
79 * Called when the user selects a day.
80 * If no days are selected, it will pass `[null, null]`.
81 * If a start date is selected but not an end date, it will pass `[selectedDate, null]`.
82 * If both a start and end date are selected, it will pass `[startDate, endDate]`.
83 */
84 onChange?: (selectedDates: DateRange) => void;
85
86 /**
87 * Called when the user changes the hovered date range, either from mouseenter or mouseleave.
88 * When triggered from mouseenter, it will pass the date range that would result from next click.
89 * When triggered from mouseleave, it will pass `undefined`.
90 */
91 onHoverChange?: (hoveredDates: DateRange, hoveredDay: Date, hoveredBoundary: Boundary) => void;
92
93 /**
94 * Called when the `shortcuts` props is enabled and the user changes the shortcut.
95 */
96 onShortcutChange?: (shortcut: DateRangeShortcut, index: number) => void;
97
98 /**
99 * Whether shortcuts to quickly select a range of dates are displayed or not.
100 * If `true`, preset shortcuts will be displayed.
101 * If `false`, no shortcuts will be displayed.
102 * If an array is provided, the custom shortcuts will be displayed.
103 *
104 * @default true
105 */
106 shortcuts?: boolean | DateRangeShortcut[];
107
108 /**
109 * The currently selected shortcut.
110 * If this prop is provided, the component acts in a controlled manner.
111 */
112 selectedShortcutIndex?: number;
113
114 /**
115 * Whether to show only a single month calendar.
116 *
117 * @default false
118 */
119 singleMonthOnly?: boolean;
120
121 /**
122 * The currently selected `DateRange`.
123 * If this prop is provided, the component acts in a controlled manner.
124 */
125 value?: DateRange;
126}
127
128// leftView and rightView controls the DayPicker displayed month
129export interface IDateRangePickerState {
130 hoverValue?: DateRange;
131 leftView?: MonthAndYear;
132 rightView?: MonthAndYear;
133 value?: DateRange;
134 time?: DateRange;
135 selectedShortcutIndex?: number;
136}
137
138export class DateRangePicker extends AbstractPureComponent2<DateRangePickerProps, IDateRangePickerState> {
139 public static defaultProps: DateRangePickerProps = {
140 allowSingleDayRange: false,
141 contiguousCalendarMonths: true,
142 dayPickerProps: {},
143 maxDate: getDefaultMaxDate(),
144 minDate: getDefaultMinDate(),
145 reverseMonthAndYearMenus: false,
146 shortcuts: true,
147 singleMonthOnly: false,
148 timePickerProps: {},
149 };
150
151 public static displayName = `${DISPLAYNAME_PREFIX}.DateRangePicker`;
152
153 // these will get merged with the user's own
154 private modifiers: DatePickerModifiers = {
155 [SELECTED_RANGE_MODIFIER]: day => {
156 const { value } = this.state;
157 return value[0] != null && value[1] != null && DateUtils.isDayInRange(day, value, true);
158 },
159 [`${SELECTED_RANGE_MODIFIER}-start`]: day => DateUtils.areSameDay(this.state.value[0], day),
160 [`${SELECTED_RANGE_MODIFIER}-end`]: day => DateUtils.areSameDay(this.state.value[1], day),
161
162 [HOVERED_RANGE_MODIFIER]: day => {
163 const {
164 hoverValue,
165 value: [selectedStart, selectedEnd],
166 } = this.state;
167 if (selectedStart == null && selectedEnd == null) {
168 return false;
169 }
170 if (hoverValue == null || hoverValue[0] == null || hoverValue[1] == null) {
171 return false;
172 }
173 return DateUtils.isDayInRange(day, hoverValue, true);
174 },
175 [`${HOVERED_RANGE_MODIFIER}-start`]: day => {
176 const { hoverValue } = this.state;
177 if (hoverValue == null || hoverValue[0] == null) {
178 return false;
179 }
180 return DateUtils.areSameDay(hoverValue[0], day);
181 },
182 [`${HOVERED_RANGE_MODIFIER}-end`]: day => {
183 const { hoverValue } = this.state;
184 if (hoverValue == null || hoverValue[1] == null) {
185 return false;
186 }
187 return DateUtils.areSameDay(hoverValue[1], day);
188 },
189 };
190
191 public constructor(props: DateRangePickerProps, context?: any) {
192 super(props, context);
193 const value = getInitialValue(props);
194 const time: DateRange = value;
195 const initialMonth = getInitialMonth(props, value);
196
197 // if the initial month is the last month of the picker's
198 // allowable range, the react-day-picker library will show
199 // the max month on the left and the *min* month on the right.
200 // subtracting one avoids that weird, wraparound state (#289).
201 const initialMonthEqualsMinMonth = DateUtils.areSameMonth(initialMonth, props.minDate);
202 const initalMonthEqualsMaxMonth = DateUtils.areSameMonth(initialMonth, props.maxDate);
203 if (!props.singleMonthOnly && !initialMonthEqualsMinMonth && initalMonthEqualsMaxMonth) {
204 initialMonth.setMonth(initialMonth.getMonth() - 1);
205 }
206
207 // show the selected end date's encompassing month in the right view if
208 // the calendars don't have to be contiguous.
209 // if left view and right view months are the same, show next month in the right view.
210 const leftView = MonthAndYear.fromDate(initialMonth);
211 const rightDate = value[1];
212 const rightView =
213 !props.contiguousCalendarMonths && rightDate != null && !DateUtils.areSameMonth(initialMonth, rightDate)
214 ? MonthAndYear.fromDate(rightDate)
215 : leftView.getNextMonth();
216 this.state = {
217 hoverValue: [null, null],
218 leftView,
219 rightView,
220 selectedShortcutIndex:
221 this.props.selectedShortcutIndex !== undefined ? this.props.selectedShortcutIndex : -1,
222 time,
223 value,
224 };
225 }
226
227 public render() {
228 const { className, contiguousCalendarMonths, singleMonthOnly } = this.props;
229 const isShowingOneMonth = singleMonthOnly || DateUtils.areSameMonth(this.props.minDate, this.props.maxDate);
230
231 const classes = classNames(DateClasses.DATEPICKER, DateClasses.DATERANGEPICKER, className, {
232 [DateClasses.DATERANGEPICKER_CONTIGUOUS]: contiguousCalendarMonths,
233 [DateClasses.DATERANGEPICKER_SINGLE_MONTH]: isShowingOneMonth,
234 });
235
236 // use the left DayPicker when we only need one
237 return (
238 <div className={classes}>
239 {this.maybeRenderShortcuts()}
240 <div>
241 {this.renderCalendars(isShowingOneMonth)}
242 {this.maybeRenderTimePickers()}
243 </div>
244 </div>
245 );
246 }
247
248 public componentDidUpdate(prevProps: DateRangePickerProps, prevState: IDateRangePickerState) {
249 super.componentDidUpdate(prevProps, prevState);
250
251 if (
252 !DateUtils.areRangesEqual(prevProps.value, this.props.value) ||
253 prevProps.contiguousCalendarMonths !== this.props.contiguousCalendarMonths
254 ) {
255 const nextState = getStateChange(
256 prevProps.value,
257 this.props.value,
258 this.state,
259 prevProps.contiguousCalendarMonths,
260 );
261 this.setState(nextState);
262 }
263
264 if (this.props.selectedShortcutIndex !== prevProps.selectedShortcutIndex) {
265 this.setState({ selectedShortcutIndex: this.props.selectedShortcutIndex });
266 }
267 }
268
269 protected validateProps(props: DateRangePickerProps) {
270 const { defaultValue, initialMonth, maxDate, minDate, boundaryToModify, value } = props;
271 const dateRange: DateRange = [minDate, maxDate];
272
273 if (defaultValue != null && !DateUtils.isDayRangeInRange(defaultValue, dateRange)) {
274 console.error(Errors.DATERANGEPICKER_DEFAULT_VALUE_INVALID);
275 }
276
277 if (initialMonth != null && !DateUtils.isMonthInRange(initialMonth, dateRange)) {
278 console.error(Errors.DATERANGEPICKER_INITIAL_MONTH_INVALID);
279 }
280
281 if (maxDate != null && minDate != null && maxDate < minDate && !DateUtils.areSameDay(maxDate, minDate)) {
282 console.error(Errors.DATERANGEPICKER_MAX_DATE_INVALID);
283 }
284
285 if (value != null && !DateUtils.isDayRangeInRange(value, dateRange)) {
286 console.error(Errors.DATERANGEPICKER_VALUE_INVALID);
287 }
288
289 if (boundaryToModify != null && boundaryToModify !== Boundary.START && boundaryToModify !== Boundary.END) {
290 console.error(Errors.DATERANGEPICKER_PREFERRED_BOUNDARY_TO_MODIFY_INVALID);
291 }
292 }
293
294 private shouldHighlightCurrentDay = (date: Date) => {
295 const { highlightCurrentDay } = this.props;
296
297 return highlightCurrentDay && DateUtils.isToday(date);
298 };
299
300 private getDateRangePickerModifiers = () => {
301 const { modifiers } = this.props;
302
303 return combineModifiers(this.modifiers, {
304 isToday: this.shouldHighlightCurrentDay,
305 ...modifiers,
306 });
307 };
308
309 private renderDay = (day: Date) => {
310 const date = day.getDate();
311
312 return <div className={DateClasses.DATEPICKER_DAY_WRAPPER}>{date}</div>;
313 };
314
315 private disabledDays = (day: Date) => !DateUtils.isDayInRange(day, [this.props.minDate, this.props.maxDate]);
316
317 private getDisabledDaysModifier = () => {
318 const {
319 dayPickerProps: { disabledDays },
320 } = this.props;
321
322 return disabledDays instanceof Array ? [this.disabledDays, ...disabledDays] : [this.disabledDays, disabledDays];
323 };
324
325 private maybeRenderShortcuts() {
326 const { shortcuts } = this.props;
327 if (shortcuts == null || shortcuts === false) {
328 return null;
329 }
330
331 const { selectedShortcutIndex } = this.state;
332 const { allowSingleDayRange, maxDate, minDate, timePrecision } = this.props;
333 return [
334 <Shortcuts
335 key="shortcuts"
336 {...{
337 allowSingleDayRange,
338 maxDate,
339 minDate,
340 selectedShortcutIndex,
341 shortcuts,
342 timePrecision,
343 }}
344 onShortcutClick={this.handleShortcutClick}
345 />,
346 <Divider key="div" />,
347 ];
348 }
349
350 private maybeRenderTimePickers() {
351 const { timePrecision, timePickerProps } = this.props;
352 if (timePrecision == null && timePickerProps === DateRangePicker.defaultProps.timePickerProps) {
353 return null;
354 }
355 return (
356 <div className={DateClasses.DATERANGEPICKER_TIMEPICKERS}>
357 <TimePicker
358 precision={timePrecision}
359 {...timePickerProps}
360 onChange={this.handleTimeChangeLeftCalendar}
361 value={this.state.time[0]}
362 />
363 <TimePicker
364 precision={timePrecision}
365 {...timePickerProps}
366 onChange={this.handleTimeChangeRightCalendar}
367 value={this.state.time[1]}
368 />
369 </div>
370 );
371 }
372
373 private handleTimeChange = (newTime: Date, dateIndex: number) => {
374 this.props.timePickerProps?.onChange?.(newTime);
375
376 const { value, time } = this.state;
377 const newValue = DateUtils.getDateTime(
378 value[dateIndex] != null ? DateUtils.clone(value[dateIndex]) : new Date(),
379 newTime,
380 );
381 const newDateRange: DateRange = [value[0], value[1]];
382 newDateRange[dateIndex] = newValue;
383 const newTimeRange: DateRange = [time[0], time[1]];
384 newTimeRange[dateIndex] = newTime;
385 this.props.onChange?.(newDateRange);
386 this.setState({ value: newDateRange, time: newTimeRange });
387 };
388
389 private handleTimeChangeLeftCalendar = (time: Date) => {
390 this.handleTimeChange(time, 0);
391 };
392
393 private handleTimeChangeRightCalendar = (time: Date) => {
394 this.handleTimeChange(time, 1);
395 };
396
397 private renderCalendars(isShowingOneMonth: boolean) {
398 const { dayPickerProps, locale, localeUtils, maxDate, minDate } = this.props;
399 const dayPickerBaseProps: DayPickerProps = {
400 locale,
401 localeUtils,
402 modifiers: this.getDateRangePickerModifiers(),
403 showOutsideDays: true,
404 ...dayPickerProps,
405 disabledDays: this.getDisabledDaysModifier(),
406 onDayClick: this.handleDayClick,
407 onDayMouseEnter: this.handleDayMouseEnter,
408 onDayMouseLeave: this.handleDayMouseLeave,
409 selectedDays: this.state.value,
410 };
411
412 if (isShowingOneMonth) {
413 return (
414 <DayPicker
415 {...dayPickerBaseProps}
416 captionElement={this.renderSingleCaption}
417 navbarElement={this.renderSingleNavbar}
418 fromMonth={minDate}
419 month={this.state.leftView.getFullDate()}
420 numberOfMonths={1}
421 onMonthChange={this.handleLeftMonthChange}
422 toMonth={maxDate}
423 renderDay={dayPickerProps?.renderDay ?? this.renderDay}
424 />
425 );
426 } else {
427 return [
428 <DayPicker
429 key="left"
430 {...dayPickerBaseProps}
431 canChangeMonth={true}
432 captionElement={this.renderLeftCaption}
433 navbarElement={this.renderLeftNavbar}
434 fromMonth={minDate}
435 month={this.state.leftView.getFullDate()}
436 numberOfMonths={1}
437 onMonthChange={this.handleLeftMonthChange}
438 toMonth={DateUtils.getDatePreviousMonth(maxDate)}
439 renderDay={dayPickerProps?.renderDay ?? this.renderDay}
440 />,
441 <DayPicker
442 key="right"
443 {...dayPickerBaseProps}
444 canChangeMonth={true}
445 captionElement={this.renderRightCaption}
446 navbarElement={this.renderRightNavbar}
447 fromMonth={DateUtils.getDateNextMonth(minDate)}
448 month={this.state.rightView.getFullDate()}
449 numberOfMonths={1}
450 onMonthChange={this.handleRightMonthChange}
451 toMonth={maxDate}
452 renderDay={dayPickerProps?.renderDay ?? this.renderDay}
453 />,
454 ];
455 }
456 }
457
458 private renderSingleNavbar = (navbarProps: NavbarElementProps) => (
459 <DatePickerNavbar {...navbarProps} maxDate={this.props.maxDate} minDate={this.props.minDate} />
460 );
461
462 private renderLeftNavbar = (navbarProps: NavbarElementProps) => (
463 <DatePickerNavbar
464 {...navbarProps}
465 hideRightNavButton={this.props.contiguousCalendarMonths}
466 maxDate={this.props.maxDate}
467 minDate={this.props.minDate}
468 />
469 );
470
471 private renderRightNavbar = (navbarProps: NavbarElementProps) => (
472 <DatePickerNavbar
473 {...navbarProps}
474 hideLeftNavButton={this.props.contiguousCalendarMonths}
475 maxDate={this.props.maxDate}
476 minDate={this.props.minDate}
477 />
478 );
479
480 private renderSingleCaption = (captionProps: CaptionElementProps) => (
481 <DatePickerCaption
482 {...captionProps}
483 maxDate={this.props.maxDate}
484 minDate={this.props.minDate}
485 onMonthChange={this.handleLeftMonthSelectChange}
486 onYearChange={this.handleLeftYearSelectChange}
487 reverseMonthAndYearMenus={this.props.reverseMonthAndYearMenus}
488 />
489 );
490
491 private renderLeftCaption = (captionProps: CaptionElementProps) => (
492 <DatePickerCaption
493 {...captionProps}
494 maxDate={DateUtils.getDatePreviousMonth(this.props.maxDate)}
495 minDate={this.props.minDate}
496 onMonthChange={this.handleLeftMonthSelectChange}
497 onYearChange={this.handleLeftYearSelectChange}
498 reverseMonthAndYearMenus={this.props.reverseMonthAndYearMenus}
499 />
500 );
501
502 private renderRightCaption = (captionProps: CaptionElementProps) => (
503 <DatePickerCaption
504 {...captionProps}
505 maxDate={this.props.maxDate}
506 minDate={DateUtils.getDateNextMonth(this.props.minDate)}
507 onMonthChange={this.handleRightMonthSelectChange}
508 onYearChange={this.handleRightYearSelectChange}
509 reverseMonthAndYearMenus={this.props.reverseMonthAndYearMenus}
510 />
511 );
512
513 private handleDayMouseEnter = (day: Date, modifiers: DayModifiers, e: React.MouseEvent<HTMLDivElement>) => {
514 this.props.dayPickerProps.onDayMouseEnter?.(day, modifiers, e);
515
516 if (modifiers.disabled) {
517 return;
518 }
519 const { dateRange, boundary } = DateRangeSelectionStrategy.getNextState(
520 this.state.value,
521 day,
522 this.props.allowSingleDayRange,
523 this.props.boundaryToModify,
524 );
525 this.setState({ hoverValue: dateRange });
526 this.props.onHoverChange?.(dateRange, day, boundary);
527 };
528
529 private handleDayMouseLeave = (day: Date, modifiers: DayModifiers, e: React.MouseEvent<HTMLDivElement>) => {
530 this.props.dayPickerProps.onDayMouseLeave?.(day, modifiers, e);
531 if (modifiers.disabled) {
532 return;
533 }
534 this.setState({ hoverValue: undefined });
535 this.props.onHoverChange?.(undefined, day, undefined);
536 };
537
538 private handleDayClick = (day: Date, modifiers: DayModifiers, e: React.MouseEvent<HTMLDivElement>) => {
539 this.props.dayPickerProps.onDayClick?.(day, modifiers, e);
540
541 if (modifiers.disabled) {
542 // rerender base component to get around bug where you can navigate past bounds by clicking days
543 this.forceUpdate();
544 return;
545 }
546
547 const nextValue = DateRangeSelectionStrategy.getNextState(
548 this.state.value,
549 day,
550 this.props.allowSingleDayRange,
551 this.props.boundaryToModify,
552 ).dateRange;
553
554 // update the hovered date range after click to show the newly selected
555 // state, at leasts until the mouse moves again
556 this.handleDayMouseEnter(day, modifiers, e);
557
558 this.handleNextState(nextValue);
559 };
560
561 private handleShortcutClick = (shortcut: DateRangeShortcut, selectedShortcutIndex: number) => {
562 const { onChange, contiguousCalendarMonths, onShortcutChange } = this.props;
563 const { dateRange, includeTime } = shortcut;
564 if (includeTime) {
565 const newDateRange: DateRange = [dateRange[0], dateRange[1]];
566 const newTimeRange: DateRange = [dateRange[0], dateRange[1]];
567 const nextState = getStateChange(this.state.value, dateRange, this.state, contiguousCalendarMonths);
568 this.setState({ ...nextState, time: newTimeRange });
569 onChange?.(newDateRange);
570 } else {
571 this.handleNextState(dateRange);
572 }
573
574 if (this.props.selectedShortcutIndex === undefined) {
575 this.setState({ selectedShortcutIndex });
576 }
577
578 onShortcutChange?.(shortcut, selectedShortcutIndex);
579 };
580
581 private handleNextState = (nextValue: DateRange) => {
582 const { value } = this.state;
583 nextValue[0] = DateUtils.getDateTime(nextValue[0], this.state.time[0]);
584 nextValue[1] = DateUtils.getDateTime(nextValue[1], this.state.time[1]);
585
586 const nextState = getStateChange(value, nextValue, this.state, this.props.contiguousCalendarMonths);
587
588 if (this.props.value == null) {
589 this.setState(nextState);
590 }
591 this.props.onChange?.(nextValue);
592 };
593
594 private handleLeftMonthChange = (newDate: Date) => {
595 const leftView = MonthAndYear.fromDate(newDate);
596 this.props.dayPickerProps.onMonthChange?.(leftView.getFullDate());
597 this.updateLeftView(leftView);
598 };
599
600 private handleRightMonthChange = (newDate: Date) => {
601 const rightView = MonthAndYear.fromDate(newDate);
602 this.props.dayPickerProps.onMonthChange?.(rightView.getFullDate());
603 this.updateRightView(rightView);
604 };
605
606 private handleLeftMonthSelectChange = (leftMonth: number) => {
607 const leftView = new MonthAndYear(leftMonth, this.state.leftView.getYear());
608 this.props.dayPickerProps.onMonthChange?.(leftView.getFullDate());
609 this.updateLeftView(leftView);
610 };
611
612 private handleRightMonthSelectChange = (rightMonth: number) => {
613 const rightView = new MonthAndYear(rightMonth, this.state.rightView.getYear());
614 this.props.dayPickerProps.onMonthChange?.(rightView.getFullDate());
615 this.updateRightView(rightView);
616 };
617
618 private updateLeftView(leftView: MonthAndYear) {
619 let rightView = this.state.rightView.clone();
620 if (!leftView.isBefore(rightView) || this.props.contiguousCalendarMonths) {
621 rightView = leftView.getNextMonth();
622 }
623 this.setViews(leftView, rightView);
624 }
625
626 private updateRightView(rightView: MonthAndYear) {
627 let leftView = this.state.leftView.clone();
628 if (!rightView.isAfter(leftView) || this.props.contiguousCalendarMonths) {
629 leftView = rightView.getPreviousMonth();
630 }
631 this.setViews(leftView, rightView);
632 }
633
634 /*
635 * The min / max months are offset by one because we are showing two months.
636 * We do a comparison check to see if
637 * a) the proposed [Month, Year] change throws the two calendars out of order
638 * b) the proposed [Month, Year] goes beyond the min / max months
639 * and rectify appropriately.
640 */
641 private handleLeftYearSelectChange = (leftDisplayYear: number) => {
642 let leftView = new MonthAndYear(this.state.leftView.getMonth(), leftDisplayYear);
643 this.props.dayPickerProps.onMonthChange?.(leftView.getFullDate());
644 const { minDate, maxDate } = this.props;
645 const adjustedMaxDate = DateUtils.getDatePreviousMonth(maxDate);
646
647 const minMonthAndYear = new MonthAndYear(minDate.getMonth(), minDate.getFullYear());
648 const maxMonthAndYear = new MonthAndYear(adjustedMaxDate.getMonth(), adjustedMaxDate.getFullYear());
649
650 if (leftView.isBefore(minMonthAndYear)) {
651 leftView = minMonthAndYear;
652 } else if (leftView.isAfter(maxMonthAndYear)) {
653 leftView = maxMonthAndYear;
654 }
655
656 let rightView = this.state.rightView.clone();
657 if (!leftView.isBefore(rightView) || this.props.contiguousCalendarMonths) {
658 rightView = leftView.getNextMonth();
659 }
660
661 this.setViews(leftView, rightView);
662 };
663
664 private handleRightYearSelectChange = (rightDisplayYear: number) => {
665 let rightView = new MonthAndYear(this.state.rightView.getMonth(), rightDisplayYear);
666 this.props.dayPickerProps.onMonthChange?.(rightView.getFullDate());
667 const { minDate, maxDate } = this.props;
668 const adjustedMinDate = DateUtils.getDateNextMonth(minDate);
669
670 const minMonthAndYear = MonthAndYear.fromDate(adjustedMinDate);
671 const maxMonthAndYear = MonthAndYear.fromDate(maxDate);
672
673 if (rightView.isBefore(minMonthAndYear)) {
674 rightView = minMonthAndYear;
675 } else if (rightView.isAfter(maxMonthAndYear)) {
676 rightView = maxMonthAndYear;
677 }
678
679 let leftView = this.state.leftView.clone();
680 if (!rightView.isAfter(leftView) || this.props.contiguousCalendarMonths) {
681 leftView = rightView.getPreviousMonth();
682 }
683
684 this.setViews(leftView, rightView);
685 };
686
687 private setViews(leftView: MonthAndYear, rightView: MonthAndYear) {
688 this.setState({ leftView, rightView });
689 }
690}
691
692function getStateChange(
693 value: DateRange,
694 nextValue: DateRange,
695 state: IDateRangePickerState,
696 contiguousCalendarMonths: boolean,
697): IDateRangePickerState {
698 if (value != null && nextValue == null) {
699 return { value: [null, null] };
700 } else if (nextValue != null) {
701 let leftView = state.leftView.clone();
702 let rightView = state.rightView.clone();
703
704 const nextValueStartView = MonthAndYear.fromDate(nextValue[0]);
705 const nextValueEndView = MonthAndYear.fromDate(nextValue[1]);
706
707 // Only end date selected.
708 // If the newly selected end date isn't in either of the displayed months, then
709 // - set the right DayPicker to the month of the selected end date
710 // - ensure the left DayPicker is before the right, changing if needed
711 if (nextValueStartView == null && nextValueEndView != null) {
712 if (!nextValueEndView.isSame(leftView) && !nextValueEndView.isSame(rightView)) {
713 rightView = nextValueEndView;
714 if (!leftView.isBefore(rightView)) {
715 leftView = rightView.getPreviousMonth();
716 }
717 }
718 } else if (nextValueStartView != null && nextValueEndView == null) {
719 // Only start date selected.
720 // If the newly selected start date isn't in either of the displayed months, then
721 // - set the left DayPicker to the month of the selected start date
722 // - ensure the right DayPicker is before the left, changing if needed
723 if (!nextValueStartView.isSame(leftView) && !nextValueStartView.isSame(rightView)) {
724 leftView = nextValueStartView;
725 if (!rightView.isAfter(leftView)) {
726 rightView = leftView.getNextMonth();
727 }
728 }
729 } else if (nextValueStartView != null && nextValueEndView != null) {
730 // Both start and end date months are identical
731 // If the selected month isn't in either of the displayed months, then
732 // - set the left DayPicker to be the selected month
733 // - set the right DayPicker to +1
734 if (nextValueStartView.isSame(nextValueEndView)) {
735 if (leftView.isSame(nextValueStartView) || rightView.isSame(nextValueStartView)) {
736 // do nothing
737 } else {
738 leftView = nextValueStartView;
739 rightView = nextValueStartView.getNextMonth();
740 }
741 } else {
742 // Different start and end date months, adjust display months.
743 if (!leftView.isSame(nextValueStartView)) {
744 leftView = nextValueStartView;
745 rightView = nextValueStartView.getNextMonth();
746 }
747 if (contiguousCalendarMonths === false && !rightView.isSame(nextValueEndView)) {
748 rightView = nextValueEndView;
749 }
750 }
751 }
752
753 return {
754 leftView,
755 rightView,
756 value: nextValue,
757 };
758 } else if (contiguousCalendarMonths === true) {
759 // contiguousCalendarMonths is toggled on.
760 // If the previous leftView and rightView are not contiguous, then set the right DayPicker to left + 1
761 if (!state.leftView.getNextMonth().isSameMonth(state.rightView)) {
762 const nextRightView = state.leftView.getNextMonth();
763 return { rightView: nextRightView };
764 }
765 }
766
767 return {};
768}
769
770function getInitialValue(props: DateRangePickerProps): DateRange | null {
771 if (props.value != null) {
772 return props.value;
773 }
774 if (props.defaultValue != null) {
775 return props.defaultValue;
776 }
777 return [null, null];
778}
779
780function getInitialMonth(props: DateRangePickerProps, value: DateRange): Date {
781 const today = new Date();
782 // != because we must have a real `Date` to begin the calendar on.
783 if (props.initialMonth != null) {
784 return props.initialMonth;
785 } else if (value[0] != null) {
786 return DateUtils.clone(value[0]);
787 } else if (value[1] != null) {
788 const month = DateUtils.clone(value[1]);
789 if (!DateUtils.areSameMonth(month, props.minDate)) {
790 month.setMonth(month.getMonth() - 1);
791 }
792 return month;
793 } else if (DateUtils.isDayInRange(today, [props.minDate, props.maxDate])) {
794 return today;
795 } else {
796 return DateUtils.getDateBetween([props.minDate, props.maxDate]);
797 }
798}