inital commit

This commit is contained in:
2026-01-01 15:25:19 +05:30
commit f0ae49465a
36361 changed files with 4894111 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
#### Example: Internationalization
```jsx inside Markdown
import * as locales from 'react-date-range/dist/locale';
import {useState} from 'react'
const nameMapper = {
ar: 'Arabic',
bg: 'Bulgarian',
ca: 'Catalan',
cs: 'Czech',
cy: 'Welsh',
da: 'Danish',
de: 'German',
el: 'Greek',
enGB: 'English (United Kingdom)',
enUS: 'English (United States)',
eo: 'Esperanto',
es: 'Spanish',
et: 'Estonian',
faIR: 'Persian',
fi: 'Finnish',
fil: 'Filipino',
fr: 'French',
hi: 'Hindi',
hr: 'Croatian',
hu: 'Hungarian',
hy: 'Armenian',
id: 'Indonesian',
is: 'Icelandic',
it: 'Italian',
ja: 'Japanese',
ka: 'Georgian',
ko: 'Korean',
lt: 'Lithuanian',
lv: 'Latvian',
mk: 'Macedonian',
nb: 'Norwegian Bokmål',
nl: 'Dutch',
pl: 'Polish',
pt: 'Portuguese',
ro: 'Romanian',
ru: 'Russian',
sk: 'Slovak',
sl: 'Slovenian',
sr: 'Serbian',
sv: 'Swedish',
th: 'Thai',
tr: 'Turkish',
uk: 'Ukrainian',
vi: 'Vietnamese',
zhCN: 'Chinese Simplified',
zhTW: 'Chinese Traditional'
};
const localeOptions = Object.keys(locales)
.map(key => ({
value: key,
label: `${key} - ${nameMapper[key] || ''}`
}))
.filter(item => nameMapper[item.value]);
const [locale, setLocale] = React.useState('ja');
const [date, setDate] = useState(null);
<div style={{ display: 'flex', flexFlow: 'column nowrap' }}>
<select
style={{ margin: '20px auto' }}
onChange={e => setLocale(e.target.value)}
value={locale}
>
{localeOptions.map((option, i) => (
<option value={option.value} key={i}>
{option.label}
</option>
))}
</select>
<Calendar onChange={item => setDate(item)}
locale={locales[locale]} date={date} />
</div>;
```

View File

@@ -0,0 +1,620 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { rangeShape } from '../DayCell';
import Month from '../Month';
import DateInput from '../DateInput';
import { calcFocusDate, generateStyles, getMonthDisplayRange } from '../../utils';
import classnames from 'classnames';
import ReactList from 'react-list';
import { shallowEqualObjects } from 'shallow-equal';
import {
addMonths,
subMonths,
format,
eachDayOfInterval,
startOfWeek,
endOfWeek,
isSameDay,
addYears,
setYear,
setMonth,
differenceInCalendarMonths,
startOfMonth,
endOfMonth,
addDays,
isSameMonth,
differenceInDays,
min,
max,
} from 'date-fns';
import { enUS as defaultLocale } from 'date-fns/locale/en-US';
import coreStyles from '../../styles';
import { ariaLabelsShape } from '../../accessibility';
class Calendar extends PureComponent {
constructor(props, context) {
super(props, context);
this.dateOptions = { locale: props.locale };
if (props.weekStartsOn !== undefined) this.dateOptions.weekStartsOn = props.weekStartsOn;
this.styles = generateStyles([coreStyles, props.classNames]);
this.listSizeCache = {};
this.isFirstRender = true;
this.state = {
monthNames: this.getMonthNames(),
focusedDate: calcFocusDate(null, props),
drag: {
status: false,
range: { startDate: null, endDate: null },
disablePreview: false,
},
scrollArea: this.calcScrollArea(props),
};
}
getMonthNames() {
return [...Array(12).keys()].map(i => this.props.locale.localize.month(i));
}
calcScrollArea(props) {
const { direction, months, scroll } = props;
if (!scroll.enabled) return { enabled: false };
const longMonthHeight = scroll.longMonthHeight || scroll.monthHeight;
if (direction === 'vertical') {
return {
enabled: true,
monthHeight: scroll.monthHeight || 220,
longMonthHeight: longMonthHeight || 260,
calendarWidth: 'auto',
calendarHeight: (scroll.calendarHeight || longMonthHeight || 240) * months,
};
}
return {
enabled: true,
monthWidth: scroll.monthWidth || 332,
calendarWidth: (scroll.calendarWidth || scroll.monthWidth || 332) * months,
monthHeight: longMonthHeight || 300,
calendarHeight: longMonthHeight || 300,
};
}
focusToDate = (date, props = this.props, preventUnnecessary = true) => {
if (!props.scroll.enabled) {
if (preventUnnecessary && props.preventSnapRefocus) {
const focusedDateDiff = differenceInCalendarMonths(date, this.state.focusedDate);
const isAllowedForward = props.calendarFocus === 'forwards' && focusedDateDiff >= 0;
const isAllowedBackward = props.calendarFocus === 'backwards' && focusedDateDiff <= 0;
if ((isAllowedForward || isAllowedBackward) && Math.abs(focusedDateDiff) < props.months) {
return;
}
}
this.setState({ focusedDate: date });
return;
}
const targetMonthIndex = differenceInCalendarMonths(date, props.minDate, this.dateOptions);
const visibleMonths = this.list.getVisibleRange();
if (preventUnnecessary && visibleMonths.includes(targetMonthIndex)) return;
this.isFirstRender = true;
this.list.scrollTo(targetMonthIndex);
this.setState({ focusedDate: date });
};
updateShownDate = (props = this.props) => {
const newProps = props.scroll.enabled
? {
...props,
months: this.list.getVisibleRange().length,
}
: props;
const newFocus = calcFocusDate(this.state.focusedDate, newProps);
this.focusToDate(newFocus, newProps);
};
updatePreview = val => {
if (!val) {
this.setState({ preview: null });
return;
}
const preview = {
startDate: val,
endDate: val,
color: this.props.color,
};
this.setState({ preview });
};
componentDidMount() {
if (this.props.scroll.enabled) {
// prevent react-list's initial render focus problem
setTimeout(() => this.focusToDate(this.state.focusedDate));
}
}
componentDidUpdate(prevProps) {
const propMapper = {
dateRange: 'ranges',
date: 'date',
};
const targetProp = propMapper[this.props.displayMode];
if (this.props[targetProp] !== prevProps[targetProp]) {
this.updateShownDate(this.props);
}
if (
prevProps.locale !== this.props.locale ||
prevProps.weekStartsOn !== this.props.weekStartsOn
) {
this.dateOptions = { locale: this.props.locale };
if (this.props.weekStartsOn !== undefined)
this.dateOptions.weekStartsOn = this.props.weekStartsOn;
this.setState({
monthNames: this.getMonthNames(),
});
}
if (!shallowEqualObjects(prevProps.scroll, this.props.scroll)) {
this.setState({ scrollArea: this.calcScrollArea(this.props) });
}
}
changeShownDate = (value, mode = 'set') => {
const { focusedDate } = this.state;
const { onShownDateChange, minDate, maxDate } = this.props;
const modeMapper = {
monthOffset: () => addMonths(focusedDate, value),
setMonth: () => setMonth(focusedDate, value),
setYear: () => setYear(focusedDate, value),
set: () => value,
};
const newDate = min([max([modeMapper[mode](), minDate]), maxDate]);
this.focusToDate(newDate, this.props, false);
onShownDateChange && onShownDateChange(newDate);
};
handleRangeFocusChange = (rangesIndex, rangeItemIndex) => {
this.props.onRangeFocusChange && this.props.onRangeFocusChange([rangesIndex, rangeItemIndex]);
};
handleScroll = () => {
const { onShownDateChange, minDate } = this.props;
const { focusedDate } = this.state;
const { isFirstRender } = this;
const visibleMonths = this.list.getVisibleRange();
// prevent scroll jump with wrong visible value
if (visibleMonths[0] === undefined) return;
const visibleMonth = addMonths(minDate, visibleMonths[0] || 0);
const isFocusedToDifferent = !isSameMonth(visibleMonth, focusedDate);
if (isFocusedToDifferent && !isFirstRender) {
this.setState({ focusedDate: visibleMonth });
onShownDateChange && onShownDateChange(visibleMonth);
}
this.isFirstRender = false;
};
renderMonthAndYear = (focusedDate, changeShownDate, props) => {
const { showMonthArrow, minDate, maxDate, showMonthAndYearPickers, ariaLabels } = props;
const upperYearLimit = (maxDate || Calendar.defaultProps.maxDate).getFullYear();
const lowerYearLimit = (minDate || Calendar.defaultProps.minDate).getFullYear();
const styles = this.styles;
return (
<div onMouseUp={e => e.stopPropagation()} className={styles.monthAndYearWrapper}>
{showMonthArrow ? (
<button
type="button"
className={classnames(styles.nextPrevButton, styles.prevButton)}
onClick={() => changeShownDate(-1, 'monthOffset')}
aria-label={ariaLabels.prevButton}>
<i />
</button>
) : null}
{showMonthAndYearPickers ? (
<span className={styles.monthAndYearPickers}>
<span className={styles.monthPicker}>
<select
value={focusedDate.getMonth()}
onChange={e => changeShownDate(e.target.value, 'setMonth')}
aria-label={ariaLabels.monthPicker}>
{this.state.monthNames.map((monthName, i) => (
<option key={i} value={i}>
{monthName}
</option>
))}
</select>
</span>
<span className={styles.monthAndYearDivider} />
<span className={styles.yearPicker}>
<select
value={focusedDate.getFullYear()}
onChange={e => changeShownDate(e.target.value, 'setYear')}
aria-label={ariaLabels.yearPicker}>
{new Array(upperYearLimit - lowerYearLimit + 1)
.fill(upperYearLimit)
.map((val, i) => {
const year = val - i;
return (
<option key={year} value={year}>
{year}
</option>
);
})}
</select>
</span>
</span>
) : (
<span className={styles.monthAndYearPickers}>
{this.state.monthNames[focusedDate.getMonth()]} {focusedDate.getFullYear()}
</span>
)}
{showMonthArrow ? (
<button
type="button"
className={classnames(styles.nextPrevButton, styles.nextButton)}
onClick={() => changeShownDate(+1, 'monthOffset')}
aria-label={ariaLabels.nextButton}>
<i />
</button>
) : null}
</div>
);
};
renderWeekdays() {
const now = new Date();
return (
<div className={this.styles.weekDays}>
{eachDayOfInterval({
start: startOfWeek(now, this.dateOptions),
end: endOfWeek(now, this.dateOptions),
}).map((day, i) => (
<span className={this.styles.weekDay} key={i}>
{format(day, this.props.weekdayDisplayFormat, this.dateOptions)}
</span>
))}
</div>
);
}
renderDateDisplay = () => {
const {
focusedRange,
color,
ranges,
rangeColors,
dateDisplayFormat,
editableDateInputs,
startDatePlaceholder,
endDatePlaceholder,
ariaLabels,
} = this.props;
const defaultColor = rangeColors[focusedRange[0]] || color;
const styles = this.styles;
return (
<div className={styles.dateDisplayWrapper}>
{ranges.map((range, i) => {
if (range.showDateDisplay === false || (range.disabled && !range.showDateDisplay))
return null;
return (
<div
className={styles.dateDisplay}
key={i}
style={{ color: range.color || defaultColor }}>
<DateInput
className={classnames(styles.dateDisplayItem, {
[styles.dateDisplayItemActive]: focusedRange[0] === i && focusedRange[1] === 0,
})}
readOnly={!editableDateInputs}
disabled={range.disabled}
value={range.startDate}
placeholder={startDatePlaceholder}
dateOptions={this.dateOptions}
dateDisplayFormat={dateDisplayFormat}
ariaLabel={
ariaLabels.dateInput &&
ariaLabels.dateInput[range.key] &&
ariaLabels.dateInput[range.key].startDate
}
onChange={this.onDragSelectionEnd}
onFocus={() => this.handleRangeFocusChange(i, 0)}
/>
<DateInput
className={classnames(styles.dateDisplayItem, {
[styles.dateDisplayItemActive]: focusedRange[0] === i && focusedRange[1] === 1,
})}
readOnly={!editableDateInputs}
disabled={range.disabled}
value={range.endDate}
placeholder={endDatePlaceholder}
dateOptions={this.dateOptions}
dateDisplayFormat={dateDisplayFormat}
ariaLabel={
ariaLabels.dateInput &&
ariaLabels.dateInput[range.key] &&
ariaLabels.dateInput[range.key].endDate
}
onChange={this.onDragSelectionEnd}
onFocus={() => this.handleRangeFocusChange(i, 1)}
/>
</div>
);
})}
</div>
);
};
onDragSelectionStart = date => {
const { onChange, dragSelectionEnabled } = this.props;
if (dragSelectionEnabled) {
this.setState({
drag: {
status: true,
range: { startDate: date, endDate: date },
disablePreview: true,
},
});
} else {
onChange && onChange(date);
}
};
onDragSelectionEnd = date => {
const { updateRange, displayMode, onChange, dragSelectionEnabled } = this.props;
if (!dragSelectionEnabled) return;
if (displayMode === 'date' || !this.state.drag.status) {
onChange && onChange(date);
return;
}
const newRange = {
startDate: this.state.drag.range.startDate,
endDate: date,
};
if (displayMode !== 'dateRange' || isSameDay(newRange.startDate, date)) {
this.setState({ drag: { status: false, range: {} } }, () => onChange && onChange(date));
} else {
this.setState({ drag: { status: false, range: {} } }, () => {
updateRange && updateRange(newRange);
});
}
};
onDragSelectionMove = date => {
const { drag } = this.state;
if (!drag.status || !this.props.dragSelectionEnabled) return;
this.setState({
drag: {
status: drag.status,
range: { startDate: drag.range.startDate, endDate: date },
disablePreview: true,
},
});
};
estimateMonthSize = (index, cache) => {
const { direction, minDate } = this.props;
const { scrollArea } = this.state;
if (cache) {
this.listSizeCache = cache;
if (cache[index]) return cache[index];
}
if (direction === 'horizontal') return scrollArea.monthWidth;
const monthStep = addMonths(minDate, index);
const { start, end } = getMonthDisplayRange(monthStep, this.dateOptions);
const isLongMonth = differenceInDays(end, start, this.dateOptions) + 1 > 7 * 5;
return isLongMonth ? scrollArea.longMonthHeight : scrollArea.monthHeight;
};
render() {
const {
showDateDisplay,
onPreviewChange,
scroll,
direction,
disabledDates,
disabledDay,
maxDate,
minDate,
rangeColors,
color,
navigatorRenderer,
className,
preview,
} = this.props;
const { scrollArea, focusedDate } = this.state;
const isVertical = direction === 'vertical';
const monthAndYearRenderer = navigatorRenderer || this.renderMonthAndYear;
const ranges = this.props.ranges.map((range, i) => ({
...range,
color: range.color || rangeColors[i] || color,
}));
return (
<div
className={classnames(this.styles.calendarWrapper, className)}
onMouseUp={() => this.setState({ drag: { status: false, range: {} } })}
onMouseLeave={() => {
this.setState({ drag: { status: false, range: {} } });
}}>
{showDateDisplay && this.renderDateDisplay()}
{monthAndYearRenderer(focusedDate, this.changeShownDate, this.props)}
{scroll.enabled ? (
<div>
{isVertical && this.renderWeekdays(this.dateOptions)}
<div
className={classnames(
this.styles.infiniteMonths,
isVertical ? this.styles.monthsVertical : this.styles.monthsHorizontal
)}
onMouseLeave={() => onPreviewChange && onPreviewChange()}
style={{
width: scrollArea.calendarWidth + 11,
height: scrollArea.calendarHeight + 11,
}}
onScroll={this.handleScroll}>
<ReactList
length={differenceInCalendarMonths(
endOfMonth(maxDate),
addDays(startOfMonth(minDate), -1),
this.dateOptions
)}
treshold={500}
type="variable"
ref={target => (this.list = target)}
itemSizeEstimator={this.estimateMonthSize}
axis={isVertical ? 'y' : 'x'}
itemRenderer={(index, key) => {
const monthStep = addMonths(minDate, index);
return (
<Month
{...this.props}
onPreviewChange={onPreviewChange || this.updatePreview}
preview={preview || this.state.preview}
ranges={ranges}
key={key}
drag={this.state.drag}
dateOptions={this.dateOptions}
disabledDates={disabledDates}
disabledDay={disabledDay}
month={monthStep}
onDragSelectionStart={this.onDragSelectionStart}
onDragSelectionEnd={this.onDragSelectionEnd}
onDragSelectionMove={this.onDragSelectionMove}
onMouseLeave={() => onPreviewChange && onPreviewChange()}
styles={this.styles}
style={
isVertical
? { height: this.estimateMonthSize(index) }
: { height: scrollArea.monthHeight, width: this.estimateMonthSize(index) }
}
showMonthName
showWeekDays={!isVertical}
/>
);
}}
/>
</div>
</div>
) : (
<div
className={classnames(
this.styles.months,
isVertical ? this.styles.monthsVertical : this.styles.monthsHorizontal
)}>
{new Array(this.props.months).fill(null).map((_, i) => {
let monthStep = addMonths(this.state.focusedDate, i);
if (this.props.calendarFocus === 'backwards') {
monthStep = subMonths(this.state.focusedDate, this.props.months - 1 - i);
}
return (
<Month
{...this.props}
onPreviewChange={onPreviewChange || this.updatePreview}
preview={preview || this.state.preview}
ranges={ranges}
key={i}
drag={this.state.drag}
dateOptions={this.dateOptions}
disabledDates={disabledDates}
disabledDay={disabledDay}
month={monthStep}
onDragSelectionStart={this.onDragSelectionStart}
onDragSelectionEnd={this.onDragSelectionEnd}
onDragSelectionMove={this.onDragSelectionMove}
onMouseLeave={() => onPreviewChange && onPreviewChange()}
styles={this.styles}
showWeekDays={!isVertical || i === 0}
showMonthName={!isVertical || i > 0}
/>
);
})}
</div>
)}
</div>
);
}
}
Calendar.defaultProps = {
showMonthArrow: true,
showMonthAndYearPickers: true,
disabledDates: [],
disabledDay: () => { },
classNames: {},
locale: defaultLocale,
ranges: [],
focusedRange: [0, 0],
dateDisplayFormat: 'MMM d, yyyy',
monthDisplayFormat: 'MMM yyyy',
weekdayDisplayFormat: 'E',
dayDisplayFormat: 'd',
showDateDisplay: true,
showPreview: true,
displayMode: 'date',
months: 1,
color: '#3d91ff',
scroll: {
enabled: false,
},
direction: 'vertical',
maxDate: addYears(new Date(), 20),
minDate: addYears(new Date(), -100),
rangeColors: ['#3d91ff', '#3ecf8e', '#fed14c'],
startDatePlaceholder: 'Early',
endDatePlaceholder: 'Continuous',
editableDateInputs: false,
dragSelectionEnabled: true,
fixedHeight: false,
calendarFocus: 'forwards',
preventSnapRefocus: false,
ariaLabels: {},
};
Calendar.propTypes = {
showMonthArrow: PropTypes.bool,
showMonthAndYearPickers: PropTypes.bool,
disabledDates: PropTypes.array,
disabledDay: PropTypes.func,
minDate: PropTypes.object,
maxDate: PropTypes.object,
date: PropTypes.object,
onChange: PropTypes.func,
onPreviewChange: PropTypes.func,
onRangeFocusChange: PropTypes.func,
classNames: PropTypes.object,
locale: PropTypes.object,
shownDate: PropTypes.object,
onShownDateChange: PropTypes.func,
ranges: PropTypes.arrayOf(rangeShape),
preview: PropTypes.shape({
startDate: PropTypes.object,
endDate: PropTypes.object,
color: PropTypes.string,
}),
dateDisplayFormat: PropTypes.string,
monthDisplayFormat: PropTypes.string,
weekdayDisplayFormat: PropTypes.string,
weekStartsOn: PropTypes.number,
dayDisplayFormat: PropTypes.string,
focusedRange: PropTypes.arrayOf(PropTypes.number),
initialFocusedRange: PropTypes.arrayOf(PropTypes.number),
months: PropTypes.number,
className: PropTypes.string,
showDateDisplay: PropTypes.bool,
showPreview: PropTypes.bool,
displayMode: PropTypes.oneOf(['dateRange', 'date']),
color: PropTypes.string,
updateRange: PropTypes.func,
scroll: PropTypes.shape({
enabled: PropTypes.bool,
monthHeight: PropTypes.number,
longMonthHeight: PropTypes.number,
monthWidth: PropTypes.number,
calendarWidth: PropTypes.number,
calendarHeight: PropTypes.number,
}),
direction: PropTypes.oneOf(['vertical', 'horizontal']),
startDatePlaceholder: PropTypes.string,
endDatePlaceholder: PropTypes.string,
navigatorRenderer: PropTypes.func,
rangeColors: PropTypes.arrayOf(PropTypes.string),
editableDateInputs: PropTypes.bool,
dragSelectionEnabled: PropTypes.bool,
fixedHeight: PropTypes.bool,
calendarFocus: PropTypes.string,
preventSnapRefocus: PropTypes.bool,
ariaLabels: ariaLabelsShape,
};
export default Calendar;

View File

@@ -0,0 +1,97 @@
.rdrCalendarWrapper {
box-sizing: border-box;
background: #ffffff;
display: inline-flex;
flex-direction: column;
user-select: none;
}
.rdrDateDisplay{
display: flex;
justify-content: space-between;
}
.rdrDateDisplayItem{
flex: 1 1;
width: 0;
text-align: center;
color: inherit;
& + &{
margin-left: 0.833em;
}
input{
text-align: inherit;
&:disabled{
cursor: default;
}
}
}
.rdrDateDisplayItemActive{}
.rdrMonthAndYearWrapper {
box-sizing: inherit;
display: flex;
justify-content: space-between;
}
.rdrMonthAndYearPickers{
flex: 1 1 auto;
display: flex;
justify-content: center;
align-items: center;
}
.rdrMonthPicker{}
.rdrYearPicker{}
.rdrNextPrevButton {
box-sizing: inherit;
cursor: pointer;
outline: none;
}
.rdrPprevButton {}
.rdrNextButton {}
.rdrMonths{
display: flex;
}
.rdrMonthsVertical{
flex-direction: column;
}
.rdrMonthsHorizontal > div > div > div{
display: flex;
flex-direction: row;
}
.rdrMonth{
width: 27.667em;
}
.rdrWeekDays{
display: flex;
}
.rdrWeekDay {
flex-basis: calc(100% / 7);
box-sizing: inherit;
text-align: center;
}
.rdrDays{
display: flex;
flex-wrap: wrap;
}
.rdrDateDisplayWrapper{}
.rdrMonthName{}
.rdrInfiniteMonths{
overflow: auto;
}

View File

@@ -0,0 +1,7 @@
import Calendar from '../Calendar';
describe('Calendar', () => {
test('Should resolve', () => {
expect(Calendar).toEqual(expect.anything());
});
});

View File

@@ -0,0 +1,108 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { format, parse, isValid, isEqual } from 'date-fns';
class DateInput extends PureComponent {
constructor(props, context) {
super(props, context);
this.state = {
invalid: false,
changed: false,
value: this.formatDate(props),
};
}
componentDidUpdate(prevProps) {
const { value } = prevProps;
if (!isEqual(value, this.props.value)) {
this.setState({ value: this.formatDate(this.props) });
}
}
formatDate({ value, dateDisplayFormat, dateOptions }) {
if (value && isValid(value)) {
return format(value, dateDisplayFormat, dateOptions);
}
return '';
}
update(value) {
const { invalid, changed } = this.state;
if (invalid || !changed || !value) {
return;
}
const { onChange, dateDisplayFormat, dateOptions } = this.props;
const parsed = parse(value, dateDisplayFormat, new Date(), dateOptions);
if (isValid(parsed)) {
this.setState({ changed: false }, () => onChange(parsed));
} else {
this.setState({ invalid: true });
}
}
onKeyDown = e => {
const { value } = this.state;
if (e.key === 'Enter') {
this.update(value);
}
};
onChange = e => {
this.setState({ value: e.target.value, changed: true, invalid: false });
};
onBlur = () => {
const { value } = this.state;
this.update(value);
};
render() {
const { className, readOnly, placeholder, ariaLabel, disabled, onFocus } = this.props;
const { value, invalid } = this.state;
return (
<span className={classnames('rdrDateInput', className)}>
<input
readOnly={readOnly}
disabled={disabled}
value={value}
placeholder={placeholder}
aria-label={ariaLabel}
onKeyDown={this.onKeyDown}
onChange={this.onChange}
onBlur={this.onBlur}
onFocus={onFocus}
/>
{invalid && <span className="rdrWarning">&#9888;</span>}
</span>
);
}
}
DateInput.propTypes = {
value: PropTypes.object,
placeholder: PropTypes.string,
disabled: PropTypes.bool,
readOnly: PropTypes.bool,
dateOptions: PropTypes.object,
dateDisplayFormat: PropTypes.string,
ariaLabel: PropTypes.string,
className: PropTypes.string,
onFocus: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
};
DateInput.defaultProps = {
readOnly: true,
disabled: false,
dateDisplayFormat: 'MMM D, YYYY',
};
export default DateInput;

View File

@@ -0,0 +1,16 @@
.rdrDateInput {
position: relative;
input {
outline: none;
}
.rdrWarning {
position: absolute;
font-size: 1.6em;
line-height: 1.6em;
top: 0;
right: .25em;
color: #FF0000;
}
}

View File

@@ -0,0 +1,29 @@
This component extends all the props of **[Calendar](#calendar)** component. In addition to those props, it has the following props:
| Prop Name | Type |
|---|---|
| **moveRangeOnFirstSelection** | boolean |
| **retainEndDateOnFirstSelection** | boolean |
| **onRangeFocusChange** | function |
| **rangeColors** | array |
| **ranges** | array |
#### Example: Editable Date Inputs
```jsx inside Markdown
import {useState} from 'react'
const [state, setState] = useState([
{
startDate: new Date(),
endDate: null,
key: 'selection'
}
]);
<DateRange
editableDateInputs={true}
onChange={item => setState([item.selection])}
moveRangeOnFirstSelection={false}
ranges={state}
/>
```

View File

@@ -0,0 +1,167 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Calendar from '../Calendar';
import { rangeShape } from '../DayCell';
import { findNextRangeIndex, generateStyles } from '../../utils';
import { isBefore, differenceInCalendarDays, addDays, min, isWithinInterval, max } from 'date-fns';
import classnames from 'classnames';
import coreStyles from '../../styles';
class DateRange extends Component {
constructor(props, context) {
super(props, context);
this.state = {
focusedRange: props.initialFocusedRange || [findNextRangeIndex(props.ranges), 0],
preview: null,
};
this.styles = generateStyles([coreStyles, props.classNames]);
}
calcNewSelection = (value, isSingleValue = true) => {
const focusedRange = this.props.focusedRange || this.state.focusedRange;
const {
ranges,
onChange,
maxDate,
moveRangeOnFirstSelection,
retainEndDateOnFirstSelection,
disabledDates,
} = this.props;
const focusedRangeIndex = focusedRange[0];
const selectedRange = ranges[focusedRangeIndex];
if (!selectedRange || !onChange) return {};
let { startDate, endDate } = selectedRange;
const now = new Date();
let nextFocusRange;
if (!isSingleValue) {
startDate = value.startDate;
endDate = value.endDate;
} else if (focusedRange[1] === 0) {
// startDate selection
const dayOffset = differenceInCalendarDays(endDate || now, startDate);
const calculateEndDate = () => {
if (moveRangeOnFirstSelection) {
return addDays(value, dayOffset);
}
if (retainEndDateOnFirstSelection) {
if (!endDate || isBefore(value, endDate)) {
return endDate;
}
return value;
}
return value || now;
};
startDate = value;
endDate = calculateEndDate();
if (maxDate) endDate = min([endDate, maxDate]);
nextFocusRange = [focusedRange[0], 1];
} else {
endDate = value;
}
// reverse dates if startDate before endDate
let isStartDateSelected = focusedRange[1] === 0;
if (isBefore(endDate, startDate)) {
isStartDateSelected = !isStartDateSelected;
[startDate, endDate] = [endDate, startDate];
}
const inValidDatesWithinRange = disabledDates.filter(disabledDate =>
isWithinInterval(disabledDate, {
start: startDate,
end: endDate,
})
);
if (inValidDatesWithinRange.length > 0) {
if (isStartDateSelected) {
startDate = addDays(max(inValidDatesWithinRange), 1);
} else {
endDate = addDays(min(inValidDatesWithinRange), -1);
}
}
if (!nextFocusRange) {
const nextFocusRangeIndex = findNextRangeIndex(this.props.ranges, focusedRange[0]);
nextFocusRange = [nextFocusRangeIndex, 0];
}
return {
wasValid: !(inValidDatesWithinRange.length > 0),
range: { startDate, endDate },
nextFocusRange: nextFocusRange,
};
};
setSelection = (value, isSingleValue) => {
const { onChange, ranges, onRangeFocusChange } = this.props;
const focusedRange = this.props.focusedRange || this.state.focusedRange;
const focusedRangeIndex = focusedRange[0];
const selectedRange = ranges[focusedRangeIndex];
if (!selectedRange) return;
const newSelection = this.calcNewSelection(value, isSingleValue);
onChange({
[selectedRange.key || `range${focusedRangeIndex + 1}`]: {
...selectedRange,
...newSelection.range,
},
});
this.setState({
focusedRange: newSelection.nextFocusRange,
preview: null,
});
onRangeFocusChange && onRangeFocusChange(newSelection.nextFocusRange);
};
handleRangeFocusChange = focusedRange => {
this.setState({ focusedRange });
this.props.onRangeFocusChange && this.props.onRangeFocusChange(focusedRange);
};
updatePreview = val => {
if (!val) {
this.setState({ preview: null });
return;
}
const { rangeColors, ranges } = this.props;
const focusedRange = this.props.focusedRange || this.state.focusedRange;
const color = ranges[focusedRange[0]]?.color || rangeColors[focusedRange[0]] || color;
this.setState({ preview: { ...val.range, color } });
};
render() {
return (
<Calendar
focusedRange={this.state.focusedRange}
onRangeFocusChange={this.handleRangeFocusChange}
preview={this.state.preview}
onPreviewChange={value => {
this.updatePreview(value ? this.calcNewSelection(value) : null);
}}
{...this.props}
displayMode="dateRange"
className={classnames(this.styles.dateRangeWrapper, this.props.className)}
onChange={this.setSelection}
updateRange={val => this.setSelection(val, false)}
ref={target => {
this.calendar = target;
}}
/>
);
}
}
DateRange.defaultProps = {
classNames: {},
ranges: [],
moveRangeOnFirstSelection: false,
retainEndDateOnFirstSelection: false,
rangeColors: ['#3d91ff', '#3ecf8e', '#fed14c'],
disabledDates: [],
};
DateRange.propTypes = {
...Calendar.propTypes,
onChange: PropTypes.func,
onRangeFocusChange: PropTypes.func,
className: PropTypes.string,
ranges: PropTypes.arrayOf(rangeShape),
moveRangeOnFirstSelection: PropTypes.bool,
retainEndDateOnFirstSelection: PropTypes.bool,
};
export default DateRange;

View File

@@ -0,0 +1,3 @@
.rdrDateRangeWrapper{
user-select: none;
}

View File

@@ -0,0 +1,84 @@
import React from 'react';
import { subDays, addDays, isSameDay } from 'date-fns';
import DateRange from '../DateRange';
import renderer from 'react-test-renderer';
let testRenderer = null;
let instance = null;
const endDate = new Date();
const startDate = subDays(endDate, 7);
const commonProps = {
ranges: [{ startDate, endDate, key: 'selection' }],
onChange: () => {},
moveRangeOnFirstSelection: false,
};
const compareRanges = (newRange, assertionRange) => {
['startDate', 'endDate'].forEach(key => {
if (!newRange[key] || !assertionRange[key]) {
return expect(newRange[key]).toEqual(assertionRange[key]);
}
return expect(isSameDay(newRange[key], assertionRange[key])).toEqual(true);
});
};
beforeEach(() => {
testRenderer = renderer.create(<DateRange {...commonProps} />);
instance = testRenderer.getInstance();
});
describe('DateRange', () => {
test('Should resolve', () => {
expect(DateRange).toEqual(expect.anything());
});
test('calculate new selection by resetting end date', () => {
const methodResult = instance.calcNewSelection(subDays(endDate, 10), true);
compareRanges(methodResult.range, {
startDate: subDays(endDate, 10),
endDate: subDays(endDate, 10),
});
});
test('calculate new selection by resetting end date if start date is not before', () => {
const methodResult = instance.calcNewSelection(addDays(endDate, 2), true);
compareRanges(methodResult.range, {
startDate: addDays(endDate, 2),
endDate: addDays(endDate, 2),
});
});
test('calculate new selection based on moveRangeOnFirstSelection prop', () => {
testRenderer.update(<DateRange {...commonProps} moveRangeOnFirstSelection />);
const methodResult = instance.calcNewSelection(subDays(endDate, 10), true);
compareRanges(methodResult.range, {
startDate: subDays(endDate, 10),
endDate: subDays(endDate, 3),
});
});
test('calculate new selection by retaining end date, based on retainEndDateOnFirstSelection prop', () => {
testRenderer.update(<DateRange {...commonProps} retainEndDateOnFirstSelection />);
const methodResult = instance.calcNewSelection(subDays(endDate, 10), true);
compareRanges(methodResult.range, {
startDate: subDays(endDate, 10),
endDate,
});
});
test('calculate new selection by retaining the unset end date, based on retainEndDateOnFirstSelection prop', () => {
testRenderer.update(
<DateRange
{...commonProps}
ranges={[{ ...commonProps.ranges[0], endDate: null }]}
retainEndDateOnFirstSelection
/>
);
const methodResult = instance.calcNewSelection(subDays(endDate, 10), true);
compareRanges(methodResult.range, {
startDate: subDays(endDate, 10),
endDate: null,
});
});
});

View File

@@ -0,0 +1,248 @@
This component wraps **[DefinedRange](#definedrange)** and **[Calendar](#calendar)** components together, and extends all the props of them.
#### Example: 2 Month View
```jsx inside Markdown
import { addDays } from 'date-fns';
import { useState } from 'react';
const [state, setState] = useState([
{
startDate: new Date(),
endDate: addDays(new Date(), 7),
key: 'selection'
}
]);
<DateRangePicker
onChange={item => setState([item.selection])}
showSelectionPreview={true}
moveRangeOnFirstSelection={false}
months={2}
ranges={state}
direction="horizontal"
/>;
```
#### Example: Backwards 2 Month View with preventSnapRefocus
```jsx inside Markdown
import { addDays } from 'date-fns';
import { useState } from 'react';
const [state, setState] = useState([
{
startDate: new Date(),
endDate: addDays(new Date(), 7),
key: 'selection'
}
]);
<DateRangePicker
onChange={item => setState([item.selection])}
showSelectionPreview={true}
moveRangeOnFirstSelection={false}
months={2}
ranges={state}
direction="horizontal"
preventSnapRefocus={true}
calendarFocus="backwards"
/>;
```
#### Example: Vertical Infinite
```jsx inside Markdown
import { addDays } from 'date-fns';
import { useState } from 'react';
const [state, setState] = useState({
selection: {
startDate: new Date(),
endDate: null,
key: 'selection'
},
compare: {
startDate: new Date(),
endDate: addDays(new Date(), 3),
key: 'compare'
}
});
<DateRangePicker
onChange={item => setState({ ...state, ...item })}
months={1}
minDate={addDays(new Date(), -300)}
maxDate={addDays(new Date(), 900)}
direction="vertical"
scroll={{ enabled: true }}
ranges={[state.selection, state.compare]}
/>;
```
#### Example: Multiple Range
```jsx inside Markdown
import { addDays } from 'date-fns';
import { useState } from 'react';
const [state, setState] = useState({
selection1: {
startDate: addDays(new Date(), 1),
endDate: null,
key: 'selection1'
},
selection2: {
startDate: addDays(new Date(), 4),
endDate: addDays(new Date(), 8),
key: 'selection2'
},
selection3: {
startDate: addDays(new Date(), 8),
endDate: addDays(new Date(), 10),
key: 'selection3',
autoFocus: false
}
});
<DateRangePicker
onChange={item => setState({ ...state, ...item })}
ranges={[state.selection1, state.selection2, state.selection3]}
/>;
```
#### Example: Insert Aria-label
```jsx inside Markdown
import { addDays } from 'date-fns';
import { useState } from 'react';
const [state, setState] = useState({
selection1: {
startDate: addDays(new Date(), -6),
endDate: new Date(),
key: 'selection1'
},
selection2: {
startDate: addDays(new Date(), 1),
endDate: addDays(new Date(), 7),
key: 'selection2'
}
});
<DateRangePicker
onChange={item => setState({ ...state, ...item })}
showSelectionPreview={true}
moveRangeOnFirstSelection={false}
months={2}
ranges={[state.selection1, state.selection2]}
direction="horizontal"
ariaLabels={{
dateInput: {
selection1: { startDate: "start date input of selction 1", endDate: "end date input of selction 1" },
selection2: { startDate: "start date input of selction 2", endDate: "end date input of selction 2" }
},
monthPicker: "month picker",
yearPicker: "year picker",
prevButton: "previous month button",
nextButton: "next month button",
}}
/>;
```
#### Example: Custom Day Cell Content
Show orange dot only for weekend
```jsx inside Markdown
import { addDays, format, isWeekend } from 'date-fns';
import { useState } from 'react';
const [state, setState] = useState({
selection1: {
startDate: addDays(new Date(), -6),
endDate: new Date(),
key: 'selection1'
},
selection2: {
startDate: addDays(new Date(), 1),
endDate: addDays(new Date(), 7),
key: 'selection2'
}
});
function customDayContent(day) {
extraDot = null;
if (isWeekend(day)) {
extraDot = (
<div
style={{
height: "5px",
width: "5px",
borderRadius: "100%",
background: "orange",
position: "absolute",
top: 2,
right: 2,
}}
/>
)
}
return (
<div>
{extraDot}
<span>{format(day, "d")}</span>
</div>
)
}
<DateRangePicker
onChange={item => setState({ ...state, ...item })}
showSelectionPreview={true}
moveRangeOnFirstSelection={false}
months={2}
ranges={[state.selection1, state.selection2]}
direction="horizontal"
dayContentRenderer={customDayContent}
ariaLabels={{
dateInput: {
selection1: { startDate: "start date input of selction 1", endDate: "end date input of selction 1" },
selection2: { startDate: "start date input of selction 2", endDate: "end date input of selction 2" }
},
monthPicker: "month picker",
yearPicker: "year picker",
prevButton: "previous month button",
nextButton: "next month button",
}}
/>;
```
#### Example: Restrict Date Selection
Restricts access for range selection to (-30, +30) days of current date.
```jsx inside Markdown
import { addDays } from 'date-fns';
import { useState } from 'react';
const [state, setState] = useState({
selection: {
startDate: new Date(),
endDate: null,
key: 'selection'
},
compare: {
startDate: new Date(),
endDate: addDays(new Date(), 3),
key: 'compare'
}
});
<DateRangePicker
onChange={item => setState({ ...state, ...item })}
months={1}
minDate={addDays(new Date(), -30)}
maxDate={addDays(new Date(), 30)}
direction="vertical"
scroll={{ enabled: true }}
ranges={[state.selection, state.compare]}
/>;
```

View File

@@ -0,0 +1,52 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import DateRange from '../DateRange';
import DefinedRange from '../DefinedRange';
import { findNextRangeIndex, generateStyles } from '../../utils';
import classnames from 'classnames';
import coreStyles from '../../styles';
class DateRangePicker extends Component {
constructor(props) {
super(props);
this.state = {
focusedRange: [findNextRangeIndex(props.ranges), 0],
};
this.styles = generateStyles([coreStyles, props.classNames]);
}
render() {
const { focusedRange } = this.state;
return (
<div className={classnames(this.styles.dateRangePickerWrapper, this.props.className)}>
<DefinedRange
focusedRange={focusedRange}
onPreviewChange={value =>
this.dateRange.updatePreview(
value ? this.dateRange.calcNewSelection(value, typeof value === 'string') : null
)
}
{...this.props}
range={this.props.ranges[focusedRange[0]]}
className={undefined}
/>
<DateRange
onRangeFocusChange={focusedRange => this.setState({ focusedRange })}
focusedRange={focusedRange}
{...this.props}
ref={t => (this.dateRange = t)}
className={undefined}
/>
</div>
);
}
}
DateRangePicker.defaultProps = {};
DateRangePicker.propTypes = {
...DateRange.propTypes,
...DefinedRange.propTypes,
className: PropTypes.string,
};
export default DateRangePicker;

View File

@@ -0,0 +1,4 @@
.rdrDateRangePickerWrapper{
display: inline-flex;
user-select: none;
}

View File

@@ -0,0 +1,224 @@
/* eslint-disable no-fallthrough */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { startOfDay, format, isSameDay, isAfter, isBefore, endOfDay } from 'date-fns';
class DayCell extends Component {
constructor(props, context) {
super(props, context);
this.state = {
hover: false,
active: false,
};
}
handleKeyEvent = event => {
const { day, onMouseDown, onMouseUp } = this.props;
if ([13 /* space */, 32 /* enter */].includes(event.keyCode)) {
if (event.type === 'keydown') onMouseDown(day);
else onMouseUp(day);
}
};
handleMouseEvent = event => {
const { day, disabled, onPreviewChange, onMouseEnter, onMouseDown, onMouseUp } = this.props;
const stateChanges = {};
if (disabled) {
onPreviewChange();
return;
}
switch (event.type) {
case 'mouseenter':
onMouseEnter(day);
onPreviewChange(day);
stateChanges.hover = true;
break;
case 'blur':
case 'mouseleave':
stateChanges.hover = false;
break;
case 'mousedown':
stateChanges.active = true;
onMouseDown(day);
break;
case 'mouseup':
event.stopPropagation();
stateChanges.active = false;
onMouseUp(day);
break;
case 'focus':
onPreviewChange(day);
break;
}
if (Object.keys(stateChanges).length) {
this.setState(stateChanges);
}
};
getClassNames = () => {
const {
isPassive,
isToday,
isWeekend,
isStartOfWeek,
isEndOfWeek,
isStartOfMonth,
isEndOfMonth,
disabled,
styles,
} = this.props;
return classnames(styles.day, {
[styles.dayPassive]: isPassive,
[styles.dayDisabled]: disabled,
[styles.dayToday]: isToday,
[styles.dayWeekend]: isWeekend,
[styles.dayStartOfWeek]: isStartOfWeek,
[styles.dayEndOfWeek]: isEndOfWeek,
[styles.dayStartOfMonth]: isStartOfMonth,
[styles.dayEndOfMonth]: isEndOfMonth,
[styles.dayHovered]: this.state.hover,
[styles.dayActive]: this.state.active,
});
};
renderPreviewPlaceholder = () => {
const { preview, day, styles } = this.props;
if (!preview) return null;
const startDate = preview.startDate ? endOfDay(preview.startDate) : null;
const endDate = preview.endDate ? startOfDay(preview.endDate) : null;
const isInRange =
(!startDate || isAfter(day, startDate)) && (!endDate || isBefore(day, endDate));
const isStartEdge = !isInRange && isSameDay(day, startDate);
const isEndEdge = !isInRange && isSameDay(day, endDate);
return (
<span
className={classnames({
[styles.dayStartPreview]: isStartEdge,
[styles.dayInPreview]: isInRange,
[styles.dayEndPreview]: isEndEdge,
})}
style={{ color: preview.color }}
/>
);
};
renderSelectionPlaceholders = () => {
const { styles, ranges, day } = this.props;
if (this.props.displayMode === 'date') {
let isSelected = isSameDay(this.props.day, this.props.date);
return isSelected ? (
<span className={styles.selected} style={{ color: this.props.color }} />
) : null;
}
const inRanges = ranges.reduce((result, range) => {
let startDate = range.startDate;
let endDate = range.endDate;
if (startDate && endDate && isBefore(endDate, startDate)) {
[startDate, endDate] = [endDate, startDate];
}
startDate = startDate ? endOfDay(startDate) : null;
endDate = endDate ? startOfDay(endDate) : null;
const isInRange =
(!startDate || isAfter(day, startDate)) && (!endDate || isBefore(day, endDate));
const isStartEdge = !isInRange && isSameDay(day, startDate);
const isEndEdge = !isInRange && isSameDay(day, endDate);
if (isInRange || isStartEdge || isEndEdge) {
return [
...result,
{
isStartEdge,
isEndEdge: isEndEdge,
isInRange,
...range,
},
];
}
return result;
}, []);
return inRanges.map((range, i) => (
<span
key={i}
className={classnames({
[styles.startEdge]: range.isStartEdge,
[styles.endEdge]: range.isEndEdge,
[styles.inRange]: range.isInRange,
})}
style={{ color: range.color || this.props.color }}
/>
));
};
render() {
const { dayContentRenderer } = this.props;
return (
<button
type="button"
onMouseEnter={this.handleMouseEvent}
onMouseLeave={this.handleMouseEvent}
onFocus={this.handleMouseEvent}
onMouseDown={this.handleMouseEvent}
onMouseUp={this.handleMouseEvent}
onBlur={this.handleMouseEvent}
onPauseCapture={this.handleMouseEvent}
onKeyDown={this.handleKeyEvent}
onKeyUp={this.handleKeyEvent}
className={this.getClassNames(this.props.styles)}
{...(this.props.disabled || this.props.isPassive ? { tabIndex: -1 } : {})}
style={{ color: this.props.color }}>
{this.renderSelectionPlaceholders()}
{this.renderPreviewPlaceholder()}
<span className={this.props.styles.dayNumber}>
{
dayContentRenderer?.(this.props.day) ||
<span>{format(this.props.day, this.props.dayDisplayFormat)}</span>
}
</span>
</button>
);
}
}
DayCell.defaultProps = {};
export const rangeShape = PropTypes.shape({
startDate: PropTypes.object,
endDate: PropTypes.object,
color: PropTypes.string,
key: PropTypes.string,
autoFocus: PropTypes.bool,
disabled: PropTypes.bool,
showDateDisplay: PropTypes.bool,
});
DayCell.propTypes = {
day: PropTypes.object.isRequired,
dayDisplayFormat: PropTypes.string,
date: PropTypes.object,
ranges: PropTypes.arrayOf(rangeShape),
preview: PropTypes.shape({
startDate: PropTypes.object,
endDate: PropTypes.object,
color: PropTypes.string,
}),
onPreviewChange: PropTypes.func,
previewColor: PropTypes.string,
disabled: PropTypes.bool,
isPassive: PropTypes.bool,
isToday: PropTypes.bool,
isWeekend: PropTypes.bool,
isStartOfWeek: PropTypes.bool,
isEndOfWeek: PropTypes.bool,
isStartOfMonth: PropTypes.bool,
isEndOfMonth: PropTypes.bool,
color: PropTypes.string,
displayMode: PropTypes.oneOf(['dateRange', 'date']),
styles: PropTypes.object,
onMouseDown: PropTypes.func,
onMouseUp: PropTypes.func,
onMouseEnter: PropTypes.func,
dayContentRenderer: PropTypes.func,
};
export default DayCell;

View File

@@ -0,0 +1,39 @@
.rdrDay {
box-sizing: inherit;
width: calc(100% / 7);
position: relative;
font: inherit;
cursor: pointer;
}
.rdrDayNumber {
display: block;
position: relative;
span{
color: #1d2429;
}
}
.rdrDayDisabled {
cursor: not-allowed;
}
@supports (-ms-ime-align: auto) {
.rdrDay {
flex-basis: 14.285% !important;
}
}
.rdrSelected, .rdrInRange, .rdrStartEdge, .rdrEndEdge{
pointer-events: none;
}
.rdrInRange{}
.rdrDayStartPreview, .rdrDayInPreview, .rdrDayEndPreview{
pointer-events: none;
}
.rdrDayHovered{}
.rdrDayActive{}

View File

@@ -0,0 +1,90 @@
#### Example: Default Labels
```jsx inside Markdown
import { useState } from 'react';
const [state, setState] = useState([
{
startDate: new Date(),
endDate: null,
key: 'selection'
}
]);
<DefinedRange
onChange={item => setState([item.selection])}
ranges={state}
/>;
```
#### Example: Custom range labels
```jsx inside Markdown
import { useState } from 'react';
const renderStaticRangeLabel = () => (
<CustomStaticRangeLabelContent text={'This is a custom label content: '} />
);
class CustomStaticRangeLabelContent extends React.Component {
constructor(props) {
super(props);
this.state = {
currentDateString: Date(),
};
this.intervalId = setInterval(() => {
this.setState({
currentDateString: Date(),
});
}, 1000);
}
componentWillUnmount() {
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
render() {
const { currentDateString } = this.state;
const { text } = this.props;
return (
<span>
<i>{text}</i>
<span className={'random-date-string'}>
<b>{currentDateString}</b>
</span>
</span>
);
}
}
const [state, setState] = useState([
{
startDate: new Date(),
endDate: null,
key: 'selection'
}
]);
<DefinedRange
onChange={item => setState([item.selection])}
ranges={state}
renderStaticRangeLabel={renderStaticRangeLabel}
staticRanges={[
{
label: 'Hoy',
hasCustomRendering: true,
range: () => ({
startDate: new Date(),
endDate: new Date()
}),
isSelected() {
return true;
}
}
]}
/>;
```

View File

@@ -0,0 +1,238 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DefinedRange tests Should render dynamic static label contents correctly 1`] = `
<div
className="rdrDefinedRangesWrapper"
>
<div
className="rdrStaticRanges"
>
<button
className="rdrStaticRange"
key="0"
onClick={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
style={
Object {
"color": null,
}
}
type="button"
>
<span
className="rdrStaticRangeLabel"
tabIndex={-1}
>
<i
className="italic-label-content"
>
Italic Content
</i>
</span>
</button>
<button
className="rdrStaticRange"
key="1"
onClick={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
style={
Object {
"color": null,
}
}
type="button"
>
<span
className="rdrStaticRangeLabel"
tabIndex={-1}
>
Static Label
</span>
</button>
<button
className="rdrStaticRange"
key="2"
onClick={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
style={
Object {
"color": null,
}
}
type="button"
>
<span
className="rdrStaticRangeLabel"
tabIndex={-1}
>
<img
className="random-image"
/>
</span>
</button>
<button
className="rdrStaticRange"
key="3"
onClick={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
style={
Object {
"color": null,
}
}
type="button"
>
<span
className="rdrStaticRangeLabel"
tabIndex={-1}
>
<b
className="bold-label-content"
>
Bold Content
</b>
</span>
</button>
</div>
<div
className="rdrInputRanges"
>
<InputRangeField
key="0"
label="days up to today"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="-"
styles={
Object {
"calendarWrapper": "rdrCalendarWrapper",
"dateDisplay": "rdrDateDisplay",
"dateDisplayItem": "rdrDateDisplayItem",
"dateDisplayItemActive": "rdrDateDisplayItemActive",
"dateDisplayWrapper": "rdrDateDisplayWrapper",
"dateRangePickerWrapper": "rdrDateRangePickerWrapper",
"dateRangeWrapper": "rdrDateRangeWrapper",
"day": "rdrDay",
"dayActive": "rdrDayActive",
"dayDisabled": "rdrDayDisabled",
"dayEndOfMonth": "rdrDayEndOfMonth",
"dayEndOfWeek": "rdrDayEndOfWeek",
"dayEndPreview": "rdrDayEndPreview",
"dayHovered": "rdrDayHovered",
"dayInPreview": "rdrDayInPreview",
"dayNumber": "rdrDayNumber",
"dayPassive": "rdrDayPassive",
"daySelected": "rdrDaySelected",
"dayStartOfMonth": "rdrDayStartOfMonth",
"dayStartOfWeek": "rdrDayStartOfWeek",
"dayStartPreview": "rdrDayStartPreview",
"dayToday": "rdrDayToday",
"dayWeekend": "rdrDayWeekend",
"days": "rdrDays",
"definedRangesWrapper": "rdrDefinedRangesWrapper",
"endEdge": "rdrEndEdge",
"inRange": "rdrInRange",
"infiniteMonths": "rdrInfiniteMonths",
"inputRange": "rdrInputRange",
"inputRangeInput": "rdrInputRangeInput",
"inputRanges": "rdrInputRanges",
"month": "rdrMonth",
"monthAndYearPickers": "rdrMonthAndYearPickers",
"monthAndYearWrapper": "rdrMonthAndYearWrapper",
"monthName": "rdrMonthName",
"monthPicker": "rdrMonthPicker",
"months": "rdrMonths",
"monthsHorizontal": "rdrMonthsHorizontal",
"monthsVertical": "rdrMonthsVertical",
"nextButton": "rdrNextButton",
"nextPrevButton": "rdrNextPrevButton",
"prevButton": "rdrPprevButton",
"selected": "rdrSelected",
"startEdge": "rdrStartEdge",
"staticRange": "rdrStaticRange",
"staticRangeLabel": "rdrStaticRangeLabel",
"staticRangeSelected": "rdrStaticRangeSelected",
"staticRanges": "rdrStaticRanges",
"weekDay": "rdrWeekDay",
"weekDays": "rdrWeekDays",
"yearPicker": "rdrYearPicker",
}
}
value="-"
/>
<InputRangeField
key="1"
label="days starting today"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="-"
styles={
Object {
"calendarWrapper": "rdrCalendarWrapper",
"dateDisplay": "rdrDateDisplay",
"dateDisplayItem": "rdrDateDisplayItem",
"dateDisplayItemActive": "rdrDateDisplayItemActive",
"dateDisplayWrapper": "rdrDateDisplayWrapper",
"dateRangePickerWrapper": "rdrDateRangePickerWrapper",
"dateRangeWrapper": "rdrDateRangeWrapper",
"day": "rdrDay",
"dayActive": "rdrDayActive",
"dayDisabled": "rdrDayDisabled",
"dayEndOfMonth": "rdrDayEndOfMonth",
"dayEndOfWeek": "rdrDayEndOfWeek",
"dayEndPreview": "rdrDayEndPreview",
"dayHovered": "rdrDayHovered",
"dayInPreview": "rdrDayInPreview",
"dayNumber": "rdrDayNumber",
"dayPassive": "rdrDayPassive",
"daySelected": "rdrDaySelected",
"dayStartOfMonth": "rdrDayStartOfMonth",
"dayStartOfWeek": "rdrDayStartOfWeek",
"dayStartPreview": "rdrDayStartPreview",
"dayToday": "rdrDayToday",
"dayWeekend": "rdrDayWeekend",
"days": "rdrDays",
"definedRangesWrapper": "rdrDefinedRangesWrapper",
"endEdge": "rdrEndEdge",
"inRange": "rdrInRange",
"infiniteMonths": "rdrInfiniteMonths",
"inputRange": "rdrInputRange",
"inputRangeInput": "rdrInputRangeInput",
"inputRanges": "rdrInputRanges",
"month": "rdrMonth",
"monthAndYearPickers": "rdrMonthAndYearPickers",
"monthAndYearWrapper": "rdrMonthAndYearWrapper",
"monthName": "rdrMonthName",
"monthPicker": "rdrMonthPicker",
"months": "rdrMonths",
"monthsHorizontal": "rdrMonthsHorizontal",
"monthsVertical": "rdrMonthsVertical",
"nextButton": "rdrNextButton",
"nextPrevButton": "rdrNextPrevButton",
"prevButton": "rdrPprevButton",
"selected": "rdrSelected",
"startEdge": "rdrStartEdge",
"staticRange": "rdrStaticRange",
"staticRangeLabel": "rdrStaticRangeLabel",
"staticRangeSelected": "rdrStaticRangeSelected",
"staticRanges": "rdrStaticRanges",
"weekDay": "rdrWeekDay",
"weekDays": "rdrWeekDays",
"yearPicker": "rdrYearPicker",
}
}
value="-"
/>
</div>
</div>
`;

View File

@@ -0,0 +1,142 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styles from '../../styles';
import { defaultInputRanges, defaultStaticRanges } from '../../defaultRanges';
import { rangeShape } from '../DayCell';
import InputRangeField from '../InputRangeField';
import cx from 'classnames';
class DefinedRange extends Component {
constructor(props) {
super(props);
this.state = {
rangeOffset: 0,
focusedInput: -1,
};
}
handleRangeChange = range => {
const { onChange, ranges, focusedRange } = this.props;
const selectedRange = ranges[focusedRange[0]];
if (!onChange || !selectedRange) return;
onChange({
[selectedRange.key || `range${focusedRange[0] + 1}`]: { ...selectedRange, ...range },
});
};
getRangeOptionValue(option) {
const { ranges = [], focusedRange = [] } = this.props;
if (typeof option.getCurrentValue !== 'function') {
return '';
}
const selectedRange = ranges[focusedRange[0]] || {};
return option.getCurrentValue(selectedRange) || '';
}
getSelectedRange(ranges, staticRange) {
const focusedRangeIndex = ranges.findIndex(range => {
if (!range.startDate || !range.endDate || range.disabled) return false;
return staticRange.isSelected(range);
});
const selectedRange = ranges[focusedRangeIndex];
return { selectedRange, focusedRangeIndex };
}
render() {
const {
headerContent,
footerContent,
onPreviewChange,
inputRanges,
staticRanges,
ranges,
renderStaticRangeLabel,
rangeColors,
className,
} = this.props;
return (
<div className={cx(styles.definedRangesWrapper, className)}>
{headerContent}
<div className={styles.staticRanges}>
{staticRanges.map((staticRange, i) => {
const { selectedRange, focusedRangeIndex } = this.getSelectedRange(ranges, staticRange);
let labelContent;
if (staticRange.hasCustomRendering) {
labelContent = renderStaticRangeLabel(staticRange);
} else {
labelContent = staticRange.label;
}
return (
<button
type="button"
className={cx(styles.staticRange, {
[styles.staticRangeSelected]: Boolean(selectedRange),
})}
style={{
color: selectedRange
? selectedRange.color || rangeColors[focusedRangeIndex]
: null,
}}
key={i}
onClick={() => this.handleRangeChange(staticRange.range(this.props))}
onFocus={() => onPreviewChange && onPreviewChange(staticRange.range(this.props))}
onMouseOver={() =>
onPreviewChange && onPreviewChange(staticRange.range(this.props))
}
onMouseLeave={() => {
onPreviewChange && onPreviewChange();
}}>
<span tabIndex={-1} className={styles.staticRangeLabel}>
{labelContent}
</span>
</button>
);
})}
</div>
<div className={styles.inputRanges}>
{inputRanges.map((rangeOption, i) => (
<InputRangeField
key={i}
styles={styles}
label={rangeOption.label}
onFocus={() => this.setState({ focusedInput: i, rangeOffset: 0 })}
onBlur={() => this.setState({ rangeOffset: 0 })}
onChange={value => this.handleRangeChange(rangeOption.range(value, this.props))}
value={this.getRangeOptionValue(rangeOption)}
/>
))}
</div>
{footerContent}
</div>
);
}
}
DefinedRange.propTypes = {
inputRanges: PropTypes.array,
staticRanges: PropTypes.array,
ranges: PropTypes.arrayOf(rangeShape),
focusedRange: PropTypes.arrayOf(PropTypes.number),
onPreviewChange: PropTypes.func,
onChange: PropTypes.func,
footerContent: PropTypes.any,
headerContent: PropTypes.any,
rangeColors: PropTypes.arrayOf(PropTypes.string),
className: PropTypes.string,
renderStaticRangeLabel: PropTypes.func,
};
DefinedRange.defaultProps = {
inputRanges: defaultInputRanges,
staticRanges: defaultStaticRanges,
ranges: [],
rangeColors: ['#3d91ff', '#3ecf8e', '#fed14c'],
focusedRange: [0, 0],
};
export default DefinedRange;

View File

@@ -0,0 +1,20 @@
.rdrDefinedRangesWrapper{}
.rdrStaticRanges{
display: flex;
flex-direction: column;
}
.rdrStaticRange{
font-size: inherit;
}
.rdrStaticRangeLabel{}
.rdrInputRanges{}
.rdrInputRange{
display: flex;
}
.rdrInputRangeInput{}

View File

@@ -0,0 +1,135 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
import DefinedRange from '../DefinedRange';
import { isSameDay } from 'date-fns';
describe('DefinedRange tests', () => {
test('Should call "renderStaticRangeLabel" callback correct amount of times according to the "hasCustomRendering" option', () => {
const renderStaticRangeLabel = jest.fn();
mount(
<DefinedRange
staticRanges={[
{
label: 'Dynamic Label',
range: {},
isSelected(range) {
const definedRange = this.range();
return (
isSameDay(range.startDate, definedRange.startDate) &&
isSameDay(range.endDate, definedRange.endDate)
);
},
hasCustomRendering: true,
},
{
label: 'Static Label',
range: {},
isSelected(range) {
const definedRange = this.range();
return (
isSameDay(range.startDate, definedRange.startDate) &&
isSameDay(range.endDate, definedRange.endDate)
);
},
},
{
label: 'Hede',
range: {},
isSelected(range) {
const definedRange = this.range();
return (
isSameDay(range.startDate, definedRange.startDate) &&
isSameDay(range.endDate, definedRange.endDate)
);
},
hasCustomRendering: true,
},
]}
renderStaticRangeLabel={renderStaticRangeLabel}
/>
);
expect(renderStaticRangeLabel).toHaveBeenCalledTimes(2);
});
test('Should render dynamic static label contents correctly', () => {
const renderItalicLabelContent = () => (
<i className={'italic-label-content'}>{'Italic Content'}</i>
);
const renderBoldLabelContent = () => <b className={'bold-label-content'}>{'Bold Content'}</b>;
const renderSomethingElse = () => <img className={'random-image'} />;
const renderStaticRangeLabel = function(staticRange) {
let result;
if (staticRange.id === 'italic') {
result = renderItalicLabelContent();
} else if (staticRange.id === 'bold') {
result = renderBoldLabelContent();
} else {
result = renderSomethingElse();
}
return result;
};
const wrapper = shallow(
<DefinedRange
staticRanges={[
{
id: 'italic',
range: {},
isSelected(range) {
const definedRange = this.range();
return (
isSameDay(range.startDate, definedRange.startDate) &&
isSameDay(range.endDate, definedRange.endDate)
);
},
hasCustomRendering: true,
},
{
label: 'Static Label',
range: {},
isSelected(range) {
const definedRange = this.range();
return (
isSameDay(range.startDate, definedRange.startDate) &&
isSameDay(range.endDate, definedRange.endDate)
);
},
},
{
id: 'whatever',
range: {},
isSelected(range) {
const definedRange = this.range();
return (
isSameDay(range.startDate, definedRange.startDate) &&
isSameDay(range.endDate, definedRange.endDate)
);
},
hasCustomRendering: true,
},
{
id: 'bold',
range: {},
isSelected(range) {
const definedRange = this.range();
return (
isSameDay(range.startDate, definedRange.startDate) &&
isSameDay(range.endDate, definedRange.endDate)
);
},
hasCustomRendering: true,
},
]}
renderStaticRangeLabel={renderStaticRangeLabel}
/>
);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,88 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`InputRangeField tests Should parse input value to number 1`] = `
<InputRangeField
label="Input label"
onBlur={[MockFunction]}
onChange={
[MockFunction] {
"calls": Array [
Array [
3,
],
Array [
12,
],
Array [
0,
],
Array [
0,
],
Array [
0,
],
Array [
99999,
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
Object {
"type": "return",
"value": undefined,
},
Object {
"type": "return",
"value": undefined,
},
Object {
"type": "return",
"value": undefined,
},
Object {
"type": "return",
"value": undefined,
},
Object {
"type": "return",
"value": undefined,
},
],
}
}
onFocus={[MockFunction]}
placeholder="-"
styles={
Object {
"inputRange": "range",
"inputRangeInput": "input",
"inputRangeLabel": "label",
}
}
value=""
>
<div
className="range"
>
<input
className="input"
max={99999}
min={0}
onBlur={[MockFunction]}
onChange={[Function]}
onFocus={[MockFunction]}
placeholder="-"
value=""
/>
<span
className="label"
>
Input label
</span>
</div>
</InputRangeField>
`;

View File

@@ -0,0 +1,71 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
const MIN = 0;
const MAX = 99999;
class InputRangeField extends Component {
constructor(props, context) {
super(props, context);
}
shouldComponentUpdate(nextProps) {
const { value, label, placeholder } = this.props;
return (
value !== nextProps.value ||
label !== nextProps.label ||
placeholder !== nextProps.placeholder
);
}
onChange = e => {
const { onChange } = this.props;
let value = parseInt(e.target.value, 10);
value = isNaN(value) ? 0 : Math.max(Math.min(MAX, value), MIN);
onChange(value);
};
render() {
const { label, placeholder, value, styles, onBlur, onFocus } = this.props;
return (
<div className={styles.inputRange}>
<input
className={styles.inputRangeInput}
placeholder={placeholder}
value={value}
min={MIN}
max={MAX}
onChange={this.onChange}
onFocus={onFocus}
onBlur={onBlur}
/>
<span className={styles.inputRangeLabel}>{label}</span>
</div>
);
}
}
InputRangeField.propTypes = {
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
label: PropTypes.oneOfType([PropTypes.element, PropTypes.node]).isRequired,
placeholder: PropTypes.string,
styles: PropTypes.shape({
inputRange: PropTypes.string,
inputRangeInput: PropTypes.string,
inputRangeLabel: PropTypes.string,
}).isRequired,
onBlur: PropTypes.func.isRequired,
onFocus: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
};
InputRangeField.defaultProps = {
value: '',
placeholder: '-',
};
export default InputRangeField;

View File

@@ -0,0 +1,111 @@
import React from 'react';
import { mount } from 'enzyme';
import InputRangeField from '../InputRangeField';
const styles = {
inputRange: 'range',
inputRangeInput: 'input',
inputRangeLabel: 'label',
};
const toChangeEvent = value => ({ target: { value } });
describe('InputRangeField tests', () => {
test('Should parse input value to number', () => {
const onChange = jest.fn();
const wrapper = mount(
<InputRangeField
label="Input label"
styles={styles}
onChange={onChange}
onFocus={jest.fn()}
onBlur={jest.fn()}
/>
);
wrapper.find('input').simulate('change', toChangeEvent('3'));
expect(onChange).lastCalledWith(3);
wrapper.find('input').simulate('change', toChangeEvent(12));
expect(onChange).lastCalledWith(12);
wrapper.find('input').simulate('change', toChangeEvent(''));
expect(onChange).lastCalledWith(0);
wrapper.find('input').simulate('change', toChangeEvent('invalid number'));
expect(onChange).lastCalledWith(0);
wrapper.find('input').simulate('change', toChangeEvent(-12));
expect(onChange).lastCalledWith(0);
wrapper.find('input').simulate('change', toChangeEvent(99999999));
expect(onChange).lastCalledWith(99999);
expect(onChange).toHaveBeenCalledTimes(6);
expect(wrapper).toMatchSnapshot();
});
test('Should rerender when props change', () => {
const wrapper = mount(
<InputRangeField
value={12}
placeholder="Placeholder"
label="Input label"
styles={styles}
onChange={jest.fn()}
onFocus={jest.fn()}
onBlur={jest.fn()}
/>
);
expect(wrapper.find(`.${styles.inputRangeInput}`).prop('value')).toEqual(12);
expect(wrapper.find(`.${styles.inputRangeInput}`).prop('placeholder')).toEqual('Placeholder');
expect(wrapper.find(`.${styles.inputRangeLabel}`).text()).toEqual('Input label');
wrapper.setProps({ value: '32' });
expect(wrapper.find(`.${styles.inputRangeInput}`).prop('value')).toEqual('32');
expect(wrapper.find(`.${styles.inputRangeInput}`).prop('placeholder')).toEqual('Placeholder');
expect(wrapper.find(`.${styles.inputRangeLabel}`).text()).toEqual('Input label');
wrapper.setProps({ placeholder: '-' });
expect(wrapper.find(`.${styles.inputRangeInput}`).prop('value')).toEqual('32');
expect(wrapper.find(`.${styles.inputRangeInput}`).prop('placeholder')).toEqual('-');
expect(wrapper.find(`.${styles.inputRangeLabel}`).text()).toEqual('Input label');
wrapper.setProps({ label: 'Label' });
expect(wrapper.find(`.${styles.inputRangeInput}`).prop('value')).toEqual('32');
expect(wrapper.find(`.${styles.inputRangeInput}`).prop('placeholder')).toEqual('-');
expect(wrapper.find(`.${styles.inputRangeLabel}`).text()).toEqual('Label');
});
test('Should render the label as a Component', () => {
const Label = () => <span className="input-range-field-label">Input label</span>;
const wrapper = mount(
<InputRangeField
value={12}
placeholder="Placeholder"
label={<Label />}
styles={styles}
onChange={jest.fn()}
onFocus={jest.fn()}
onBlur={jest.fn()}
/>
);
expect(wrapper.find(`.${styles.inputRangeInput}`).prop('value')).toEqual(12);
expect(wrapper.find(`.${styles.inputRangeInput}`).prop('placeholder')).toEqual('Placeholder');
expect(wrapper.find(`.${styles.inputRangeLabel}`).text()).toEqual('Input label');
expect(wrapper.find(`.${styles.inputRangeLabel}`).text()).toEqual('Input label');
expect(wrapper.find(`.input-range-field-label`).text()).toEqual('Input label');
wrapper.setProps({ value: '32' });
expect(wrapper.find(`.${styles.inputRangeInput}`).prop('value')).toEqual('32');
expect(wrapper.find(`.${styles.inputRangeInput}`).prop('placeholder')).toEqual('Placeholder');
expect(wrapper.find(`.${styles.inputRangeLabel}`).text()).toEqual('Input label');
wrapper.setProps({ placeholder: '-' });
expect(wrapper.find(`.${styles.inputRangeInput}`).prop('value')).toEqual('32');
expect(wrapper.find(`.${styles.inputRangeInput}`).prop('placeholder')).toEqual('-');
expect(wrapper.find(`.${styles.inputRangeLabel}`).text()).toEqual('Input label');
wrapper.setProps({ label: 'Label' });
expect(wrapper.find(`.${styles.inputRangeInput}`).prop('value')).toEqual('32');
expect(wrapper.find(`.${styles.inputRangeInput}`).prop('placeholder')).toEqual('-');
expect(wrapper.find(`.${styles.inputRangeLabel}`).text()).toEqual('Label');
});
});

View File

@@ -0,0 +1,148 @@
/* eslint-disable no-fallthrough */
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import DayCell, { rangeShape } from '../DayCell';
import {
format,
startOfDay,
endOfDay,
startOfWeek,
endOfWeek,
isBefore,
isSameDay,
isAfter,
isWeekend,
isWithinInterval,
eachDayOfInterval,
} from 'date-fns';
import { getMonthDisplayRange } from '../../utils';
function renderWeekdays(styles, dateOptions, weekdayDisplayFormat) {
const now = new Date();
return (
<div className={styles.weekDays}>
{eachDayOfInterval({
start: startOfWeek(now, dateOptions),
end: endOfWeek(now, dateOptions),
}).map((day, i) => (
<span className={styles.weekDay} key={i}>
{format(day, weekdayDisplayFormat, dateOptions)}
</span>
))}
</div>
);
}
class Month extends PureComponent {
render() {
const now = new Date();
const { displayMode, focusedRange, drag, styles, disabledDates, disabledDay } = this.props;
const minDate = this.props.minDate && startOfDay(this.props.minDate);
const maxDate = this.props.maxDate && endOfDay(this.props.maxDate);
const monthDisplay = getMonthDisplayRange(
this.props.month,
this.props.dateOptions,
this.props.fixedHeight
);
let ranges = this.props.ranges;
if (displayMode === 'dateRange' && drag.status) {
let { startDate, endDate } = drag.range;
ranges = ranges.map((range, i) => {
if (i !== focusedRange[0]) return range;
return {
...range,
startDate,
endDate,
};
});
}
const showPreview = this.props.showPreview && !drag.disablePreview;
return (
<div className={styles.month} style={this.props.style}>
{this.props.showMonthName ? (
<div className={styles.monthName}>
{format(this.props.month, this.props.monthDisplayFormat, this.props.dateOptions)}
</div>
) : null}
{this.props.showWeekDays &&
renderWeekdays(styles, this.props.dateOptions, this.props.weekdayDisplayFormat)}
<div className={styles.days} onMouseLeave={this.props.onMouseLeave}>
{eachDayOfInterval({ start: monthDisplay.start, end: monthDisplay.end }).map(
(day, index) => {
const isStartOfMonth = isSameDay(day, monthDisplay.startDateOfMonth);
const isEndOfMonth = isSameDay(day, monthDisplay.endDateOfMonth);
const isOutsideMinMax =
(minDate && isBefore(day, minDate)) || (maxDate && isAfter(day, maxDate));
const isDisabledSpecifically = disabledDates.some(disabledDate =>
isSameDay(disabledDate, day)
);
const isDisabledDay = disabledDay(day);
return (
<DayCell
{...this.props}
ranges={ranges}
day={day}
preview={showPreview ? this.props.preview : null}
isWeekend={isWeekend(day, this.props.dateOptions)}
isToday={isSameDay(day, now)}
isStartOfWeek={isSameDay(day, startOfWeek(day, this.props.dateOptions))}
isEndOfWeek={isSameDay(day, endOfWeek(day, this.props.dateOptions))}
isStartOfMonth={isStartOfMonth}
isEndOfMonth={isEndOfMonth}
key={index}
disabled={isOutsideMinMax || isDisabledSpecifically || isDisabledDay}
isPassive={
!isWithinInterval(day, {
start: monthDisplay.startDateOfMonth,
end: monthDisplay.endDateOfMonth,
})
}
styles={styles}
onMouseDown={this.props.onDragSelectionStart}
onMouseUp={this.props.onDragSelectionEnd}
onMouseEnter={this.props.onDragSelectionMove}
dragRange={drag.range}
drag={drag.status}
/>
);
}
)}
</div>
</div>
);
}
}
Month.defaultProps = {};
Month.propTypes = {
style: PropTypes.object,
styles: PropTypes.object,
month: PropTypes.object,
drag: PropTypes.object,
dateOptions: PropTypes.object,
disabledDates: PropTypes.array,
disabledDay: PropTypes.func,
preview: PropTypes.shape({
startDate: PropTypes.object,
endDate: PropTypes.object,
}),
showPreview: PropTypes.bool,
displayMode: PropTypes.oneOf(['dateRange', 'date']),
minDate: PropTypes.object,
maxDate: PropTypes.object,
ranges: PropTypes.arrayOf(rangeShape),
focusedRange: PropTypes.arrayOf(PropTypes.number),
onDragSelectionStart: PropTypes.func,
onDragSelectionEnd: PropTypes.func,
onDragSelectionMove: PropTypes.func,
onMouseLeave: PropTypes.func,
monthDisplayFormat: PropTypes.string,
weekdayDisplayFormat: PropTypes.string,
dayDisplayFormat: PropTypes.string,
showWeekDays: PropTypes.bool,
showMonthName: PropTypes.bool,
fixedHeight: PropTypes.bool,
};
export default Month;