UNPKG

13.7 kBJSXView Raw
1import React from 'react';
2import PropTypes from 'prop-types';
3import momentPropTypes from 'react-moment-proptypes';
4import { forbidExtraProps, mutuallyExclusiveProps, nonNegativeInteger } from 'airbnb-prop-types';
5import { withStyles, withStylesPropTypes } from 'react-with-styles';
6import moment from 'moment';
7import { addEventListener } from 'consolidated-events';
8
9import { CalendarDayPhrases } from '../defaultPhrases';
10import getPhrasePropTypes from '../utils/getPhrasePropTypes';
11import noflip from '../utils/noflip';
12
13import CalendarMonth from './CalendarMonth';
14
15import isTransitionEndSupported from '../utils/isTransitionEndSupported';
16import getTransformStyles from '../utils/getTransformStyles';
17import getCalendarMonthWidth from '../utils/getCalendarMonthWidth';
18import toISOMonthString from '../utils/toISOMonthString';
19import isPrevMonth from '../utils/isPrevMonth';
20import isNextMonth from '../utils/isNextMonth';
21
22import ModifiersShape from '../shapes/ModifiersShape';
23import ScrollableOrientationShape from '../shapes/ScrollableOrientationShape';
24import DayOfWeekShape from '../shapes/DayOfWeekShape';
25
26
27import {
28 HORIZONTAL_ORIENTATION,
29 VERTICAL_ORIENTATION,
30 VERTICAL_SCROLLABLE,
31 DAY_SIZE,
32} from '../constants';
33
34const propTypes = forbidExtraProps({
35 ...withStylesPropTypes,
36 enableOutsideDays: PropTypes.bool,
37 firstVisibleMonthIndex: PropTypes.number,
38 horizontalMonthPadding: nonNegativeInteger,
39 initialMonth: momentPropTypes.momentObj,
40 isAnimating: PropTypes.bool,
41 numberOfMonths: PropTypes.number,
42 modifiers: PropTypes.objectOf(PropTypes.objectOf(ModifiersShape)),
43 orientation: ScrollableOrientationShape,
44 onDayClick: PropTypes.func,
45 onDayMouseEnter: PropTypes.func,
46 onDayMouseLeave: PropTypes.func,
47 onMonthTransitionEnd: PropTypes.func,
48 onMonthChange: PropTypes.func,
49 onYearChange: PropTypes.func,
50 renderMonthText: mutuallyExclusiveProps(PropTypes.func, 'renderMonthText', 'renderMonthElement'),
51 renderCalendarDay: PropTypes.func,
52 renderDayContents: PropTypes.func,
53 translationValue: PropTypes.number,
54 renderMonthElement: mutuallyExclusiveProps(PropTypes.func, 'renderMonthText', 'renderMonthElement'),
55 daySize: nonNegativeInteger,
56 focusedDate: momentPropTypes.momentObj, // indicates focusable day
57 isFocused: PropTypes.bool, // indicates whether or not to move focus to focusable day
58 firstDayOfWeek: DayOfWeekShape,
59 setMonthTitleHeight: PropTypes.func,
60 isRTL: PropTypes.bool,
61 transitionDuration: nonNegativeInteger,
62 verticalBorderSpacing: nonNegativeInteger,
63
64 // i18n
65 monthFormat: PropTypes.string,
66 phrases: PropTypes.shape(getPhrasePropTypes(CalendarDayPhrases)),
67 dayAriaLabelFormat: PropTypes.string,
68});
69
70const defaultProps = {
71 enableOutsideDays: false,
72 firstVisibleMonthIndex: 0,
73 horizontalMonthPadding: 13,
74 initialMonth: moment(),
75 isAnimating: false,
76 numberOfMonths: 1,
77 modifiers: {},
78 orientation: HORIZONTAL_ORIENTATION,
79 onDayClick() {},
80 onDayMouseEnter() {},
81 onDayMouseLeave() {},
82 onMonthChange() {},
83 onYearChange() {},
84 onMonthTransitionEnd() {},
85 renderMonthText: null,
86 renderCalendarDay: undefined,
87 renderDayContents: null,
88 translationValue: null,
89 renderMonthElement: null,
90 daySize: DAY_SIZE,
91 focusedDate: null,
92 isFocused: false,
93 firstDayOfWeek: null,
94 setMonthTitleHeight: null,
95 isRTL: false,
96 transitionDuration: 200,
97 verticalBorderSpacing: undefined,
98
99 // i18n
100 monthFormat: 'MMMM YYYY', // english locale
101 phrases: CalendarDayPhrases,
102 dayAriaLabelFormat: undefined,
103};
104
105function getMonths(initialMonth, numberOfMonths, withoutTransitionMonths) {
106 let month = initialMonth.clone();
107 if (!withoutTransitionMonths) month = month.subtract(1, 'month');
108
109 const months = [];
110 for (let i = 0; i < (withoutTransitionMonths ? numberOfMonths : numberOfMonths + 2); i += 1) {
111 months.push(month);
112 month = month.clone().add(1, 'month');
113 }
114
115 return months;
116}
117
118class CalendarMonthGrid extends React.PureComponent {
119 constructor(props) {
120 super(props);
121 const withoutTransitionMonths = props.orientation === VERTICAL_SCROLLABLE;
122 this.state = {
123 months: getMonths(props.initialMonth, props.numberOfMonths, withoutTransitionMonths),
124 };
125
126 this.isTransitionEndSupported = isTransitionEndSupported();
127 this.onTransitionEnd = this.onTransitionEnd.bind(this);
128 this.setContainerRef = this.setContainerRef.bind(this);
129
130 this.locale = moment.locale();
131 this.onMonthSelect = this.onMonthSelect.bind(this);
132 this.onYearSelect = this.onYearSelect.bind(this);
133 }
134
135 componentDidMount() {
136 this.removeEventListener = addEventListener(
137 this.container,
138 'transitionend',
139 this.onTransitionEnd,
140 );
141 }
142
143 componentWillReceiveProps(nextProps) {
144 const { initialMonth, numberOfMonths, orientation } = nextProps;
145 const { months } = this.state;
146
147 const {
148 initialMonth: prevInitialMonth,
149 numberOfMonths: prevNumberOfMonths,
150 } = this.props;
151 const hasMonthChanged = !prevInitialMonth.isSame(initialMonth, 'month');
152 const hasNumberOfMonthsChanged = prevNumberOfMonths !== numberOfMonths;
153 let newMonths = months;
154
155 if (hasMonthChanged && !hasNumberOfMonthsChanged) {
156 if (isNextMonth(prevInitialMonth, initialMonth)) {
157 newMonths = months.slice(1);
158 newMonths.push(months[months.length - 1].clone().add(1, 'month'));
159 } else if (isPrevMonth(prevInitialMonth, initialMonth)) {
160 newMonths = months.slice(0, months.length - 1);
161 newMonths.unshift(months[0].clone().subtract(1, 'month'));
162 } else {
163 const withoutTransitionMonths = orientation === VERTICAL_SCROLLABLE;
164 newMonths = getMonths(initialMonth, numberOfMonths, withoutTransitionMonths);
165 }
166 }
167
168 if (hasNumberOfMonthsChanged) {
169 const withoutTransitionMonths = orientation === VERTICAL_SCROLLABLE;
170 newMonths = getMonths(initialMonth, numberOfMonths, withoutTransitionMonths);
171 }
172
173 const momentLocale = moment.locale();
174 if (this.locale !== momentLocale) {
175 this.locale = momentLocale;
176 newMonths = newMonths.map((m) => m.locale(this.locale));
177 }
178
179 this.setState({
180 months: newMonths,
181 });
182 }
183
184 componentDidUpdate() {
185 const {
186 isAnimating,
187 transitionDuration,
188 onMonthTransitionEnd,
189 } = this.props;
190
191 // For IE9, immediately call onMonthTransitionEnd instead of
192 // waiting for the animation to complete. Similarly, if transitionDuration
193 // is set to 0, also immediately invoke the onMonthTransitionEnd callback
194 if ((!this.isTransitionEndSupported || !transitionDuration) && isAnimating) {
195 onMonthTransitionEnd();
196 }
197 }
198
199 componentWillUnmount() {
200 if (this.removeEventListener) this.removeEventListener();
201 }
202
203 onTransitionEnd() {
204 const { onMonthTransitionEnd } = this.props;
205 onMonthTransitionEnd();
206 }
207
208 onMonthSelect(currentMonth, newMonthVal) {
209 const newMonth = currentMonth.clone();
210 const { onMonthChange, orientation } = this.props;
211 const { months } = this.state;
212 const withoutTransitionMonths = orientation === VERTICAL_SCROLLABLE;
213 let initialMonthSubtraction = months.indexOf(currentMonth);
214 if (!withoutTransitionMonths) {
215 initialMonthSubtraction -= 1;
216 }
217 newMonth.set('month', newMonthVal).subtract(initialMonthSubtraction, 'months');
218 onMonthChange(newMonth);
219 }
220
221 onYearSelect(currentMonth, newYearVal) {
222 const newMonth = currentMonth.clone();
223 const { onYearChange, orientation } = this.props;
224 const { months } = this.state;
225 const withoutTransitionMonths = orientation === VERTICAL_SCROLLABLE;
226 let initialMonthSubtraction = months.indexOf(currentMonth);
227 if (!withoutTransitionMonths) {
228 initialMonthSubtraction -= 1;
229 }
230 newMonth.set('year', newYearVal).subtract(initialMonthSubtraction, 'months');
231 onYearChange(newMonth);
232 }
233
234 setContainerRef(ref) {
235 this.container = ref;
236 }
237
238 render() {
239 const {
240 enableOutsideDays,
241 firstVisibleMonthIndex,
242 horizontalMonthPadding,
243 isAnimating,
244 modifiers,
245 numberOfMonths,
246 monthFormat,
247 orientation,
248 translationValue,
249 daySize,
250 onDayMouseEnter,
251 onDayMouseLeave,
252 onDayClick,
253 renderMonthText,
254 renderCalendarDay,
255 renderDayContents,
256 renderMonthElement,
257 onMonthTransitionEnd,
258 firstDayOfWeek,
259 focusedDate,
260 isFocused,
261 isRTL,
262 styles,
263 phrases,
264 dayAriaLabelFormat,
265 transitionDuration,
266 verticalBorderSpacing,
267 setMonthTitleHeight,
268 css,
269 } = this.props;
270
271 const { months } = this.state;
272 const isVertical = orientation === VERTICAL_ORIENTATION;
273 const isVerticalScrollable = orientation === VERTICAL_SCROLLABLE;
274 const isHorizontal = orientation === HORIZONTAL_ORIENTATION;
275
276 const calendarMonthWidth = getCalendarMonthWidth(
277 daySize,
278 horizontalMonthPadding,
279 );
280
281 const width = isVertical || isVerticalScrollable
282 ? calendarMonthWidth
283 : (numberOfMonths + 2) * calendarMonthWidth;
284
285 const transformType = (isVertical || isVerticalScrollable) ? 'translateY' : 'translateX';
286 const transformValue = `${transformType}(${translationValue}px)`;
287
288 return (
289 <div
290 {...css(
291 styles.CalendarMonthGrid,
292 isHorizontal && styles.CalendarMonthGrid__horizontal,
293 isVertical && styles.CalendarMonthGrid__vertical,
294 isVerticalScrollable && styles.CalendarMonthGrid__vertical_scrollable,
295 isAnimating && styles.CalendarMonthGrid__animating,
296 isAnimating && transitionDuration && {
297 transition: `transform ${transitionDuration}ms ease-in-out`,
298 },
299 {
300 ...getTransformStyles(transformValue),
301 width,
302 },
303 )}
304 ref={this.setContainerRef}
305 onTransitionEnd={onMonthTransitionEnd}
306 >
307 {months.map((month, i) => {
308 const isVisible = (i >= firstVisibleMonthIndex)
309 && (i < firstVisibleMonthIndex + numberOfMonths);
310 const hideForAnimation = i === 0 && !isVisible;
311 const showForAnimation = i === 0 && isAnimating && isVisible;
312 const monthString = toISOMonthString(month);
313 return (
314 <div
315 key={monthString}
316 {...css(
317 isHorizontal && styles.CalendarMonthGrid_month__horizontal,
318 hideForAnimation && styles.CalendarMonthGrid_month__hideForAnimation,
319 showForAnimation && !isVertical && !isRTL && {
320 position: 'absolute',
321 left: -calendarMonthWidth,
322 },
323 showForAnimation && !isVertical && isRTL && {
324 position: 'absolute',
325 right: 0,
326 },
327 showForAnimation && isVertical && {
328 position: 'absolute',
329 top: -translationValue,
330 },
331 !isVisible && !isAnimating && styles.CalendarMonthGrid_month__hidden,
332 )}
333 >
334 <CalendarMonth
335 month={month}
336 isVisible={isVisible}
337 enableOutsideDays={enableOutsideDays}
338 modifiers={modifiers[monthString]}
339 monthFormat={monthFormat}
340 orientation={orientation}
341 onDayMouseEnter={onDayMouseEnter}
342 onDayMouseLeave={onDayMouseLeave}
343 onDayClick={onDayClick}
344 onMonthSelect={this.onMonthSelect}
345 onYearSelect={this.onYearSelect}
346 renderMonthText={renderMonthText}
347 renderCalendarDay={renderCalendarDay}
348 renderDayContents={renderDayContents}
349 renderMonthElement={renderMonthElement}
350 firstDayOfWeek={firstDayOfWeek}
351 daySize={daySize}
352 focusedDate={isVisible ? focusedDate : null}
353 isFocused={isFocused}
354 phrases={phrases}
355 setMonthTitleHeight={setMonthTitleHeight}
356 dayAriaLabelFormat={dayAriaLabelFormat}
357 verticalBorderSpacing={verticalBorderSpacing}
358 horizontalMonthPadding={horizontalMonthPadding}
359 />
360 </div>
361 );
362 })}
363 </div>
364 );
365 }
366}
367
368CalendarMonthGrid.propTypes = propTypes;
369CalendarMonthGrid.defaultProps = defaultProps;
370
371export default withStyles(({
372 reactDates: {
373 color,
374 noScrollBarOnVerticalScrollable,
375 spacing,
376 zIndex,
377 },
378}) => ({
379 CalendarMonthGrid: {
380 background: color.background,
381 textAlign: noflip('left'),
382 zIndex,
383 },
384
385 CalendarMonthGrid__animating: {
386 zIndex: zIndex + 1,
387 },
388
389 CalendarMonthGrid__horizontal: {
390 position: 'absolute',
391 left: noflip(spacing.dayPickerHorizontalPadding),
392 },
393
394 CalendarMonthGrid__vertical: {
395 margin: '0 auto',
396 },
397
398 CalendarMonthGrid__vertical_scrollable: {
399 margin: '0 auto',
400 overflowY: 'scroll',
401 ...(noScrollBarOnVerticalScrollable && {
402 '-webkitOverflowScrolling': 'touch',
403 '::-webkit-scrollbar': {
404 '-webkit-appearance': 'none',
405 display: 'none',
406 },
407 }),
408 },
409
410 CalendarMonthGrid_month__horizontal: {
411 display: 'inline-block',
412 verticalAlign: 'top',
413 minHeight: '100%',
414 },
415
416 CalendarMonthGrid_month__hideForAnimation: {
417 position: 'absolute',
418 zIndex: zIndex - 1,
419 opacity: 0,
420 pointerEvents: 'none',
421 },
422
423 CalendarMonthGrid_month__hidden: {
424 visibility: 'hidden',
425 },
426}), { pureComponent: typeof React.PureComponent !== 'undefined' })(CalendarMonthGrid);