@wix/design-system
Version:
@wix/design-system
317 lines • 14.1 kB
JavaScript
import { st, classes, cssStates } from './BaseCalendar.st.css.js';
import React from 'react';
import PropTypes from 'prop-types';
import DayPicker from 'react-day-picker';
import localeUtilsFactory from '../../common/LocaleUtils/LocaleUtils';
import { WixStyleReactEnvironmentContext } from '../../WixStyleReactEnvironmentProvider/context';
import { SupportedWixLocales } from 'wix-design-systems-locale-utils';
class BaseCalendar extends React.PureComponent {
constructor() {
super(...arguments);
this._renderDay = (day, modifiers) => {
const { dateIndication } = this.props;
const isOutsideDay = !!modifiers[cssStates({ outside: true })];
const isSelectedDay = !!modifiers[cssStates({ selected: true })];
const dateIndicationNode = dateIndication &&
dateIndication({ date: day, isSelected: isSelectedDay });
const shouldHasIndication = dateIndicationNode && !isOutsideDay;
return (React.createElement("div", { className: st(classes.dayWrapper, {
hasIndication: shouldHasIndication,
}), "data-date": `${day.getFullYear()}-${day.getMonth()}-${day.getDate()}`, "data-outsideday": isOutsideDay },
React.createElement("div", { className: classes.dayText }, day.getDate()),
shouldHasIndication ? (React.createElement("div", { className: classes.dayIndicationContainer }, dateIndicationNode)) : null));
};
this._handleDayClick = (value, modifiers = {}, event = null) => {
this._preventActionEventDefault(event);
const propsValue = this.props.value || {};
const { onChange, shouldCloseOnSelect } = this.props;
const isNotRange = (value) => {
if (!('from' in value) || !value.from) {
return false;
}
if (!('to' in value) || !value.to) {
return false;
}
return true;
};
const isCompleteRange = (value) => {
if (!('from' in value) || !('to' in value)) {
return false;
}
return !!(value.from && value.to);
};
if (this.props.selectionMode === 'range') {
if (isNotRange(propsValue) || isCompleteRange(propsValue)) {
onChange({ from: value }, modifiers);
}
else {
/** Previous `if` checks that both are missing or both are present. At least one should be here */
const anchor = (propsValue.from || propsValue.to);
const newVal = anchor < value
? { from: anchor, to: value }
: { from: value, to: anchor };
onChange(newVal, modifiers);
shouldCloseOnSelect && this.props.onClose(event);
}
}
else {
onChange(value, modifiers);
shouldCloseOnSelect && this.props.onClose(event);
}
};
this._preventActionEventDefault = (event = null) => {
if (event &&
(!('key' in event) || (event.key !== 'Escape' && event.key !== 'Tab'))) {
event.preventDefault();
}
};
this._createWeekdayElement = (localeUtils) => {
return ({ className, weekday, }) => {
const weekdayShort = localeUtils.formatWeekdayShort(weekday);
const weekdayLong = localeUtils.formatWeekdayLong(weekday);
return (React.createElement("div", { className: className, "aria-label": weekdayLong, role: "columnheader" },
React.createElement("abbr", { "data-hook": "weekday-day" }, weekdayShort)));
};
};
this._createDayPickerProps = () => {
const { filterDate, excludePastDates, numOfMonths, firstDayOfWeek, rtl, today, onDisplayedViewChange, displayedMonth, captionElement, allowSelectingOutsideDays, size, } = this.props;
const locale = this._getLocale();
const value = BaseCalendar.parseValue(this.props.value);
const localeUtils = localeUtilsFactory(locale, firstDayOfWeek);
const { from, to } = value instanceof Date ? { from: undefined, to: undefined } : value;
const singleDay = !from && !to && value;
const firstOfMonth = [
new Date(displayedMonth.getFullYear(), displayedMonth.getMonth(), 1),
new Date(displayedMonth.getFullYear(), displayedMonth.getMonth() + 1, 1),
];
const lastOfMonth = [
new Date(displayedMonth.getFullYear(), displayedMonth.getMonth() + 1, 0),
new Date(displayedMonth.getFullYear(), displayedMonth.getMonth() + 2, 0),
];
const selectedDays = this._getSelectedDays(value);
const weekdayElement = this._createWeekdayElement(localeUtils);
const modifiers = {
[cssStates({ start: true })]: from,
[cssStates({ end: true })]: to,
[cssStates({ firstOfMonth: true })]: firstOfMonth,
[cssStates({ lastOfMonth: true })]: lastOfMonth,
[cssStates({ singleDay: true })]: singleDay,
...this.props.modifiers,
};
if (today) {
/** parseValue always returns Date if you give it a Date */
modifiers[cssStates({ today: true })] = BaseCalendar.parseValue(today);
}
// We must add the dummy state since ReactDayPicker use it as a selector in their code
const outsideCssState = allowSelectingOutsideDays
? cssStates({ dummyOutside: true })
: cssStates({ outside: true });
return {
disabledDays: [
(date) => !filterDate(new Date(date)),
excludePastDates ? { before: new Date() } : {},
],
initialMonth: displayedMonth,
initialYear: displayedMonth,
selectedDays,
month: displayedMonth,
year: displayedMonth,
locale: typeof locale === 'string' ? locale : '',
fixedWeeks: true,
onKeyDown: this._handleKeyDown,
onDayClick: this._handleDayClick,
// @ts-expect-error Missing props probably aren't used
localeUtils,
navbarElement: () => null,
captionElement,
// @ts-expect-error Probably a live bug or there's no caption to click
onCaptionClick: this._preventActionEventDefault,
onDayKeyDown: this._handleDayKeyDown,
numberOfMonths: numOfMonths,
modifiers,
renderDay: this._renderDay,
dir: rtl ? 'rtl' : 'ltr',
tabIndex: 0,
weekdayElement,
classNames: {
/* The classes: 'DayPicker', 'DayPicker-wrapper', 'DayPicker-Month', 'DayPicker-Day', 'disabled'
are used as selectors for the elements at the drivers and at the e2e tests */
container: st(classes.container, { size }, 'DayPicker'),
wrapper: 'DayPicker-wrapper',
interactionDisabled: 'DayPicker--interactionDisabled',
months: st(classes.months, { twoMonths: numOfMonths > 1 }),
month: st(classes.month, { size }, 'DayPicker-Month'),
weekdays: classes.weekdays,
weekdaysRow: classes.weekdaysRow,
weekday: st(classes.weekday, { size }),
body: st(classes.body, { size }),
week: classes.week,
weekNumber: 'DayPicker-WeekNumber',
day: st(classes.day, { size }, 'DayPicker-Day'),
// default modifiers
today: cssStates({ today: !today }),
selected: cssStates({ selected: true }),
disabled: st('disabled', cssStates({ disabled: true })),
outside: outsideCssState,
},
onMonthChange: onDisplayedViewChange,
};
};
this._handleKeyDown = (event) => {
const { onKeyDown } = this.props;
if (onKeyDown) {
onKeyDown(event);
return;
}
if (event.key === 'Escape') {
this.props.onClose(event);
}
};
this._toggleFirstDayTabIndex = (tabIndex) => {
const firstDay = this._getDayPicker().dayPicker.querySelector(`.DayPicker-Day[tabindex="${tabIndex}"]`);
if (!firstDay) {
throw new Error('<BaseCalendar /> No first day found in day picker');
}
firstDay.tabIndex = tabIndex === 0 ? -1 : 0;
};
this._focusSelectedDay = () => {
if (this.dayPickerRef) {
const selectedDay = this.dayPickerRef.dayPicker.querySelector(`.${cssStates({ selected: true })}`);
if (selectedDay) {
// The 'unfocused' class is used as a selector at the drivers and e2e test
selectedDay.classList.add(cssStates({ unfocused: true }), 'unfocused');
selectedDay.tabIndex = 0;
selectedDay.focus();
this._toggleFirstDayTabIndex(0);
}
else {
this._toggleFirstDayTabIndex(-1);
}
}
};
this._handleDayKeyDown = (_value, _modifiers, event = null) => {
this._preventActionEventDefault(event);
const unfocusedDay = this._getDayPicker().dayPicker.querySelector(`.${cssStates({ unfocused: true })}`);
if (unfocusedDay) {
// The 'unfocused' class is used as a selector at the drivers and e2e test
unfocusedDay.classList.remove(cssStates({ unfocused: true }), 'unfocused');
}
};
}
_getSelectedDays(value) {
const { from, to } = !value || value instanceof Date
? { from: undefined, to: undefined }
: value;
if (from && to) {
return { from, to };
}
else if (from) {
return { after: BaseCalendar.prevDay(from) };
}
else if (to) {
return { before: BaseCalendar.nextDay(to) };
}
else {
// Single day OR empty value
return value;
}
}
_getDayPicker() {
if (!this.dayPickerRef) {
throw new Error(`<BaseCalendar /> didn't have a day picker set`);
}
return this.dayPickerRef;
}
_getLocale() {
return this.props.locale || this.context.locale || 'en';
}
componentDidMount() {
this.props.autoFocus && this._focusSelectedDay();
if (this.dayPickerRef) {
// @ts-expect-error wrapper is badly declared on DayPicker class
this.dayPickerRef.wrapper.tabIndex = -1;
}
}
componentDidUpdate(prevProps) {
if (!prevProps.autoFocus && this.props.autoFocus) {
this._focusSelectedDay();
}
}
render() {
const { dataHook, className, size } = this.props;
return (React.createElement("div", { "data-hook": dataHook, "data-size": size, className: st(classes.root, className), onClick: this._preventActionEventDefault, role: "dialog", tabIndex: -1 },
React.createElement(DayPicker, { ref: ref => (this.dayPickerRef = ref), ...this._createDayPickerProps() })));
}
}
BaseCalendar.displayName = 'BaseCalendar';
BaseCalendar.defaultProps = {
className: '',
filterDate: () => true,
dateIndication: () => null,
shouldCloseOnSelect: true,
onClose: () => { },
autoFocus: true,
excludePastDates: false,
selectionMode: 'day',
numOfMonths: 1,
size: 'medium',
allowSelectingOutsideDays: false,
};
BaseCalendar.propTypes = {
dataHook: PropTypes.string,
autoFocus: PropTypes.bool,
numOfMonths: PropTypes.oneOf([1, 2]),
firstDayOfWeek: PropTypes.oneOf([0, 1, 2, 3, 4, 5, 6]),
className: PropTypes.string,
onChange: PropTypes.func.isRequired,
onClose: PropTypes.func,
onKeyDown: PropTypes.func,
excludePastDates: PropTypes.bool,
filterDate: PropTypes.func,
value: PropTypes.oneOfType([
PropTypes.instanceOf(Date),
PropTypes.shape({
from: PropTypes.instanceOf(Date),
to: PropTypes.instanceOf(Date),
}),
]),
selectionMode: PropTypes.oneOf(['day', 'range']),
shouldCloseOnSelect: PropTypes.bool,
locale: PropTypes.oneOfType([PropTypes.oneOf(SupportedWixLocales)]),
rtl: PropTypes.bool,
dateIndication: PropTypes.func,
today: PropTypes.instanceOf(Date),
displayedMonth: PropTypes.instanceOf(Date).isRequired,
onDisplayedViewChange: PropTypes.func.isRequired,
captionElement: PropTypes.node.isRequired,
modifiers: PropTypes.object,
allowSelectingOutsideDays: PropTypes.bool,
};
/** Return a value in which all string-dates are parsed into Date objects */
BaseCalendar.parseValue = (value) => {
if (!value) {
return new Date();
}
if (value instanceof Date) {
return value;
}
else {
return {
from: value.from,
to: value.to,
};
}
};
BaseCalendar.nextDay = (date) => {
const day = new Date(date);
day.setDate(day.getDate() + 1);
return day;
};
BaseCalendar.prevDay = (date) => {
const day = new Date(date);
day.setDate(day.getDate() - 1);
return day;
};
export default BaseCalendar;
BaseCalendar.contextType = WixStyleReactEnvironmentContext;
//# sourceMappingURL=BaseCalendar.js.map