react-date-range-headless
Version:
A React component for choosing dates and date ranges. A fork of hypeserver/react-date-range in which the Headless UI Listbox (Select) uses instead html select and options
175 lines (165 loc) • 5.8 kB
JavaScript
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,
disableReverseDates,
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 (!disableReverseDates) {
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}
onHoverDate={this.props.onHoverDate}
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: [],
disableReverseDates: false,
};
DateRange.propTypes = {
...Calendar.propTypes,
onChange: PropTypes.func,
onRangeFocusChange: PropTypes.func,
className: PropTypes.string,
ranges: PropTypes.arrayOf(rangeShape),
moveRangeOnFirstSelection: PropTypes.bool,
retainEndDateOnFirstSelection: PropTypes.bool,
disableReverseDates: PropTypes.bool,
};
export default DateRange;