Skip to content
Snippets Groups Projects
Commit 1984a74c authored by Maja Wichrowska's avatar Maja Wichrowska
Browse files

Separates out modifiers from DateRangePicker class and into a DayPickerWithModifiers class

parent 283beb68
No related branches found
No related tags found
No related merge requests found
......@@ -93,17 +93,13 @@
}
}
.DateRangePicker__picker--valid-date-hovered {
.CalendarMonth__day--hovered-span,
.CalendarMonth__day--after-hovered-start {
background: $react-dates-color-primary-shade-4;
border: 1px double $react-dates-color-primary-shade-3;
color: $react-dates-color-secondary;
}
.CalendarMonth__day--hovered-span,
.CalendarMonth__day--after-hovered-start {
background: $react-dates-color-primary-shade-4;
border: 1px double $react-dates-color-primary-shade-3;
color: $react-dates-color-secondary;
}
.DateRangePicker__picker--valid-date-hovered .CalendarMonth__day--selected-start,
.DateRangePicker__picker--valid-date-hovered .CalendarMonth__day--selected-end,
.CalendarMonth__day--selected-start,
.CalendarMonth__day--selected-end,
.CalendarMonth__day--selected {
......
......@@ -4,6 +4,7 @@ var DateRangePickerInputController = require('./lib/components/DateRangePickerIn
var SingleDatePicker = require('./lib/components/SingleDatePicker').default;
var SingleDatePickerInput = require('./lib/components/SingleDatePickerInput').default;
var DayPicker = require('./lib/components/DayPicker').default;
var DayPickerRangeController = require('./lib/components/DayPickerRangeController').default;
var CalendarMonthGrid = require('./lib/components/CalendarMonthGrid').default;
var CalendarMonth = require('./lib/components/CalendarMonth').default;
var CalendarDay = require('./lib/components/CalendarDay').default;
......@@ -29,6 +30,7 @@ module.exports = {
DateRangePickerInput: DateRangePickerInput,
SingleDatePickerInput: SingleDatePickerInput,
DayPicker: DayPicker,
DayPickerRangeController: DayPickerRangeController,
CalendarMonthGrid: CalendarMonthGrid,
CalendarMonth: CalendarMonth,
CalendarDay: CalendarDay,
......
......@@ -3,17 +3,14 @@ import ReactDOM from 'react-dom';
import moment from 'moment';
import cx from 'classnames';
import Portal from 'react-portal';
import includes from 'array-includes';
import isTouchDevice from '../utils/isTouchDevice';
import getResponsiveContainerStyles from '../utils/getResponsiveContainerStyles';
import isInclusivelyAfterDay from '../utils/isInclusivelyAfterDay';
import isNextDay from '../utils/isNextDay';
import isSameDay from '../utils/isSameDay';
import DateRangePickerInputController from './DateRangePickerInputController';
import DayPicker from './DayPicker';
import DayPickerRangeController from './DayPickerRangeController';
import CloseButton from '../svg/close.svg';
......@@ -36,7 +33,6 @@ const defaultProps = {
focusedInput: null,
minimumNights: 1,
isDayBlocked: () => false,
disabledDays: [],
isOutsideRange: day => !isInclusivelyAfterDay(day, moment()),
enableOutsideDays: false,
numberOfMonths: 2,
......@@ -74,15 +70,11 @@ export default class DateRangePicker extends React.Component {
super(props);
this.state = {
dayPickerContainerStyles: {},
hoverDate: null,
};
this.isTouchDevice = isTouchDevice();
this.onOutsideClick = this.onOutsideClick.bind(this);
this.onDayMouseEnter = this.onDayMouseEnter.bind(this);
this.onDayMouseLeave = this.onDayMouseLeave.bind(this);
this.onDayClick = this.onDayClick.bind(this);
this.responsivizePickerPosition = this.responsivizePickerPosition.bind(this);
}
......@@ -96,56 +88,6 @@ export default class DateRangePicker extends React.Component {
window.removeEventListener('resize', this.responsivizePickerPosition);
}
onDayClick(day, modifiers, e) {
const { keepOpenOnDateSelect, minimumNights } = this.props;
if (e) e.preventDefault();
if (includes(modifiers, 'blocked')) return;
const { focusedInput } = this.props;
let { startDate, endDate } = this.props;
if (focusedInput === START_DATE) {
this.props.onFocusChange(END_DATE);
startDate = day;
if (isInclusivelyAfterDay(day, endDate)) {
endDate = null;
}
} else if (focusedInput === END_DATE) {
const firstAllowedEndDate = startDate && startDate.clone().add(minimumNights, 'days');
if (!startDate) {
endDate = day;
this.props.onFocusChange(START_DATE);
} else if (isInclusivelyAfterDay(day, firstAllowedEndDate)) {
endDate = day;
if (!keepOpenOnDateSelect) this.props.onFocusChange(null);
} else {
startDate = day;
endDate = null;
}
}
this.props.onDatesChange({ startDate, endDate });
}
onDayMouseEnter(day) {
if (this.isTouchDevice) return;
this.setState({
hoverDate: day,
});
}
onDayMouseLeave() {
if (this.isTouchDevice) return;
this.setState({
hoverDate: null,
});
}
onOutsideClick() {
const { focusedInput, onFocusChange } = this.props;
if (!focusedInput) return;
......@@ -161,7 +103,6 @@ export default class DateRangePicker extends React.Component {
withFullScreenPortal,
anchorDirection,
} = this.props;
const { hoverDate } = this.state;
const showDatepicker = focusedInput === START_DATE || focusedInput === END_DATE;
const dayPickerClassName = cx('DateRangePicker__picker', {
......@@ -173,7 +114,6 @@ export default class DateRangePicker extends React.Component {
'DateRangePicker__picker--vertical': orientation === VERTICAL_ORIENTATION,
'DateRangePicker__picker--portal': withPortal || withFullScreenPortal,
'DateRangePicker__picker--full-screen-portal': withFullScreenPortal,
'DateRangePicker__picker--valid-date-hovered': hoverDate && !this.isBlocked(hoverDate),
});
return dayPickerClassName;
......@@ -205,64 +145,6 @@ export default class DateRangePicker extends React.Component {
}
}
doesNotMeetMinimumNights(day) {
const { startDate, isOutsideRange, focusedInput, minimumNights } = this.props;
if (focusedInput !== END_DATE) return false;
if (startDate) {
const dayDiff = day.diff(startDate, 'days');
return dayDiff < minimumNights && dayDiff >= 0;
}
return isOutsideRange(moment(day).subtract(minimumNights, 'days'));
}
isDayAfterHoveredStartDate(day) {
const { startDate, endDate, minimumNights } = this.props;
const { hoverDate } = this.state;
return !!startDate && !endDate && isNextDay(hoverDate, day) && minimumNights > 0 &&
isSameDay(hoverDate, startDate);
}
isEndDate(day) {
return isSameDay(day, this.props.endDate);
}
isHovered(day) {
return isSameDay(day, this.state.hoverDate);
}
isInHoveredSpan(day) {
const { startDate, endDate } = this.props;
const { hoverDate } = this.state;
const isForwardRange = !!startDate && !endDate &&
(day.isBetween(startDate, hoverDate) ||
isSameDay(hoverDate, day));
const isBackwardRange = !!endDate && !startDate &&
(day.isBetween(hoverDate, endDate) ||
isSameDay(hoverDate, day));
return isForwardRange || isBackwardRange;
}
isInSelectedSpan(day) {
const { startDate, endDate } = this.props;
return day.isBetween(startDate, endDate);
}
isLastInRange(day) {
return this.isInSelectedSpan(day) && isNextDay(day, this.props.endDate);
}
isStartDate(day) {
return isSameDay(day, this.props.startDate);
}
isBlocked(day) {
const { isDayBlocked, isOutsideRange } = this.props;
return isDayBlocked(day) || isOutsideRange(day) || this.doesNotMeetMinimumNights(day);
}
maybeRenderDayPickerWithPortal() {
const { focusedInput, withPortal, withFullScreenPortal } = this.props;
......@@ -288,34 +170,20 @@ export default class DateRangePicker extends React.Component {
navNext,
onPrevMonthClick,
onNextMonthClick,
onDatesChange,
onFocusChange,
withPortal,
withFullScreenPortal,
enableOutsideDays,
initialVisibleMonth,
focusedInput,
startDate,
endDate,
minimumNights,
keepOpenOnDateSelect,
} = this.props;
const { dayPickerContainerStyles } = this.state;
const modifiers = {
blocked: day => this.isBlocked(day),
'blocked-calendar': day => isDayBlocked(day),
'blocked-out-of-range': day => isOutsideRange(day),
'blocked-minimum-nights': day => this.doesNotMeetMinimumNights(day),
valid: day => !this.isBlocked(day),
// before anything has been set or after both are set
hovered: day => this.isHovered(day),
// while start date has been set, but end date has not been
'hovered-span': day => this.isInHoveredSpan(day),
'after-hovered-start': day => this.isDayAfterHoveredStartDate(day),
'last-in-range': day => this.isLastInRange(day),
// once a start date and end date have been set
'selected-start': day => this.isStartDate(day),
'selected-end': day => this.isEndDate(day),
'selected-span': day => this.isInSelectedSpan(day),
};
const onOutsideClick = !withFullScreenPortal ? this.onOutsideClick : undefined;
return (
......@@ -324,11 +192,10 @@ export default class DateRangePicker extends React.Component {
className={this.getDayPickerContainerClasses()}
style={dayPickerContainerStyles}
>
<DayPicker
<DayPickerRangeController
ref={ref => { this.dayPicker = ref; }}
orientation={orientation}
enableOutsideDays={enableOutsideDays}
modifiers={modifiers}
numberOfMonths={numberOfMonths}
onDayMouseEnter={this.onDayMouseEnter}
onDayMouseLeave={this.onDayMouseLeave}
......@@ -336,6 +203,11 @@ export default class DateRangePicker extends React.Component {
onDayTouchTap={this.onDayClick}
onPrevMonthClick={onPrevMonthClick}
onNextMonthClick={onNextMonthClick}
onDatesChange={onDatesChange}
onFocusChange={onFocusChange}
focusedInput={focusedInput}
startDate={startDate}
endDate={endDate}
monthFormat={monthFormat}
withPortal={withPortal || withFullScreenPortal}
hidden={!focusedInput}
......@@ -343,6 +215,10 @@ export default class DateRangePicker extends React.Component {
onOutsideClick={onOutsideClick}
navPrev={navPrev}
navNext={navNext}
minimumNights={minimumNights}
isOutsideRange={isOutsideRange}
isDayBlocked={isDayBlocked}
keepOpenOnDateSelect={keepOpenOnDateSelect}
/>
{withFullScreenPortal &&
......
......@@ -28,6 +28,10 @@ const propTypes = {
withPortal: PropTypes.bool,
hidden: PropTypes.bool,
initialVisibleMonth: PropTypes.func,
navPrev: PropTypes.node,
navNext: PropTypes.node,
onDayClick: PropTypes.func,
onDayMouseDown: PropTypes.func,
onDayMouseUp: PropTypes.func,
......@@ -39,8 +43,6 @@ const propTypes = {
onPrevMonthClick: PropTypes.func,
onNextMonthClick: PropTypes.func,
onOutsideClick: PropTypes.func,
navPrev: PropTypes.node,
navNext: PropTypes.node,
// i18n
monthFormat: PropTypes.string,
......@@ -53,9 +55,12 @@ const defaultProps = {
orientation: HORIZONTAL_ORIENTATION,
withPortal: false,
hidden: false,
initialVisibleMonth: () => moment(),
navPrev: null,
navNext: null,
initialVisibleMonth: () => moment(),
onDayClick() {},
onDayMouseDown() {},
onDayMouseUp() {},
......
import React, { PropTypes } from 'react';
import momentPropTypes from 'react-moment-proptypes';
import moment from 'moment';
import includes from 'array-includes';
import isTouchDevice from '../utils/isTouchDevice';
import isInclusivelyAfterDay from '../utils/isInclusivelyAfterDay';
import isNextDay from '../utils/isNextDay';
import isSameDay from '../utils/isSameDay';
import FocusedInputShape from '../shapes/FocusedInputShape';
import OrientationShape from '../shapes/OrientationShape';
import {
START_DATE,
END_DATE,
HORIZONTAL_ORIENTATION,
} from '../../constants';
import DayPicker from './DayPicker';
const propTypes = {
startDate: momentPropTypes.momentObj,
endDate: momentPropTypes.momentObj,
onDatesChange: PropTypes.func,
focusedInput: FocusedInputShape,
onFocusChange: PropTypes.func,
keepOpenOnDateSelect: PropTypes.bool,
minimumNights: PropTypes.number,
isOutsideRange: PropTypes.func,
isDayBlocked: PropTypes.func,
// DayPicker props
enableOutsideDays: PropTypes.bool,
numberOfMonths: PropTypes.number,
orientation: OrientationShape,
withPortal: PropTypes.bool,
hidden: PropTypes.bool,
initialVisibleMonth: PropTypes.func,
navPrev: PropTypes.node,
navNext: PropTypes.node,
onDayClick: PropTypes.func,
onDayMouseDown: PropTypes.func,
onDayMouseUp: PropTypes.func,
onDayMouseEnter: PropTypes.func,
onDayMouseLeave: PropTypes.func,
onDayTouchStart: PropTypes.func,
onDayTouchEnd: PropTypes.func,
onDayTouchTap: PropTypes.func,
onPrevMonthClick: PropTypes.func,
onNextMonthClick: PropTypes.func,
onOutsideClick: PropTypes.func,
// i18n
monthFormat: PropTypes.string,
};
const defaultProps = {
onDatesChange() {},
focusedInput: null,
onFocusChange() {},
keepOpenOnDateSelect: false,
minimumNights: 1,
isOutsideRange() {},
isDayBlocked() {},
// DayPicker props
enableOutsideDays: false,
numberOfMonths: 1,
orientation: HORIZONTAL_ORIENTATION,
withPortal: false,
hidden: false,
initialVisibleMonth: () => moment(),
navPrev: null,
navNext: null,
onDayClick() {},
onDayMouseDown() {},
onDayMouseUp() {},
onDayMouseEnter() {},
onDayMouseLeave() {},
onDayTouchStart() {},
onDayTouchTap() {},
onDayTouchEnd() {},
onPrevMonthClick() {},
onNextMonthClick() {},
onOutsideClick() {},
// i18n
monthFormat: 'MMMM YYYY',
};
export default class DayPickerRangeController extends React.Component {
constructor(props) {
super(props);
this.state = {
hoverDate: null,
};
this.isTouchDevice = isTouchDevice();
this.onDayClick = this.onDayClick.bind(this);
this.onDayMouseEnter = this.onDayMouseEnter.bind(this);
this.onDayMouseLeave = this.onDayMouseLeave.bind(this);
}
onDayClick(day, modifiers, e) {
const { keepOpenOnDateSelect, minimumNights } = this.props;
if (e) e.preventDefault();
if (includes(modifiers, 'blocked')) return;
const { focusedInput } = this.props;
let { startDate, endDate } = this.props;
if (focusedInput === START_DATE) {
this.props.onFocusChange(END_DATE);
startDate = day;
if (isInclusivelyAfterDay(day, endDate)) {
endDate = null;
}
} else if (focusedInput === END_DATE) {
const firstAllowedEndDate = startDate && startDate.clone().add(minimumNights, 'days');
if (!startDate) {
endDate = day;
this.props.onFocusChange(START_DATE);
} else if (isInclusivelyAfterDay(day, firstAllowedEndDate)) {
endDate = day;
if (!keepOpenOnDateSelect) this.props.onFocusChange(null);
} else {
startDate = day;
endDate = null;
}
}
this.props.onDatesChange({ startDate, endDate });
}
onDayMouseEnter(day) {
if (this.isTouchDevice) return;
this.setState({
hoverDate: day,
});
}
onDayMouseLeave() {
if (this.isTouchDevice) return;
this.setState({
hoverDate: null,
});
}
doesNotMeetMinimumNights(day) {
const { startDate, isOutsideRange, focusedInput, minimumNights } = this.props;
if (focusedInput !== END_DATE) return false;
if (startDate) {
const dayDiff = day.diff(startDate, 'days');
return dayDiff < minimumNights && dayDiff >= 0;
}
return isOutsideRange(moment(day).subtract(minimumNights, 'days'));
}
isDayAfterHoveredStartDate(day) {
const { startDate, endDate, minimumNights } = this.props;
const { hoverDate } = this.state;
return !!startDate && !endDate && !this.isBlocked(day) && isNextDay(hoverDate, day) &&
minimumNights > 0 && isSameDay(hoverDate, startDate);
}
isEndDate(day) {
return isSameDay(day, this.props.endDate);
}
isHovered(day) {
return isSameDay(day, this.state.hoverDate);
}
isInHoveredSpan(day) {
const { startDate, endDate } = this.props;
const { hoverDate } = this.state;
const isForwardRange = !!startDate && !endDate &&
(day.isBetween(startDate, hoverDate) ||
isSameDay(hoverDate, day));
const isBackwardRange = !!endDate && !startDate &&
(day.isBetween(hoverDate, endDate) ||
isSameDay(hoverDate, day));
const isValidDayHovered = hoverDate && !this.isBlocked(hoverDate);
return (isForwardRange || isBackwardRange) && isValidDayHovered;
}
isInSelectedSpan(day) {
const { startDate, endDate } = this.props;
return day.isBetween(startDate, endDate);
}
isLastInRange(day) {
return this.isInSelectedSpan(day) && isNextDay(day, this.props.endDate);
}
isStartDate(day) {
return isSameDay(day, this.props.startDate);
}
isBlocked(day) {
const { isDayBlocked, isOutsideRange } = this.props;
return isDayBlocked(day) || isOutsideRange(day) || this.doesNotMeetMinimumNights(day);
}
render() {
const {
isDayBlocked,
isOutsideRange,
numberOfMonths,
orientation,
monthFormat,
navPrev,
navNext,
onOutsideClick,
onPrevMonthClick,
onNextMonthClick,
withPortal,
enableOutsideDays,
initialVisibleMonth,
focusedInput,
} = this.props;
const modifiers = {
blocked: day => this.isBlocked(day),
'blocked-calendar': day => isDayBlocked(day),
'blocked-out-of-range': day => isOutsideRange(day),
'blocked-minimum-nights': day => this.doesNotMeetMinimumNights(day),
valid: day => !this.isBlocked(day),
// before anything has been set or after both are set
hovered: day => this.isHovered(day),
// while start date has been set, but end date has not been
'hovered-span': day => this.isInHoveredSpan(day),
'after-hovered-start': day => this.isDayAfterHoveredStartDate(day),
'last-in-range': day => this.isLastInRange(day),
// once a start date and end date have been set
'selected-start': day => this.isStartDate(day),
'selected-end': day => this.isEndDate(day),
'selected-span': day => this.isInSelectedSpan(day),
};
return (
<DayPicker
ref={ref => { this.dayPicker = ref; }}
orientation={orientation}
enableOutsideDays={enableOutsideDays}
modifiers={modifiers}
numberOfMonths={numberOfMonths}
onDayMouseEnter={this.onDayMouseEnter}
onDayMouseLeave={this.onDayMouseLeave}
onDayMouseDown={this.onDayClick}
onDayTouchTap={this.onDayClick}
onPrevMonthClick={onPrevMonthClick}
onNextMonthClick={onNextMonthClick}
monthFormat={monthFormat}
withPortal={withPortal}
hidden={!focusedInput}
initialVisibleMonth={initialVisibleMonth}
onOutsideClick={onOutsideClick}
navPrev={navPrev}
navNext={navNext}
/>
);
}
}
DayPickerRangeController.propTypes = propTypes;
DayPickerRangeController.defaultProps = defaultProps;
......@@ -161,11 +161,16 @@ storiesOf('DateRangePicker', module)
minimumNights={0}
/>
))
.add('allows previous three month only', () => (
.add('allows all days', () => (
<DateRangePickerWrapper
isOutsideRange={day => false}
/>
))
.add('allows next two weeks only', () => (
<DateRangePickerWrapper
isOutsideRange={day =>
!isInclusivelyAfterDay(day, moment().startOf('month').subtract(3, 'months')) ||
isInclusivelyAfterDay(day, moment().startOf('month'))
!isInclusivelyAfterDay(day, moment()) ||
isInclusivelyAfterDay(day, moment().add(2, 'weeks'))
}
/>
))
......
......@@ -75,6 +75,22 @@ describe('DateRangePickerInputController', () => {
});
});
describe('#onClearFocus', () => {
it('calls props.onFocusChange', () => {
const onFocusChangeStub = sinon.stub();
const wrapper = shallow(<DateRangePickerInputController onFocusChange={onFocusChangeStub} />);
wrapper.instance().onClearFocus();
expect(onFocusChangeStub.callCount).to.equal(1);
});
it('calls props.onFocusChange with null arg', () => {
const onFocusChangeStub = sinon.stub();
const wrapper = shallow(<DateRangePickerInputController onFocusChange={onFocusChangeStub} />);
wrapper.instance().onClearFocus();
expect(onFocusChangeStub.calledWith(null)).to.equal(true);
});
});
describe('#onEndDateChange', () => {
describe('is a valid end date', () => {
const validFutureDateString = moment(today).add(10, 'days').format('YYYY-MM-DD');
......
This diff is collapsed.
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment