/* eslint react/no-array-index-key: 0 */
import React from 'react';
import PropTypes from 'prop-types';
import momentPropTypes from 'react-moment-proptypes';
import { forbidExtraProps, mutuallyExclusiveProps, nonNegativeInteger } from 'airbnb-prop-types';
import { css, withStyles, withStylesPropTypes } from 'react-with-styles';
import moment from 'moment';
import { CalendarDayPhrases } from '../defaultPhrases';
import getPhrasePropTypes from '../utils/getPhrasePropTypes';
import CalendarWeek from './CalendarWeek';
import CalendarDay from './CalendarDay';
import calculateDimension from '../utils/calculateDimension';
import getCalendarMonthWeeks from '../utils/getCalendarMonthWeeks';
import isSameDay from '../utils/isSameDay';
import toISODateString from '../utils/toISODateString';
import ModifiersShape from '../shapes/ModifiersShape';
import ScrollableOrientationShape from '../shapes/ScrollableOrientationShape';
import DayOfWeekShape from '../shapes/DayOfWeekShape';
import {
HORIZONTAL_ORIENTATION,
VERTICAL_SCROLLABLE,
DAY_SIZE,
} from '../constants';
const propTypes = forbidExtraProps({
...withStylesPropTypes,
month: momentPropTypes.momentObj,
horizontalMonthPadding: nonNegativeInteger,
isVisible: PropTypes.bool,
enableOutsideDays: PropTypes.bool,
modifiers: PropTypes.objectOf(ModifiersShape),
orientation: ScrollableOrientationShape,
daySize: nonNegativeInteger,
onDayClick: PropTypes.func,
onDayMouseEnter: PropTypes.func,
onDayMouseLeave: PropTypes.func,
onMonthSelect: PropTypes.func,
onYearSelect: PropTypes.func,
renderMonthText: mutuallyExclusiveProps(PropTypes.func, 'renderMonthText', 'renderMonthElement'),
renderCalendarDay: PropTypes.func,
renderDayContents: PropTypes.func,
renderMonthElement: mutuallyExclusiveProps(PropTypes.func, 'renderMonthText', 'renderMonthElement'),
firstDayOfWeek: DayOfWeekShape,
setMonthTitleHeight: PropTypes.func,
verticalBorderSpacing: nonNegativeInteger,
focusedDate: momentPropTypes.momentObj, // indicates focusable day
isFocused: PropTypes.bool, // indicates whether or not to move focus to focusable day
// i18n
monthFormat: PropTypes.string,
phrases: PropTypes.shape(getPhrasePropTypes(CalendarDayPhrases)),
dayAriaLabelFormat: PropTypes.string,
});
const defaultProps = {
month: moment(),
horizontalMonthPadding: 13,
isVisible: true,
enableOutsideDays: false,
modifiers: {},
orientation: HORIZONTAL_ORIENTATION,
daySize: DAY_SIZE,
onDayClick() {},
onDayMouseEnter() {},
onDayMouseLeave() {},
onMonthSelect() {},
onYearSelect() {},
renderMonthText: null,
renderCalendarDay: (props) => (),
renderDayContents: null,
renderMonthElement: null,
firstDayOfWeek: null,
setMonthTitleHeight: null,
focusedDate: null,
isFocused: false,
// i18n
monthFormat: 'MMMM YYYY', // english locale
phrases: CalendarDayPhrases,
dayAriaLabelFormat: undefined,
verticalBorderSpacing: undefined,
};
class CalendarMonth extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
weeks: getCalendarMonthWeeks(
props.month,
props.enableOutsideDays,
props.firstDayOfWeek == null ? moment.localeData().firstDayOfWeek() : props.firstDayOfWeek,
),
};
this.setCaptionRef = this.setCaptionRef.bind(this);
this.setMonthTitleHeight = this.setMonthTitleHeight.bind(this);
}
componentDidMount() {
this.setMonthTitleHeightTimeout = setTimeout(this.setMonthTitleHeight, 0);
}
componentWillReceiveProps(nextProps) {
const { month, enableOutsideDays, firstDayOfWeek } = nextProps;
const {
month: prevMonth,
enableOutsideDays: prevEnableOutsideDays,
firstDayOfWeek: prevFirstDayOfWeek,
} = this.props;
if (
!month.isSame(prevMonth)
|| enableOutsideDays !== prevEnableOutsideDays
|| firstDayOfWeek !== prevFirstDayOfWeek
) {
this.setState({
weeks: getCalendarMonthWeeks(
month,
enableOutsideDays,
firstDayOfWeek == null ? moment.localeData().firstDayOfWeek() : firstDayOfWeek,
),
});
}
}
componentWillUnmount() {
if (this.setMonthTitleHeightTimeout) {
clearTimeout(this.setMonthTitleHeightTimeout);
}
}
setMonthTitleHeight() {
const { setMonthTitleHeight } = this.props;
if (setMonthTitleHeight) {
const captionHeight = calculateDimension(this.captionRef, 'height', true, true);
setMonthTitleHeight(captionHeight);
}
}
setCaptionRef(ref) {
this.captionRef = ref;
}
render() {
const {
dayAriaLabelFormat,
daySize,
focusedDate,
horizontalMonthPadding,
isFocused,
isVisible,
modifiers,
month,
monthFormat,
onDayClick,
onDayMouseEnter,
onDayMouseLeave,
onMonthSelect,
onYearSelect,
orientation,
phrases,
renderCalendarDay,
renderDayContents,
renderMonthElement,
renderMonthText,
styles,
verticalBorderSpacing,
} = this.props;
const { weeks } = this.state;
const monthTitle = renderMonthText ? renderMonthText(month) : month.format(monthFormat);
const verticalScrollable = orientation === VERTICAL_SCROLLABLE;
return (
{renderMonthElement ? (
renderMonthElement({
month,
onMonthSelect,
onYearSelect,
isVisible,
})
) : (
{monthTitle}
)}
{weeks.map((week, i) => (
{week.map((day, dayOfWeek) => renderCalendarDay({
key: dayOfWeek,
day,
daySize,
isOutsideDay: !day || day.month() !== month.month(),
tabIndex: isVisible && isSameDay(day, focusedDate) ? 0 : -1,
isFocused,
onDayMouseEnter,
onDayMouseLeave,
onDayClick,
renderDayContents,
phrases,
modifiers: modifiers[toISODateString(day)],
ariaLabelFormat: dayAriaLabelFormat,
}))}
))}
);
}
}
CalendarMonth.propTypes = propTypes;
CalendarMonth.defaultProps = defaultProps;
export default withStyles(({ reactDates: { color, font, spacing } }) => ({
CalendarMonth: {
background: color.background,
textAlign: 'center',
verticalAlign: 'top',
userSelect: 'none',
},
CalendarMonth_table: {
borderCollapse: 'collapse',
borderSpacing: 0,
},
CalendarMonth_verticalSpacing: {
borderCollapse: 'separate',
},
CalendarMonth_caption: {
color: color.text,
fontSize: font.captionSize,
textAlign: 'center',
paddingTop: spacing.captionPaddingTop,
paddingBottom: spacing.captionPaddingBottom,
captionSide: 'initial',
},
CalendarMonth_caption__verticalScrollable: {
paddingTop: 12,
paddingBottom: 7,
},
}), { pureComponent: typeof React.PureComponent !== 'undefined' })(CalendarMonth);