react-dates
Version:
A responsive and accessible date range picker component built with React
362 lines (316 loc) • 11 kB
JSX
import React from 'react';
import PropTypes from 'prop-types';
import shallowCompare from 'react-addons-shallow-compare';
import momentPropTypes from 'react-moment-proptypes';
import { forbidExtraProps, nonNegativeInteger } from 'airbnb-prop-types';
import { css, withStyles, withStylesPropTypes } from 'react-with-styles';
import moment from 'moment';
import { addEventListener } from 'consolidated-events';
import { CalendarDayPhrases } from '../defaultPhrases';
import getPhrasePropTypes from '../utils/getPhrasePropTypes';
import CalendarMonth from './CalendarMonth';
import isTransitionEndSupported from '../utils/isTransitionEndSupported';
import getTransformStyles from '../utils/getTransformStyles';
import getCalendarMonthWidth from '../utils/getCalendarMonthWidth';
import toISOMonthString from '../utils/toISOMonthString';
import isAfterDay from '../utils/isAfterDay';
import ScrollableOrientationShape from '../shapes/ScrollableOrientationShape';
import DayOfWeekShape from '../shapes/DayOfWeekShape';
import {
HORIZONTAL_ORIENTATION,
VERTICAL_ORIENTATION,
VERTICAL_SCROLLABLE,
DAY_SIZE,
} from '../constants';
const propTypes = forbidExtraProps({
...withStylesPropTypes,
enableOutsideDays: PropTypes.bool,
firstVisibleMonthIndex: PropTypes.number,
initialMonth: momentPropTypes.momentObj,
isAnimating: PropTypes.bool,
numberOfMonths: PropTypes.number,
modifiers: PropTypes.object,
orientation: ScrollableOrientationShape,
onDayClick: PropTypes.func,
onDayMouseEnter: PropTypes.func,
onDayMouseLeave: PropTypes.func,
onMonthTransitionEnd: PropTypes.func,
renderMonth: PropTypes.func,
renderDay: PropTypes.func,
transformValue: PropTypes.string,
daySize: nonNegativeInteger,
focusedDate: momentPropTypes.momentObj, // indicates focusable day
isFocused: PropTypes.bool, // indicates whether or not to move focus to focusable day
firstDayOfWeek: DayOfWeekShape,
setCalendarMonthHeights: PropTypes.func,
isRTL: PropTypes.bool,
// i18n
monthFormat: PropTypes.string,
phrases: PropTypes.shape(getPhrasePropTypes(CalendarDayPhrases)),
dayAriaLabelFormat: PropTypes.string,
});
const defaultProps = {
enableOutsideDays: false,
firstVisibleMonthIndex: 0,
initialMonth: moment(),
isAnimating: false,
numberOfMonths: 1,
modifiers: {},
orientation: HORIZONTAL_ORIENTATION,
onDayClick() {},
onDayMouseEnter() {},
onDayMouseLeave() {},
onMonthTransitionEnd() {},
renderMonth: null,
renderDay: null,
transformValue: 'none',
daySize: DAY_SIZE,
focusedDate: null,
isFocused: false,
firstDayOfWeek: null,
setCalendarMonthHeights() {},
isRTL: false,
// i18n
monthFormat: 'MMMM YYYY', // english locale
phrases: CalendarDayPhrases,
};
function getMonths(initialMonth, numberOfMonths, withoutTransitionMonths) {
let month = initialMonth.clone();
if (!withoutTransitionMonths) month = month.subtract(1, 'month');
const months = [];
for (let i = 0; i < (withoutTransitionMonths ? numberOfMonths : numberOfMonths + 2); i += 1) {
months.push(month);
month = month.clone().add(1, 'month');
}
return months;
}
class CalendarMonthGrid extends React.Component {
constructor(props) {
super(props);
const withoutTransitionMonths = props.orientation === VERTICAL_SCROLLABLE;
this.state = {
months: getMonths(props.initialMonth, props.numberOfMonths, withoutTransitionMonths),
};
this.calendarMonthHeights = [];
this.isTransitionEndSupported = isTransitionEndSupported();
this.onTransitionEnd = this.onTransitionEnd.bind(this);
this.setContainerRef = this.setContainerRef.bind(this);
this.locale = moment.locale();
}
componentDidMount() {
const { setCalendarMonthHeights } = this.props;
this.removeEventListener = addEventListener(
this.container,
'transitionend',
this.onTransitionEnd,
);
this.setCalendarMonthHeightsTimeout = setTimeout(() => {
setCalendarMonthHeights(this.calendarMonthHeights);
}, 0);
}
componentWillReceiveProps(nextProps) {
const { initialMonth, numberOfMonths, orientation } = nextProps;
const { months } = this.state;
const hasMonthChanged = !this.props.initialMonth.isSame(initialMonth, 'month');
const hasNumberOfMonthsChanged = this.props.numberOfMonths !== numberOfMonths;
let newMonths = months;
if (hasMonthChanged && !hasNumberOfMonthsChanged) {
if (isAfterDay(initialMonth, this.props.initialMonth)) {
newMonths = months.slice(1);
newMonths.push(months[months.length - 1].clone().add(1, 'month'));
} else {
newMonths = months.slice(0, months.length - 1);
newMonths.unshift(months[0].clone().subtract(1, 'month'));
}
}
if (hasNumberOfMonthsChanged) {
const withoutTransitionMonths = orientation === VERTICAL_SCROLLABLE;
newMonths = getMonths(initialMonth, numberOfMonths, withoutTransitionMonths);
}
const momentLocale = moment.locale();
if (this.locale !== momentLocale) {
this.locale = momentLocale;
newMonths = newMonths.map(m => m.locale(this.locale));
}
this.setState({
months: newMonths,
});
}
shouldComponentUpdate(nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
}
componentDidUpdate(prevProps) {
const { isAnimating, onMonthTransitionEnd, setCalendarMonthHeights } = this.props;
// For IE9, immediately call onMonthTransitionEnd instead of
// waiting for the animation to complete
if (!this.isTransitionEndSupported && isAnimating) {
onMonthTransitionEnd();
}
if (!isAnimating && prevProps.isAnimating) {
this.setCalendarMonthHeightsTimeout = setTimeout(() => {
setCalendarMonthHeights(this.calendarMonthHeights);
}, 0);
}
}
componentWillUnmount() {
if (this.removeEventListener) this.removeEventListener();
if (this.setCalendarMonthHeightsTimeout) {
clearTimeout(this.setCalendarMonthHeightsTimeout);
}
}
onTransitionEnd() {
this.props.onMonthTransitionEnd();
}
setContainerRef(ref) {
this.container = ref;
}
setMonthHeight(height, i) {
if (this.calendarMonthHeights[i]) {
if (i === 0) {
this.calendarMonthHeights = [height].concat(this.calendarMonthHeights.slice(0, -1));
} else if (i === this.calendarMonthHeights.length - 1) {
this.calendarMonthHeights = this.calendarMonthHeights.slice(1).concat(height);
}
} else {
this.calendarMonthHeights[i] = height;
}
}
render() {
const {
enableOutsideDays,
firstVisibleMonthIndex,
isAnimating,
modifiers,
numberOfMonths,
monthFormat,
orientation,
transformValue,
daySize,
onDayMouseEnter,
onDayMouseLeave,
onDayClick,
renderMonth,
renderDay,
onMonthTransitionEnd,
firstDayOfWeek,
focusedDate,
isFocused,
isRTL,
styles,
phrases,
dayAriaLabelFormat,
} = this.props;
const { months } = this.state;
const isVertical = orientation === VERTICAL_ORIENTATION;
const isVerticalScrollable = orientation === VERTICAL_SCROLLABLE;
const isHorizontal = orientation === HORIZONTAL_ORIENTATION;
const calendarMonthWidth = getCalendarMonthWidth(daySize);
const width = isVertical || isVerticalScrollable ?
calendarMonthWidth :
(numberOfMonths + 2) * calendarMonthWidth;
return (
<div
{...css(
styles.CalendarMonthGrid,
isHorizontal && styles.CalendarMonthGrid__horizontal,
isVertical && styles.CalendarMonthGrid__vertical,
isVerticalScrollable && styles.CalendarMonthGrid__vertical_scrollable,
isAnimating && styles.CalendarMonthGrid__animating,
isAnimating && {
transition: 'transform 0.2s ease-in-out',
},
{
...getTransformStyles(transformValue),
width,
},
)}
ref={this.setContainerRef}
onTransitionEnd={onMonthTransitionEnd}
>
{months.map((month, i) => {
const isVisible = (i >= firstVisibleMonthIndex)
&& (i < firstVisibleMonthIndex + numberOfMonths);
const hideForAnimation = i === 0 && !isVisible;
const showForAnimation = i === 0 && isAnimating && isVisible;
const monthString = toISOMonthString(month);
return (
<div
key={monthString}
{...css(
isHorizontal && styles.CalendarMonthGrid_month__horizontal,
hideForAnimation && styles.CalendarMonthGrid_month__hideForAnimation,
showForAnimation && !isVertical && !isRTL && {
position: 'absolute',
left: -calendarMonthWidth,
},
showForAnimation && !isVertical && isRTL && {
position: 'absolute',
right: 0,
},
showForAnimation && isVertical && {
position: 'absolute',
top: -this.calendarMonthHeights[0],
},
)}
>
<CalendarMonth
month={month}
isVisible={isVisible}
enableOutsideDays={enableOutsideDays}
modifiers={modifiers[monthString]}
monthFormat={monthFormat}
orientation={orientation}
onDayMouseEnter={onDayMouseEnter}
onDayMouseLeave={onDayMouseLeave}
onDayClick={onDayClick}
renderMonth={renderMonth}
renderDay={renderDay}
firstDayOfWeek={firstDayOfWeek}
daySize={daySize}
focusedDate={isVisible ? focusedDate : null}
isFocused={isFocused}
phrases={phrases}
setMonthHeight={(height) => { this.setMonthHeight(height, i); }}
dayAriaLabelFormat={dayAriaLabelFormat}
/>
</div>
);
})}
</div>
);
}
}
CalendarMonthGrid.propTypes = propTypes;
CalendarMonthGrid.defaultProps = defaultProps;
export default withStyles(({ reactDates: { color, zIndex } }) => ({
CalendarMonthGrid: {
background: color.background,
textAlign: 'left',
zIndex,
},
CalendarMonthGrid__animating: {
zIndex: zIndex + 1,
},
CalendarMonthGrid__horizontal: {
position: 'absolute',
left: 9,
},
CalendarMonthGrid__vertical: {
margin: '0 auto',
},
CalendarMonthGrid__vertical_scrollable: {
margin: '0 auto',
overflowY: 'scroll',
},
CalendarMonthGrid_month__horizontal: {
display: 'inline-block',
verticalAlign: 'top',
minHeight: '100%',
},
CalendarMonthGrid_month__hideForAnimation: {
position: 'absolute',
zIndex: zIndex - 1,
opacity: 0,
pointerEvents: 'none',
},
}))(CalendarMonthGrid);