UNPKG

@wix/design-system

Version:

@wix/design-system

317 lines 14.1 kB
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