wix-style-react
Version:
380 lines (316 loc) • 12.1 kB
JavaScript
import { st, classes } from './Calendar.st.css';
import React from 'react';
import PropTypes from 'prop-types';
import BaseCalendar from './BaseCalendar/BaseCalendar';
import addMonths from 'date-fns/addMonths';
import subMonths from 'date-fns/subMonths';
import startOfMonth from 'date-fns/startOfMonth';
import isSameDay from 'date-fns/isSameDay';
import { CalendarView } from './utils';
import localeUtilsFactory from '../common/LocaleUtils/LocaleUtils';
// todo: remove it in the next major version.
// This file is used for backward compatibility. Since the `locale` prop allows passing `date-fns locale object` as value we still need to support it!
import dateFnsLocaleUtilsFactory from '../common/LocaleUtils/DateFnsLocaleUtils';
import deprecationLog from '../utils/deprecationLog';
import DatePickerHead from './DatePickerHead';
import { WixStyleReactEnvironmentContext } from '../WixStyleReactEnvironmentProvider/context';
import { supportedWixlocales } from 'wix-design-systems-locale-utils';
export default class Calendar extends React.PureComponent {
static displayName = 'Calendar';
static defaultProps = {
className: '',
filterDate: () => true,
dateIndication: () => null,
shouldCloseOnSelect: true,
onClose: () => {},
autoFocus: true,
excludePastDates: false,
selectionMode: 'day',
showMonthDropdown: false,
showYearDropdown: false,
numOfMonths: 1,
};
constructor(props) {
super(props);
const initialMonth = Calendar.getUpdatedMonth(
props.value,
props.numOfMonths,
);
this.state = {
month: initialMonth || new Date(),
};
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.value !== this.props.value) {
const month = Calendar.getUpdatedMonth(
nextProps.value,
nextProps.numOfMonths,
this.state.month,
);
if (month) {
this.setState({ month });
}
}
}
static areValuesEqual(value1 = {}, value2 = {}) {
if (!Boolean(value1) && !Boolean(value2)) {
return true;
}
if (Calendar.isRangeValue(value1) && Calendar.isRangeValue(value2)) {
return (
isSameDay(value1.from, value2.from) && isSameDay(value1.to, value2.to)
);
}
return isSameDay(value1, value2);
}
static optionalParse = BaseCalendar.optionalParse;
/** Return a value in which all string-dates are parsed into Date objects */
static parseValue = BaseCalendar.parseValue;
static nextDay = BaseCalendar.nextDay;
static prevDay = BaseCalendar.prevDay;
static isSingleDay(value) {
return value instanceof Date;
}
static isRangeValue(value) {
return Boolean(value.from || value.to);
}
static getUpdatedMonth = (nextPropsValue, numOfMonths, currentMonthDate) => {
const nextValue = Calendar.parseValue(nextPropsValue);
if (!currentMonthDate) {
return Calendar.isSingleDay(nextValue)
? nextValue
: nextValue.from || nextValue.to;
}
const view = new CalendarView(currentMonthDate, numOfMonths);
if (Calendar.isSingleDay(nextValue)) {
if (!view.isContained(nextValue)) {
return nextValue;
}
} else {
const { from, to } = nextValue;
if (from && view.isAfterView(from)) {
// F--- => F---
// VVVVV => VVVVV
return from;
} else if (to && view.isBeforeView(to)) {
if (view.isRangeFits(from, to)) {
// F-T => F-T
// VVVVV => VVVVV
return from;
} else {
// F-----T => F-----T
// VVVVV => VVVVV
return subMonths(to, numOfMonths - 1);
}
} else if (
from &&
view.isBeforeView(from) &&
to &&
view.isAfterView(to)
) {
// F-------T => F-------T
// VVVVV => VVVVV
return from; // choose the 'from' anchor arbitrarly
}
}
/*
* We only changed the month if the day (or range.edges) are outside the view.
* This is to avoid changing the month right after a user clicks on the calendar.
*/
return null;
};
_setMonth = month => {
this.setState({ month });
const { onMonthChange } = this.props;
if (onMonthChange) {
onMonthChange(month);
}
};
_getLocaleUtilsFactory = (locale, firstDayOfWeek) => {
// The `dateFnsLocaleUtilsFactory` is used for backward compatibility.
// In case that the user passes an `date-fns locale object` we need to use our old `LocaleUtilsFactory`.
return typeof locale === 'string'
? localeUtilsFactory(locale, firstDayOfWeek)
: dateFnsLocaleUtilsFactory(locale, firstDayOfWeek);
};
_createCaptionElement = () => {
const {
showMonthDropdown,
showYearDropdown,
leftArrowAriaLabel,
leftArrowAriaLabelledBy,
rightArrowAriaLabel,
rightArrowAriaLabelledBy,
monthDropdownAriaLabel,
monthDropdownAriaLabelledBy,
yearDropdownAriaLabel,
yearDropdownAriaLabelledBy,
} = this.props;
const { month } = this.state;
const locale = this._getLocale();
const localeUtils = this._getLocaleUtilsFactory(locale);
return (
<DatePickerHead
{...{
className: classes.header,
date: month,
showYearDropdown,
showMonthDropdown,
locale: typeof locale === 'string' ? locale : '',
localeUtils,
onChange: this._setMonth,
onLeftArrowClick: () =>
this._setMonth(startOfMonth(addMonths(month, -1))),
onRightArrowClick: () =>
this._setMonth(startOfMonth(addMonths(month, 1))),
leftArrowAriaLabel,
leftArrowAriaLabelledBy,
rightArrowAriaLabel,
rightArrowAriaLabelledBy,
monthDropdownAriaLabel,
monthDropdownAriaLabelledBy,
yearDropdownAriaLabel,
yearDropdownAriaLabelledBy,
}}
/>
);
};
_getLocale() {
if (typeof this.props.locale !== 'string') {
deprecationLog(
'<Calendar/> prop "locale" with value `date-fns locale object` is deprecated and will be removed in next major release, please pass a string instead',
);
}
return this.props.locale || this.context.locale || 'en';
}
render() {
const {
dataHook,
className,
autoFocus,
numOfMonths,
firstDayOfWeek,
onChange,
onClose,
excludePastDates,
filterDate,
value,
selectionMode,
shouldCloseOnSelect,
locale,
rtl,
dateIndication,
today,
} = this.props;
const { month } = this.state;
return (
<BaseCalendar
dataHook={dataHook}
className={st(classes.root, className)}
autoFocus={autoFocus}
numOfMonths={numOfMonths}
firstDayOfWeek={firstDayOfWeek}
onChange={onChange}
onClose={onClose}
excludePastDates={excludePastDates}
filterDate={filterDate}
value={value}
selectionMode={selectionMode}
shouldCloseOnSelect={shouldCloseOnSelect}
locale={locale}
rtl={rtl}
dateIndication={dateIndication}
today={today}
onDisplayedViewChange={this._setMonth}
displayedMonth={month}
captionElement={this._createCaptionElement()}
/>
);
}
}
Calendar.contextType = WixStyleReactEnvironmentContext;
Calendar.propTypes = {
/** Applies as data-hook HTML attribute that can be used in the tests */
dataHook: PropTypes.string,
/** Focus selected day automatically when component mounts or updates */
autoFocus: PropTypes.bool,
/** Allows to display multiple months at once. Currently it shows 1 or 2 months only. */
numOfMonths: PropTypes.oneOf([1, 2]),
/** First day of the week, allowing only from 0 to 6 (Sunday to Saturday). The default value is 1 which means Monday. */
firstDayOfWeek: PropTypes.oneOf([0, 1, 2, 3, 4, 5, 6]),
/** A single CSS class name to be appended to the root element. */
className: PropTypes.string,
/** Provides a callback function when day in selected in the calendar */
onChange: PropTypes.func.isRequired,
/** Defines a callback function that is called with the date of the first day of the month whenever the user selects a month in the calendar */
onMonthChange: PropTypes.func,
/** Defines a callback function that is called whenever a user presses escape, clicks outside of the element or a date is selected and `shouldCloseOnSelect` is set. Receives an event as a first argument. */
onClose: PropTypes.func,
/** Specify whether past dates should be selectable or not */
excludePastDates: PropTypes.bool,
/**
* ##### Specify selectable dates:
* * `param` {Date} `date` - a date to check
* * `return` {boolean} - true if `date` should be selectable, false otherwise
*/
filterDate: PropTypes.func,
/** Defines the selected date */
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.instanceOf(Date),
PropTypes.shape({
from: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
to: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
}),
]),
/** Whether the user should be able to select a date range, or just a single day */
selectionMode: PropTypes.oneOf(['day', 'range']),
/** Displays a selectable yearDropdown */
showYearDropdown: PropTypes.bool,
/** Displays a selectable monthDropdown */
showMonthDropdown: PropTypes.bool,
/** Specify whether the calendar closes on day selection */
shouldCloseOnSelect: PropTypes.bool,
/** Specify date picker instance locale */
locale: PropTypes.oneOfType([
PropTypes.oneOf(supportedWixlocales),
PropTypes.shape({
code: PropTypes.string,
formatDistance: PropTypes.func,
formatRelative: PropTypes.func,
localize: PropTypes.object,
formatLong: PropTypes.object,
match: PropTypes.object,
options: PropTypes.object,
}),
]),
/** Specify whether RTL mode is enabled or not. When true, the keyboard navigation will be changed means pressing on the right arrow will navigate to the previous day, and pressing on the left arrow will navigate to the next day. */
rtl: PropTypes.bool,
/**
##### Add an indication under a specific date.
Function returns the indication node of a specific date or null if this day doesn't have an indication.
* - `param` {date: Date, isSelected: boolean }
* - `date` - a date
* - `isSelected` - whether this date is the selected date
* - `return` {React.node} - the indication node of a specific date or null if this day doesn't have an indication.
*/
dateIndication: PropTypes.func,
/** Sets today's date. The today indication is added automatically according to the user timezone but in some cases, we need the ability to add the today indication based on the business timezone. */
today: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
/** Defines a string value that labels the left arrow in calendar header */
leftArrowAriaLabel: PropTypes.string,
/** Identifies the element that labels the left arrow in calendar header */
leftArrowAriaLabelledBy: PropTypes.string,
/** Defines a string value that labels the right arrow in calendar header */
rightArrowAriaLabel: PropTypes.string,
/** Identifies the element that labels the right arrow in calendar header */
rightArrowAriaLabelledBy: PropTypes.string,
/** Defines a string value that labels the months dropdown in calendar header */
monthDropdownAriaLabel: PropTypes.string,
/** Identifies the element that labels the months dropdown in calendar header */
monthDropdownAriaLabelledBy: PropTypes.string,
/** Defines a string value that labels the years dropdown in calendar header */
yearDropdownAriaLabel: PropTypes.string,
/** Identifies the element that labels the years dropdown in calendar header */
yearDropdownAriaLabelledBy: PropTypes.string,
};