UNPKG

39.1 kBJSXView Raw
1import React from 'react';
2import PropTypes from 'prop-types';
3import { forbidExtraProps, mutuallyExclusiveProps, nonNegativeInteger } from 'airbnb-prop-types';
4import { withStyles, withStylesPropTypes } from 'react-with-styles';
5
6import moment from 'moment';
7import throttle from 'lodash/throttle';
8import isTouchDevice from 'is-touch-device';
9import OutsideClickHandler from 'react-outside-click-handler';
10
11import { DayPickerPhrases } from '../defaultPhrases';
12import getPhrasePropTypes from '../utils/getPhrasePropTypes';
13import noflip from '../utils/noflip';
14
15import CalendarMonthGrid from './CalendarMonthGrid';
16import DayPickerNavigation from './DayPickerNavigation';
17import DayPickerKeyboardShortcuts, {
18 TOP_LEFT,
19 TOP_RIGHT,
20 BOTTOM_RIGHT,
21} from './DayPickerKeyboardShortcuts';
22
23import getNumberOfCalendarMonthWeeks from '../utils/getNumberOfCalendarMonthWeeks';
24import getCalendarMonthWidth from '../utils/getCalendarMonthWidth';
25import calculateDimension from '../utils/calculateDimension';
26import getActiveElement from '../utils/getActiveElement';
27import isDayVisible from '../utils/isDayVisible';
28
29import ModifiersShape from '../shapes/ModifiersShape';
30import ScrollableOrientationShape from '../shapes/ScrollableOrientationShape';
31import DayOfWeekShape from '../shapes/DayOfWeekShape';
32import CalendarInfoPositionShape from '../shapes/CalendarInfoPositionShape';
33
34import {
35 HORIZONTAL_ORIENTATION,
36 VERTICAL_ORIENTATION,
37 VERTICAL_SCROLLABLE,
38 DAY_SIZE,
39 INFO_POSITION_TOP,
40 INFO_POSITION_BOTTOM,
41 INFO_POSITION_BEFORE,
42 INFO_POSITION_AFTER,
43 MODIFIER_KEY_NAMES,
44} from '../constants';
45
46const MONTH_PADDING = 23;
47const PREV_TRANSITION = 'prev';
48const NEXT_TRANSITION = 'next';
49const MONTH_SELECTION_TRANSITION = 'month_selection';
50const YEAR_SELECTION_TRANSITION = 'year_selection';
51
52const propTypes = forbidExtraProps({
53 ...withStylesPropTypes,
54
55 // calendar presentation props
56 enableOutsideDays: PropTypes.bool,
57 numberOfMonths: PropTypes.number,
58 orientation: ScrollableOrientationShape,
59 withPortal: PropTypes.bool,
60 onOutsideClick: PropTypes.func,
61 hidden: PropTypes.bool,
62 initialVisibleMonth: PropTypes.func,
63 firstDayOfWeek: DayOfWeekShape,
64 renderCalendarInfo: PropTypes.func,
65 calendarInfoPosition: CalendarInfoPositionShape,
66 hideKeyboardShortcutsPanel: PropTypes.bool,
67 daySize: nonNegativeInteger,
68 isRTL: PropTypes.bool,
69 verticalHeight: nonNegativeInteger,
70 noBorder: PropTypes.bool,
71 transitionDuration: nonNegativeInteger,
72 verticalBorderSpacing: nonNegativeInteger,
73 horizontalMonthPadding: nonNegativeInteger,
74 renderKeyboardShortcutsButton: PropTypes.func,
75
76 // navigation props
77 disablePrev: PropTypes.bool,
78 disableNext: PropTypes.bool,
79 navPrev: PropTypes.node,
80 navNext: PropTypes.node,
81 noNavButtons: PropTypes.bool,
82 onPrevMonthClick: PropTypes.func,
83 onNextMonthClick: PropTypes.func,
84 onMonthChange: PropTypes.func,
85 onYearChange: PropTypes.func,
86 onMultiplyScrollableMonths: PropTypes.func, // VERTICAL_SCROLLABLE daypickers only
87
88 // month props
89 renderMonthText: mutuallyExclusiveProps(PropTypes.func, 'renderMonthText', 'renderMonthElement'),
90 renderMonthElement: mutuallyExclusiveProps(PropTypes.func, 'renderMonthText', 'renderMonthElement'),
91 renderWeekHeaderElement: PropTypes.func,
92
93 // day props
94 modifiers: PropTypes.objectOf(PropTypes.objectOf(ModifiersShape)),
95 renderCalendarDay: PropTypes.func,
96 renderDayContents: PropTypes.func,
97 onDayClick: PropTypes.func,
98 onDayMouseEnter: PropTypes.func,
99 onDayMouseLeave: PropTypes.func,
100
101 // accessibility props
102 isFocused: PropTypes.bool,
103 getFirstFocusableDay: PropTypes.func,
104 onBlur: PropTypes.func,
105 showKeyboardShortcuts: PropTypes.bool,
106 onTab: PropTypes.func,
107 onShiftTab: PropTypes.func,
108
109 // internationalization
110 monthFormat: PropTypes.string,
111 weekDayFormat: PropTypes.string,
112 phrases: PropTypes.shape(getPhrasePropTypes(DayPickerPhrases)),
113 dayAriaLabelFormat: PropTypes.string,
114});
115
116export const defaultProps = {
117 // calendar presentation props
118 enableOutsideDays: false,
119 numberOfMonths: 2,
120 orientation: HORIZONTAL_ORIENTATION,
121 withPortal: false,
122 onOutsideClick() {},
123 hidden: false,
124 initialVisibleMonth: () => moment(),
125 firstDayOfWeek: null,
126 renderCalendarInfo: null,
127 calendarInfoPosition: INFO_POSITION_BOTTOM,
128 hideKeyboardShortcutsPanel: false,
129 daySize: DAY_SIZE,
130 isRTL: false,
131 verticalHeight: null,
132 noBorder: false,
133 transitionDuration: undefined,
134 verticalBorderSpacing: undefined,
135 horizontalMonthPadding: 13,
136 renderKeyboardShortcutsButton: undefined,
137
138 // navigation props
139 disablePrev: false,
140 disableNext: false,
141 navPrev: null,
142 navNext: null,
143 noNavButtons: false,
144 onPrevMonthClick() {},
145 onNextMonthClick() {},
146 onMonthChange() {},
147 onYearChange() {},
148 onMultiplyScrollableMonths() {},
149
150 // month props
151 renderMonthText: null,
152 renderMonthElement: null,
153 renderWeekHeaderElement: null,
154
155 // day props
156 modifiers: {},
157 renderCalendarDay: undefined,
158 renderDayContents: null,
159 onDayClick() {},
160 onDayMouseEnter() {},
161 onDayMouseLeave() {},
162
163 // accessibility props
164 isFocused: false,
165 getFirstFocusableDay: null,
166 onBlur() {},
167 showKeyboardShortcuts: false,
168 onTab() {},
169 onShiftTab() {},
170
171 // internationalization
172 monthFormat: 'MMMM YYYY',
173 weekDayFormat: 'dd',
174 phrases: DayPickerPhrases,
175 dayAriaLabelFormat: undefined,
176};
177
178class DayPicker extends React.PureComponent {
179 constructor(props) {
180 super(props);
181
182 const currentMonth = props.hidden ? moment() : props.initialVisibleMonth();
183
184 let focusedDate = currentMonth.clone().startOf('month');
185 if (props.getFirstFocusableDay) {
186 focusedDate = props.getFirstFocusableDay(currentMonth);
187 }
188
189 const { horizontalMonthPadding } = props;
190
191 const translationValue = props.isRTL && this.isHorizontal()
192 ? -getCalendarMonthWidth(props.daySize, horizontalMonthPadding)
193 : 0;
194
195 this.hasSetInitialVisibleMonth = !props.hidden;
196 this.state = {
197 currentMonth,
198 monthTransition: null,
199 translationValue,
200 scrollableMonthMultiple: 1,
201 calendarMonthWidth: getCalendarMonthWidth(props.daySize, horizontalMonthPadding),
202 focusedDate: (!props.hidden || props.isFocused) ? focusedDate : null,
203 nextFocusedDate: null,
204 showKeyboardShortcuts: props.showKeyboardShortcuts,
205 onKeyboardShortcutsPanelClose() {},
206 isTouchDevice: isTouchDevice(),
207 withMouseInteractions: true,
208 calendarInfoWidth: 0,
209 monthTitleHeight: null,
210 hasSetHeight: false,
211 };
212
213 this.setCalendarMonthWeeks(currentMonth);
214
215 this.calendarMonthGridHeight = 0;
216 this.setCalendarInfoWidthTimeout = null;
217 this.setCalendarMonthGridHeightTimeout = null;
218
219 this.onKeyDown = this.onKeyDown.bind(this);
220 this.throttledKeyDown = throttle(this.onFinalKeyDown, 200, { trailing: false });
221 this.onPrevMonthClick = this.onPrevMonthClick.bind(this);
222 this.onPrevMonthTransition = this.onPrevMonthTransition.bind(this);
223 this.onNextMonthClick = this.onNextMonthClick.bind(this);
224 this.onNextMonthTransition = this.onNextMonthTransition.bind(this);
225 this.onMonthChange = this.onMonthChange.bind(this);
226 this.onYearChange = this.onYearChange.bind(this);
227
228 this.multiplyScrollableMonths = this.multiplyScrollableMonths.bind(this);
229 this.updateStateAfterMonthTransition = this.updateStateAfterMonthTransition.bind(this);
230
231 this.openKeyboardShortcutsPanel = this.openKeyboardShortcutsPanel.bind(this);
232 this.closeKeyboardShortcutsPanel = this.closeKeyboardShortcutsPanel.bind(this);
233
234 this.setCalendarInfoRef = this.setCalendarInfoRef.bind(this);
235 this.setContainerRef = this.setContainerRef.bind(this);
236 this.setTransitionContainerRef = this.setTransitionContainerRef.bind(this);
237 this.setMonthTitleHeight = this.setMonthTitleHeight.bind(this);
238 }
239
240 componentDidMount() {
241 const { currentMonth } = this.state;
242 if (this.calendarInfo) {
243 this.setState({
244 isTouchDevice: isTouchDevice(),
245 calendarInfoWidth: calculateDimension(this.calendarInfo, 'width', true, true),
246 });
247 } else {
248 this.setState({ isTouchDevice: isTouchDevice() });
249 }
250
251 this.setCalendarMonthWeeks(currentMonth);
252 }
253
254 componentWillReceiveProps(nextProps) {
255 const {
256 hidden,
257 isFocused,
258 showKeyboardShortcuts,
259 onBlur,
260 renderMonthText,
261 horizontalMonthPadding,
262 } = nextProps;
263 const { currentMonth } = this.state;
264
265 if (!hidden) {
266 if (!this.hasSetInitialVisibleMonth) {
267 this.hasSetInitialVisibleMonth = true;
268 this.setState({
269 currentMonth: nextProps.initialVisibleMonth(),
270 });
271 }
272 }
273
274 const {
275 daySize,
276 isFocused: prevIsFocused,
277 renderMonthText: prevRenderMonthText,
278 } = this.props;
279
280 if (nextProps.daySize !== daySize) {
281 this.setState({
282 calendarMonthWidth: getCalendarMonthWidth(
283 nextProps.daySize,
284 horizontalMonthPadding,
285 ),
286 });
287 }
288
289 if (isFocused !== prevIsFocused) {
290 if (isFocused) {
291 const focusedDate = this.getFocusedDay(currentMonth);
292
293 let { onKeyboardShortcutsPanelClose } = this.state;
294 if (nextProps.showKeyboardShortcuts) {
295 // the ? shortcut came from the input and we should return input there once it is close
296 onKeyboardShortcutsPanelClose = onBlur;
297 }
298
299 this.setState({
300 showKeyboardShortcuts,
301 onKeyboardShortcutsPanelClose,
302 focusedDate,
303 withMouseInteractions: false,
304 });
305 } else {
306 this.setState({ focusedDate: null });
307 }
308 }
309
310 if (renderMonthText !== prevRenderMonthText) {
311 this.setState({
312 monthTitleHeight: null,
313 });
314 }
315 }
316
317 componentWillUpdate() {
318 const { transitionDuration } = this.props;
319
320 // Calculating the dimensions trigger a DOM repaint which
321 // breaks the CSS transition.
322 // The setTimeout will wait until the transition ends.
323 if (this.calendarInfo) {
324 this.setCalendarInfoWidthTimeout = setTimeout(() => {
325 const { calendarInfoWidth } = this.state;
326 const calendarInfoPanelWidth = calculateDimension(this.calendarInfo, 'width', true, true);
327 if (calendarInfoWidth !== calendarInfoPanelWidth) {
328 this.setState({
329 calendarInfoWidth: calendarInfoPanelWidth,
330 });
331 }
332 }, transitionDuration);
333 }
334 }
335
336 componentDidUpdate(prevProps) {
337 const {
338 orientation, daySize, isFocused, numberOfMonths,
339 } = this.props;
340 const { focusedDate, monthTitleHeight } = this.state;
341
342 if (
343 this.isHorizontal()
344 && (orientation !== prevProps.orientation || daySize !== prevProps.daySize)
345 ) {
346 const visibleCalendarWeeks = this.calendarMonthWeeks.slice(1, numberOfMonths + 1);
347 const calendarMonthWeeksHeight = Math.max(0, ...visibleCalendarWeeks) * (daySize - 1);
348 const newMonthHeight = monthTitleHeight + calendarMonthWeeksHeight + 1;
349 this.adjustDayPickerHeight(newMonthHeight);
350 }
351
352 if (!prevProps.isFocused && isFocused && !focusedDate) {
353 this.container.focus();
354 }
355 }
356
357 componentWillUnmount() {
358 clearTimeout(this.setCalendarInfoWidthTimeout);
359 clearTimeout(this.setCalendarMonthGridHeightTimeout);
360 }
361
362 onKeyDown(e) {
363 e.stopPropagation();
364
365 if (!MODIFIER_KEY_NAMES.has(e.key)) {
366 this.throttledKeyDown(e);
367 }
368 }
369
370 onFinalKeyDown(e) {
371 this.setState({ withMouseInteractions: false });
372
373 const {
374 onBlur,
375 onTab,
376 onShiftTab,
377 isRTL,
378 } = this.props;
379 const { focusedDate, showKeyboardShortcuts } = this.state;
380 if (!focusedDate) return;
381
382 const newFocusedDate = focusedDate.clone();
383
384 let didTransitionMonth = false;
385
386 // focus might be anywhere when the keyboard shortcuts panel is opened so we want to
387 // return it to wherever it was before when the panel was opened
388 const activeElement = getActiveElement();
389 const onKeyboardShortcutsPanelClose = () => {
390 if (activeElement) activeElement.focus();
391 };
392
393 switch (e.key) {
394 case 'ArrowUp':
395 e.preventDefault();
396 newFocusedDate.subtract(1, 'week');
397 didTransitionMonth = this.maybeTransitionPrevMonth(newFocusedDate);
398 break;
399 case 'ArrowLeft':
400 e.preventDefault();
401 if (isRTL) {
402 newFocusedDate.add(1, 'day');
403 } else {
404 newFocusedDate.subtract(1, 'day');
405 }
406 didTransitionMonth = this.maybeTransitionPrevMonth(newFocusedDate);
407 break;
408 case 'Home':
409 e.preventDefault();
410 newFocusedDate.startOf('week');
411 didTransitionMonth = this.maybeTransitionPrevMonth(newFocusedDate);
412 break;
413 case 'PageUp':
414 e.preventDefault();
415 newFocusedDate.subtract(1, 'month');
416 didTransitionMonth = this.maybeTransitionPrevMonth(newFocusedDate);
417 break;
418
419 case 'ArrowDown':
420 e.preventDefault();
421 newFocusedDate.add(1, 'week');
422 didTransitionMonth = this.maybeTransitionNextMonth(newFocusedDate);
423 break;
424 case 'ArrowRight':
425 e.preventDefault();
426 if (isRTL) {
427 newFocusedDate.subtract(1, 'day');
428 } else {
429 newFocusedDate.add(1, 'day');
430 }
431 didTransitionMonth = this.maybeTransitionNextMonth(newFocusedDate);
432 break;
433 case 'End':
434 e.preventDefault();
435 newFocusedDate.endOf('week');
436 didTransitionMonth = this.maybeTransitionNextMonth(newFocusedDate);
437 break;
438 case 'PageDown':
439 e.preventDefault();
440 newFocusedDate.add(1, 'month');
441 didTransitionMonth = this.maybeTransitionNextMonth(newFocusedDate);
442 break;
443
444 case '?':
445 this.openKeyboardShortcutsPanel(onKeyboardShortcutsPanelClose);
446 break;
447
448 case 'Escape':
449 if (showKeyboardShortcuts) {
450 this.closeKeyboardShortcutsPanel();
451 } else {
452 onBlur(e);
453 }
454 break;
455
456 case 'Tab':
457 if (e.shiftKey) {
458 onShiftTab();
459 } else {
460 onTab(e);
461 }
462 break;
463
464 default:
465 break;
466 }
467
468 // If there was a month transition, do not update the focused date until the transition has
469 // completed. Otherwise, attempting to focus on a DOM node may interrupt the CSS animation. If
470 // didTransitionMonth is true, the focusedDate gets updated in #updateStateAfterMonthTransition
471 if (!didTransitionMonth) {
472 this.setState({
473 focusedDate: newFocusedDate,
474 });
475 }
476 }
477
478 onPrevMonthClick(e) {
479 if (e) e.preventDefault();
480 this.onPrevMonthTransition();
481 }
482
483 onPrevMonthTransition(nextFocusedDate) {
484 const { daySize, isRTL, numberOfMonths } = this.props;
485 const { calendarMonthWidth, monthTitleHeight } = this.state;
486
487 let translationValue;
488 if (this.isVertical()) {
489 const calendarMonthWeeksHeight = this.calendarMonthWeeks[0] * (daySize - 1);
490 translationValue = monthTitleHeight + calendarMonthWeeksHeight + 1;
491 } else if (this.isHorizontal()) {
492 translationValue = calendarMonthWidth;
493 if (isRTL) {
494 translationValue = -2 * calendarMonthWidth;
495 }
496
497 const visibleCalendarWeeks = this.calendarMonthWeeks.slice(0, numberOfMonths);
498 const calendarMonthWeeksHeight = Math.max(0, ...visibleCalendarWeeks) * (daySize - 1);
499 const newMonthHeight = monthTitleHeight + calendarMonthWeeksHeight + 1;
500 this.adjustDayPickerHeight(newMonthHeight);
501 }
502
503 this.setState({
504 monthTransition: PREV_TRANSITION,
505 translationValue,
506 focusedDate: null,
507 nextFocusedDate,
508 });
509 }
510
511 onMonthChange(currentMonth) {
512 this.setCalendarMonthWeeks(currentMonth);
513 this.calculateAndSetDayPickerHeight();
514
515 // Translation value is a hack to force an invisible transition that
516 // properly rerenders the CalendarMonthGrid
517 this.setState({
518 monthTransition: MONTH_SELECTION_TRANSITION,
519 translationValue: 0.00001,
520 focusedDate: null,
521 nextFocusedDate: currentMonth,
522 currentMonth,
523 });
524 }
525
526 onYearChange(currentMonth) {
527 this.setCalendarMonthWeeks(currentMonth);
528 this.calculateAndSetDayPickerHeight();
529
530 // Translation value is a hack to force an invisible transition that
531 // properly rerenders the CalendarMonthGrid
532 this.setState({
533 monthTransition: YEAR_SELECTION_TRANSITION,
534 translationValue: 0.0001,
535 focusedDate: null,
536 nextFocusedDate: currentMonth,
537 currentMonth,
538 });
539 }
540
541 onNextMonthClick(e) {
542 if (e) e.preventDefault();
543 this.onNextMonthTransition();
544 }
545
546 onNextMonthTransition(nextFocusedDate) {
547 const { isRTL, numberOfMonths, daySize } = this.props;
548 const { calendarMonthWidth, monthTitleHeight } = this.state;
549
550 let translationValue;
551
552 if (this.isVertical()) {
553 const firstVisibleMonthWeeks = this.calendarMonthWeeks[1];
554 const calendarMonthWeeksHeight = firstVisibleMonthWeeks * (daySize - 1);
555 translationValue = -(monthTitleHeight + calendarMonthWeeksHeight + 1);
556 }
557
558 if (this.isHorizontal()) {
559 translationValue = -calendarMonthWidth;
560 if (isRTL) {
561 translationValue = 0;
562 }
563
564 const visibleCalendarWeeks = this.calendarMonthWeeks.slice(2, numberOfMonths + 2);
565 const calendarMonthWeeksHeight = Math.max(0, ...visibleCalendarWeeks) * (daySize - 1);
566 const newMonthHeight = monthTitleHeight + calendarMonthWeeksHeight + 1;
567 this.adjustDayPickerHeight(newMonthHeight);
568 }
569
570 this.setState({
571 monthTransition: NEXT_TRANSITION,
572 translationValue,
573 focusedDate: null,
574 nextFocusedDate,
575 });
576 }
577
578 getFirstDayOfWeek() {
579 const { firstDayOfWeek } = this.props;
580 if (firstDayOfWeek == null) {
581 return moment.localeData().firstDayOfWeek();
582 }
583
584 return firstDayOfWeek;
585 }
586
587 getWeekHeaders() {
588 const { weekDayFormat } = this.props;
589 const { currentMonth } = this.state;
590 const firstDayOfWeek = this.getFirstDayOfWeek();
591
592 const weekHeaders = [];
593 for (let i = 0; i < 7; i += 1) {
594 weekHeaders.push(currentMonth.day((i + firstDayOfWeek) % 7).format(weekDayFormat));
595 }
596
597 return weekHeaders;
598 }
599
600 getFirstVisibleIndex() {
601 const { orientation } = this.props;
602 const { monthTransition } = this.state;
603
604 if (orientation === VERTICAL_SCROLLABLE) return 0;
605
606 let firstVisibleMonthIndex = 1;
607 if (monthTransition === PREV_TRANSITION) {
608 firstVisibleMonthIndex -= 1;
609 } else if (monthTransition === NEXT_TRANSITION) {
610 firstVisibleMonthIndex += 1;
611 }
612
613 return firstVisibleMonthIndex;
614 }
615
616 getFocusedDay(newMonth) {
617 const { getFirstFocusableDay, numberOfMonths } = this.props;
618
619 let focusedDate;
620 if (getFirstFocusableDay) {
621 focusedDate = getFirstFocusableDay(newMonth);
622 }
623
624 if (newMonth && (!focusedDate || !isDayVisible(focusedDate, newMonth, numberOfMonths))) {
625 focusedDate = newMonth.clone().startOf('month');
626 }
627
628 return focusedDate;
629 }
630
631 setMonthTitleHeight(monthTitleHeight) {
632 this.setState({
633 monthTitleHeight,
634 }, () => {
635 this.calculateAndSetDayPickerHeight();
636 });
637 }
638
639 setCalendarMonthWeeks(currentMonth) {
640 const { numberOfMonths } = this.props;
641
642 this.calendarMonthWeeks = [];
643 let month = currentMonth.clone().subtract(1, 'months');
644 const firstDayOfWeek = this.getFirstDayOfWeek();
645 for (let i = 0; i < numberOfMonths + 2; i += 1) {
646 const numberOfWeeks = getNumberOfCalendarMonthWeeks(month, firstDayOfWeek);
647 this.calendarMonthWeeks.push(numberOfWeeks);
648 month = month.add(1, 'months');
649 }
650 }
651
652 setContainerRef(ref) {
653 this.container = ref;
654 }
655
656 setCalendarInfoRef(ref) {
657 this.calendarInfo = ref;
658 }
659
660 setTransitionContainerRef(ref) {
661 this.transitionContainer = ref;
662 }
663
664 maybeTransitionNextMonth(newFocusedDate) {
665 const { numberOfMonths } = this.props;
666 const { currentMonth, focusedDate } = this.state;
667
668 const newFocusedDateMonth = newFocusedDate.month();
669 const focusedDateMonth = focusedDate.month();
670 const isNewFocusedDateVisible = isDayVisible(newFocusedDate, currentMonth, numberOfMonths);
671 if (newFocusedDateMonth !== focusedDateMonth && !isNewFocusedDateVisible) {
672 this.onNextMonthTransition(newFocusedDate);
673 return true;
674 }
675
676 return false;
677 }
678
679 maybeTransitionPrevMonth(newFocusedDate) {
680 const { numberOfMonths } = this.props;
681 const { currentMonth, focusedDate } = this.state;
682
683 const newFocusedDateMonth = newFocusedDate.month();
684 const focusedDateMonth = focusedDate.month();
685 const isNewFocusedDateVisible = isDayVisible(newFocusedDate, currentMonth, numberOfMonths);
686 if (newFocusedDateMonth !== focusedDateMonth && !isNewFocusedDateVisible) {
687 this.onPrevMonthTransition(newFocusedDate);
688 return true;
689 }
690
691 return false;
692 }
693
694 multiplyScrollableMonths(e) {
695 const { onMultiplyScrollableMonths } = this.props;
696 if (e) e.preventDefault();
697
698 if (onMultiplyScrollableMonths) onMultiplyScrollableMonths(e);
699
700 this.setState(({ scrollableMonthMultiple }) => ({
701 scrollableMonthMultiple: scrollableMonthMultiple + 1,
702 }));
703 }
704
705 isHorizontal() {
706 const { orientation } = this.props;
707 return orientation === HORIZONTAL_ORIENTATION;
708 }
709
710 isVertical() {
711 const { orientation } = this.props;
712 return orientation === VERTICAL_ORIENTATION || orientation === VERTICAL_SCROLLABLE;
713 }
714
715 updateStateAfterMonthTransition() {
716 const {
717 onPrevMonthClick,
718 onNextMonthClick,
719 numberOfMonths,
720 onMonthChange,
721 onYearChange,
722 isRTL,
723 } = this.props;
724
725 const {
726 currentMonth,
727 monthTransition,
728 focusedDate,
729 nextFocusedDate,
730 withMouseInteractions,
731 calendarMonthWidth,
732 } = this.state;
733
734 if (!monthTransition) return;
735
736 const newMonth = currentMonth.clone();
737 const firstDayOfWeek = this.getFirstDayOfWeek();
738 if (monthTransition === PREV_TRANSITION) {
739 newMonth.subtract(1, 'month');
740 if (onPrevMonthClick) onPrevMonthClick(newMonth);
741 const newInvisibleMonth = newMonth.clone().subtract(1, 'month');
742 const numberOfWeeks = getNumberOfCalendarMonthWeeks(newInvisibleMonth, firstDayOfWeek);
743 this.calendarMonthWeeks = [numberOfWeeks, ...this.calendarMonthWeeks.slice(0, -1)];
744 } else if (monthTransition === NEXT_TRANSITION) {
745 newMonth.add(1, 'month');
746 if (onNextMonthClick) onNextMonthClick(newMonth);
747 const newInvisibleMonth = newMonth.clone().add(numberOfMonths, 'month');
748 const numberOfWeeks = getNumberOfCalendarMonthWeeks(newInvisibleMonth, firstDayOfWeek);
749 this.calendarMonthWeeks = [...this.calendarMonthWeeks.slice(1), numberOfWeeks];
750 } else if (monthTransition === MONTH_SELECTION_TRANSITION) {
751 if (onMonthChange) onMonthChange(newMonth);
752 } else if (monthTransition === YEAR_SELECTION_TRANSITION) {
753 if (onYearChange) onYearChange(newMonth);
754 }
755
756 let newFocusedDate = null;
757 if (nextFocusedDate) {
758 newFocusedDate = nextFocusedDate;
759 } else if (!focusedDate && !withMouseInteractions) {
760 newFocusedDate = this.getFocusedDay(newMonth);
761 }
762
763 this.setState({
764 currentMonth: newMonth,
765 monthTransition: null,
766 translationValue: (isRTL && this.isHorizontal()) ? -calendarMonthWidth : 0,
767 nextFocusedDate: null,
768 focusedDate: newFocusedDate,
769 }, () => {
770 // we don't want to focus on the relevant calendar day after a month transition
771 // if the user is navigating around using a mouse
772 if (withMouseInteractions) {
773 const activeElement = getActiveElement();
774 if (
775 activeElement
776 && activeElement !== document.body
777 && this.container.contains(activeElement)
778 && activeElement.blur
779 ) {
780 activeElement.blur();
781 }
782 }
783 });
784 }
785
786 adjustDayPickerHeight(newMonthHeight) {
787 const monthHeight = newMonthHeight + MONTH_PADDING;
788 if (monthHeight !== this.calendarMonthGridHeight) {
789 this.transitionContainer.style.height = `${monthHeight}px`;
790 if (!this.calendarMonthGridHeight) {
791 this.setCalendarMonthGridHeightTimeout = setTimeout(() => {
792 this.setState({ hasSetHeight: true });
793 }, 0);
794 }
795 this.calendarMonthGridHeight = monthHeight;
796 }
797 }
798
799 calculateAndSetDayPickerHeight() {
800 const { daySize, numberOfMonths } = this.props;
801 const { monthTitleHeight } = this.state;
802
803 const visibleCalendarWeeks = this.calendarMonthWeeks.slice(1, numberOfMonths + 1);
804 const calendarMonthWeeksHeight = Math.max(0, ...visibleCalendarWeeks) * (daySize - 1);
805 const newMonthHeight = monthTitleHeight + calendarMonthWeeksHeight + 1;
806
807 if (this.isHorizontal()) {
808 this.adjustDayPickerHeight(newMonthHeight);
809 }
810 }
811
812 openKeyboardShortcutsPanel(onCloseCallBack) {
813 this.setState({
814 showKeyboardShortcuts: true,
815 onKeyboardShortcutsPanelClose: onCloseCallBack,
816 });
817 }
818
819 closeKeyboardShortcutsPanel() {
820 const { onKeyboardShortcutsPanelClose } = this.state;
821
822 if (onKeyboardShortcutsPanelClose) {
823 onKeyboardShortcutsPanelClose();
824 }
825
826 this.setState({
827 onKeyboardShortcutsPanelClose: null,
828 showKeyboardShortcuts: false,
829 });
830 }
831
832 renderNavigation() {
833 const {
834 disablePrev,
835 disableNext,
836 navPrev,
837 navNext,
838 noNavButtons,
839 orientation,
840 phrases,
841 isRTL,
842 } = this.props;
843
844 if (noNavButtons) {
845 return null;
846 }
847
848 const onNextMonthClick = orientation === VERTICAL_SCROLLABLE
849 ? this.multiplyScrollableMonths
850 : this.onNextMonthClick;
851
852 return (
853 <DayPickerNavigation
854 disablePrev={disablePrev}
855 disableNext={disableNext}
856 onPrevMonthClick={this.onPrevMonthClick}
857 onNextMonthClick={onNextMonthClick}
858 navPrev={navPrev}
859 navNext={navNext}
860 orientation={orientation}
861 phrases={phrases}
862 isRTL={isRTL}
863 />
864 );
865 }
866
867 renderWeekHeader(index) {
868 const {
869 daySize,
870 horizontalMonthPadding,
871 orientation,
872 renderWeekHeaderElement,
873 styles,
874 css,
875 } = this.props;
876
877 const { calendarMonthWidth } = this.state;
878
879 const verticalScrollable = orientation === VERTICAL_SCROLLABLE;
880
881 const horizontalStyle = {
882 left: index * calendarMonthWidth,
883 };
884 const verticalStyle = {
885 marginLeft: -calendarMonthWidth / 2,
886 };
887
888 let weekHeaderStyle = {}; // no styles applied to the vertical-scrollable orientation
889 if (this.isHorizontal()) {
890 weekHeaderStyle = horizontalStyle;
891 } else if (this.isVertical() && !verticalScrollable) {
892 weekHeaderStyle = verticalStyle;
893 }
894
895 const weekHeaders = this.getWeekHeaders();
896 const header = weekHeaders.map((day) => (
897 <li key={day} {...css(styles.DayPicker_weekHeader_li, { width: daySize })}>
898 {renderWeekHeaderElement ? renderWeekHeaderElement(day) : <small>{day}</small>}
899 </li>
900 ));
901
902 return (
903 <div
904 {...css(
905 styles.DayPicker_weekHeader,
906 this.isVertical() && styles.DayPicker_weekHeader__vertical,
907 verticalScrollable && styles.DayPicker_weekHeader__verticalScrollable,
908 weekHeaderStyle,
909 { padding: `0 ${horizontalMonthPadding}px` },
910 )}
911 key={`week-${index}`}
912 >
913 <ul {...css(styles.DayPicker_weekHeader_ul)}>
914 {header}
915 </ul>
916 </div>
917 );
918 }
919
920 render() {
921 const {
922 calendarMonthWidth,
923 currentMonth,
924 monthTransition,
925 translationValue,
926 scrollableMonthMultiple,
927 focusedDate,
928 showKeyboardShortcuts,
929 isTouchDevice: isTouch,
930 hasSetHeight,
931 calendarInfoWidth,
932 monthTitleHeight,
933 } = this.state;
934
935 const {
936 css,
937 enableOutsideDays,
938 numberOfMonths,
939 orientation,
940 modifiers,
941 withPortal,
942 onDayClick,
943 onDayMouseEnter,
944 onDayMouseLeave,
945 firstDayOfWeek,
946 renderMonthText,
947 renderCalendarDay,
948 renderDayContents,
949 renderCalendarInfo,
950 renderMonthElement,
951 renderKeyboardShortcutsButton,
952 calendarInfoPosition,
953 hideKeyboardShortcutsPanel,
954 onOutsideClick,
955 monthFormat,
956 daySize,
957 isFocused,
958 isRTL,
959 styles,
960 theme,
961 phrases,
962 verticalHeight,
963 dayAriaLabelFormat,
964 noBorder,
965 transitionDuration,
966 verticalBorderSpacing,
967 horizontalMonthPadding,
968 } = this.props;
969
970 const { reactDates: { spacing: { dayPickerHorizontalPadding } } } = theme;
971
972 const isHorizontal = this.isHorizontal();
973
974 const numOfWeekHeaders = this.isVertical() ? 1 : numberOfMonths;
975 const weekHeaders = [];
976 for (let i = 0; i < numOfWeekHeaders; i += 1) {
977 weekHeaders.push(this.renderWeekHeader(i));
978 }
979
980 const verticalScrollable = orientation === VERTICAL_SCROLLABLE;
981 let height;
982 if (isHorizontal) {
983 height = this.calendarMonthGridHeight;
984 } else if (this.isVertical() && !verticalScrollable && !withPortal) {
985 // If the user doesn't set a desired height,
986 // we default back to this kind of made-up value that generally looks good
987 height = verticalHeight || 1.75 * calendarMonthWidth;
988 }
989
990 const isCalendarMonthGridAnimating = monthTransition !== null;
991
992 const shouldFocusDate = !isCalendarMonthGridAnimating && isFocused;
993
994 let keyboardShortcutButtonLocation = BOTTOM_RIGHT;
995 if (this.isVertical()) {
996 keyboardShortcutButtonLocation = withPortal ? TOP_LEFT : TOP_RIGHT;
997 }
998
999 const shouldAnimateHeight = isHorizontal && hasSetHeight;
1000
1001 const calendarInfoPositionTop = calendarInfoPosition === INFO_POSITION_TOP;
1002 const calendarInfoPositionBottom = calendarInfoPosition === INFO_POSITION_BOTTOM;
1003 const calendarInfoPositionBefore = calendarInfoPosition === INFO_POSITION_BEFORE;
1004 const calendarInfoPositionAfter = calendarInfoPosition === INFO_POSITION_AFTER;
1005 const calendarInfoIsInline = calendarInfoPositionBefore || calendarInfoPositionAfter;
1006
1007 const calendarInfo = renderCalendarInfo && (
1008 <div
1009 ref={this.setCalendarInfoRef}
1010 {...css((calendarInfoIsInline) && styles.DayPicker_calendarInfo__horizontal)}
1011 >
1012 {renderCalendarInfo()}
1013 </div>
1014 );
1015
1016 const calendarInfoPanelWidth = renderCalendarInfo && calendarInfoIsInline
1017 ? calendarInfoWidth
1018 : 0;
1019
1020 const firstVisibleMonthIndex = this.getFirstVisibleIndex();
1021 const wrapperHorizontalWidth = (calendarMonthWidth * numberOfMonths)
1022 + (2 * dayPickerHorizontalPadding);
1023 // Adding `1px` because of whitespace between 2 inline-block
1024 const fullHorizontalWidth = wrapperHorizontalWidth + calendarInfoPanelWidth + 1;
1025
1026 const transitionContainerStyle = {
1027 width: isHorizontal && wrapperHorizontalWidth,
1028 height,
1029 };
1030
1031 const dayPickerWrapperStyle = {
1032 width: isHorizontal && wrapperHorizontalWidth,
1033 };
1034
1035 const dayPickerStyle = {
1036 width: isHorizontal && fullHorizontalWidth,
1037
1038 // These values are to center the datepicker (approximately) on the page
1039 marginLeft: isHorizontal && withPortal ? -fullHorizontalWidth / 2 : null,
1040 marginTop: isHorizontal && withPortal ? -calendarMonthWidth / 2 : null,
1041 };
1042
1043 return (
1044 <div
1045 {...css(
1046 styles.DayPicker,
1047 isHorizontal && styles.DayPicker__horizontal,
1048 verticalScrollable && styles.DayPicker__verticalScrollable,
1049 isHorizontal && withPortal && styles.DayPicker_portal__horizontal,
1050 this.isVertical() && withPortal && styles.DayPicker_portal__vertical,
1051 dayPickerStyle,
1052 !monthTitleHeight && styles.DayPicker__hidden,
1053 !noBorder && styles.DayPicker__withBorder,
1054 )}
1055 >
1056 <OutsideClickHandler onOutsideClick={onOutsideClick}>
1057 {(calendarInfoPositionTop || calendarInfoPositionBefore) && calendarInfo}
1058
1059 <div
1060 {...css(
1061 dayPickerWrapperStyle,
1062 calendarInfoIsInline && isHorizontal && styles.DayPicker_wrapper__horizontal,
1063 )}
1064 >
1065
1066 <div
1067 {...css(
1068 styles.DayPicker_weekHeaders,
1069 isHorizontal && styles.DayPicker_weekHeaders__horizontal,
1070 )}
1071 aria-hidden="true"
1072 role="presentation"
1073 >
1074 {weekHeaders}
1075 </div>
1076
1077 <div // eslint-disable-line jsx-a11y/no-noninteractive-element-interactions
1078 {...css(styles.DayPicker_focusRegion)}
1079 ref={this.setContainerRef}
1080 onClick={(e) => { e.stopPropagation(); }}
1081 onKeyDown={this.onKeyDown}
1082 onMouseUp={() => { this.setState({ withMouseInteractions: true }); }}
1083 tabIndex={-1}
1084 role="application"
1085 aria-roledescription={phrases.roleDescription}
1086 aria-label={phrases.calendarLabel}
1087 >
1088 {!verticalScrollable && this.renderNavigation()}
1089
1090 <div
1091 {...css(
1092 styles.DayPicker_transitionContainer,
1093 shouldAnimateHeight && styles.DayPicker_transitionContainer__horizontal,
1094 this.isVertical() && styles.DayPicker_transitionContainer__vertical,
1095 verticalScrollable && styles.DayPicker_transitionContainer__verticalScrollable,
1096 transitionContainerStyle,
1097 )}
1098 ref={this.setTransitionContainerRef}
1099 >
1100 <CalendarMonthGrid
1101 setMonthTitleHeight={!monthTitleHeight ? this.setMonthTitleHeight : undefined}
1102 translationValue={translationValue}
1103 enableOutsideDays={enableOutsideDays}
1104 firstVisibleMonthIndex={firstVisibleMonthIndex}
1105 initialMonth={currentMonth}
1106 isAnimating={isCalendarMonthGridAnimating}
1107 modifiers={modifiers}
1108 orientation={orientation}
1109 numberOfMonths={numberOfMonths * scrollableMonthMultiple}
1110 onDayClick={onDayClick}
1111 onDayMouseEnter={onDayMouseEnter}
1112 onDayMouseLeave={onDayMouseLeave}
1113 onMonthChange={this.onMonthChange}
1114 onYearChange={this.onYearChange}
1115 renderMonthText={renderMonthText}
1116 renderCalendarDay={renderCalendarDay}
1117 renderDayContents={renderDayContents}
1118 renderMonthElement={renderMonthElement}
1119 onMonthTransitionEnd={this.updateStateAfterMonthTransition}
1120 monthFormat={monthFormat}
1121 daySize={daySize}
1122 firstDayOfWeek={firstDayOfWeek}
1123 isFocused={shouldFocusDate}
1124 focusedDate={focusedDate}
1125 phrases={phrases}
1126 isRTL={isRTL}
1127 dayAriaLabelFormat={dayAriaLabelFormat}
1128 transitionDuration={transitionDuration}
1129 verticalBorderSpacing={verticalBorderSpacing}
1130 horizontalMonthPadding={horizontalMonthPadding}
1131 />
1132 {verticalScrollable && this.renderNavigation()}
1133 </div>
1134
1135 {!isTouch && !hideKeyboardShortcutsPanel && (
1136 <DayPickerKeyboardShortcuts
1137 block={this.isVertical() && !withPortal}
1138 buttonLocation={keyboardShortcutButtonLocation}
1139 showKeyboardShortcutsPanel={showKeyboardShortcuts}
1140 openKeyboardShortcutsPanel={this.openKeyboardShortcutsPanel}
1141 closeKeyboardShortcutsPanel={this.closeKeyboardShortcutsPanel}
1142 phrases={phrases}
1143 renderKeyboardShortcutsButton={renderKeyboardShortcutsButton}
1144 />
1145 )}
1146 </div>
1147 </div>
1148
1149 {(calendarInfoPositionBottom || calendarInfoPositionAfter) && calendarInfo}
1150 </OutsideClickHandler>
1151 </div>
1152 );
1153 }
1154}
1155
1156DayPicker.propTypes = propTypes;
1157DayPicker.defaultProps = defaultProps;
1158
1159export { DayPicker as PureDayPicker };
1160
1161export default withStyles(({
1162 reactDates: {
1163 color,
1164 font,
1165 noScrollBarOnVerticalScrollable,
1166 spacing,
1167 zIndex,
1168 },
1169}) => ({
1170 DayPicker: {
1171 background: color.background,
1172 position: 'relative',
1173 textAlign: noflip('left'),
1174 },
1175
1176 DayPicker__horizontal: {
1177 background: color.background,
1178 },
1179
1180 DayPicker__verticalScrollable: {
1181 height: '100%',
1182 },
1183
1184 DayPicker__hidden: {
1185 visibility: 'hidden',
1186 },
1187
1188 DayPicker__withBorder: {
1189 boxShadow: noflip('0 2px 6px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(0, 0, 0, 0.07)'),
1190 borderRadius: 3,
1191 },
1192
1193 DayPicker_portal__horizontal: {
1194 boxShadow: 'none',
1195 position: 'absolute',
1196 left: noflip('50%'),
1197 top: '50%',
1198 },
1199
1200 DayPicker_portal__vertical: {
1201 position: 'initial',
1202 },
1203
1204 DayPicker_focusRegion: {
1205 outline: 'none',
1206 },
1207
1208 DayPicker_calendarInfo__horizontal: {
1209 display: 'inline-block',
1210 verticalAlign: 'top',
1211 },
1212
1213 DayPicker_wrapper__horizontal: {
1214 display: 'inline-block',
1215 verticalAlign: 'top',
1216 },
1217
1218 DayPicker_weekHeaders: {
1219 position: 'relative',
1220 },
1221
1222 DayPicker_weekHeaders__horizontal: {
1223 marginLeft: noflip(spacing.dayPickerHorizontalPadding),
1224 },
1225
1226 DayPicker_weekHeader: {
1227 color: color.placeholderText,
1228 position: 'absolute',
1229 top: 62,
1230 zIndex: zIndex + 2,
1231 textAlign: noflip('left'),
1232 },
1233
1234 DayPicker_weekHeader__vertical: {
1235 left: noflip('50%'),
1236 },
1237
1238 DayPicker_weekHeader__verticalScrollable: {
1239 top: 0,
1240 display: 'table-row',
1241 borderBottom: `1px solid ${color.core.border}`,
1242 background: color.background,
1243 marginLeft: noflip(0),
1244 left: noflip(0),
1245 width: '100%',
1246 textAlign: 'center',
1247 },
1248
1249 DayPicker_weekHeader_ul: {
1250 listStyle: 'none',
1251 margin: '1px 0',
1252 paddingLeft: noflip(0),
1253 paddingRight: noflip(0),
1254 fontSize: font.size,
1255 },
1256
1257 DayPicker_weekHeader_li: {
1258 display: 'inline-block',
1259 textAlign: 'center',
1260 },
1261
1262 DayPicker_transitionContainer: {
1263 position: 'relative',
1264 overflow: 'hidden',
1265 borderRadius: 3,
1266 },
1267
1268 DayPicker_transitionContainer__horizontal: {
1269 transition: 'height 0.2s ease-in-out',
1270 },
1271
1272 DayPicker_transitionContainer__vertical: {
1273 width: '100%',
1274 },
1275
1276 DayPicker_transitionContainer__verticalScrollable: {
1277 paddingTop: 20,
1278 height: '100%',
1279 position: 'absolute',
1280 top: 0,
1281 bottom: 0,
1282 right: noflip(0),
1283 left: noflip(0),
1284 overflowY: 'scroll',
1285 ...(noScrollBarOnVerticalScrollable && {
1286 '-webkitOverflowScrolling': 'touch',
1287 '::-webkit-scrollbar': {
1288 '-webkit-appearance': 'none',
1289 display: 'none',
1290 },
1291 }),
1292 },
1293}), { pureComponent: typeof React.PureComponent !== 'undefined' })(DayPicker);