Skip to content

Commit d8aa65a

Browse files
committed
Allow same day input when minimumNights={0}
Fixes react-dates#353.
1 parent c96a237 commit d8aa65a

File tree

3 files changed

+236
-25
lines changed

3 files changed

+236
-25
lines changed

src/components/DateRangePicker.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ export default class DateRangePicker extends React.Component {
400400
readOnly,
401401
phrases,
402402
isOutsideRange,
403+
minimumNights,
403404
withPortal,
404405
withFullScreenPortal,
405406
displayFormat,
@@ -439,6 +440,7 @@ export default class DateRangePicker extends React.Component {
439440
reopenPickerOnClearDates={reopenPickerOnClearDates}
440441
keepOpenOnDateSelect={keepOpenOnDateSelect}
441442
isOutsideRange={isOutsideRange}
443+
minimumNights={minimumNights}
442444
withFullScreenPortal={withFullScreenPortal}
443445
onDatesChange={onDatesChange}
444446
onFocusChange={this.onDateRangePickerInputFocus}

src/components/DateRangePickerInputController.jsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import toISODateString from '../utils/toISODateString';
1616

1717
import isInclusivelyAfterDay from '../utils/isInclusivelyAfterDay';
1818
import isInclusivelyBeforeDay from '../utils/isInclusivelyBeforeDay';
19+
import isSameDay from '../utils/isSameDay';
1920

2021
import { START_DATE, END_DATE } from '../../constants';
2122

@@ -41,6 +42,7 @@ const propTypes = forbidExtraProps({
4142
keepOpenOnDateSelect: PropTypes.bool,
4243
reopenPickerOnClearDates: PropTypes.bool,
4344
withFullScreenPortal: PropTypes.bool,
45+
minimumNights: PropTypes.number,
4446
isOutsideRange: PropTypes.func,
4547
displayFormat: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
4648

@@ -85,6 +87,7 @@ const defaultProps = {
8587
keepOpenOnDateSelect: false,
8688
reopenPickerOnClearDates: false,
8789
withFullScreenPortal: false,
90+
minimumNights: 1,
8891
isOutsideRange: day => !isInclusivelyAfterDay(day, moment()),
8992
displayFormat: () => moment.localeData().longDateFormat('L'),
9093

@@ -130,14 +133,16 @@ export default class DateRangePickerInputController extends React.Component {
130133
const {
131134
startDate,
132135
isOutsideRange,
136+
minimumNights,
133137
keepOpenOnDateSelect,
134138
onDatesChange,
135139
} = this.props;
136140

137141
const endDate = toMomentObject(endDateString, this.getDisplayFormat());
138142

139143
const isEndDateValid = endDate && !isOutsideRange(endDate) &&
140-
!isInclusivelyBeforeDay(endDate, startDate);
144+
(!isInclusivelyBeforeDay(endDate, startDate) ||
145+
(minimumNights === 0 && isSameDay(endDate, startDate)));
141146
if (isEndDateValid) {
142147
onDatesChange({ startDate, endDate });
143148
if (!keepOpenOnDateSelect) this.onClearFocus();
@@ -166,10 +171,11 @@ export default class DateRangePickerInputController extends React.Component {
166171
const startDate = toMomentObject(startDateString, this.getDisplayFormat());
167172

168173
let { endDate } = this.props;
169-
const { isOutsideRange, onDatesChange, onFocusChange } = this.props;
174+
const { isOutsideRange, minimumNights, onDatesChange, onFocusChange } = this.props;
170175
const isStartDateValid = startDate && !isOutsideRange(startDate);
171176
if (isStartDateValid) {
172-
if (isInclusivelyBeforeDay(endDate, startDate)) {
177+
if (isInclusivelyBeforeDay(endDate, startDate) &&
178+
!(minimumNights === 0 && isSameDay(endDate, startDate))) {
173179
endDate = null;
174180
}
175181

test/components/DateRangePickerInputController_spec.jsx

Lines changed: 225 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -113,33 +113,168 @@ describe('DateRangePickerInputController', () => {
113113
describe('#onEndDateChange', () => {
114114
describe('is a valid end date', () => {
115115
const validFutureDateString = moment(today).add(10, 'days').format('YYYY-MM-DD');
116-
it('calls props.onDatesChange with correct arguments', () => {
117-
const onDatesChangeStub = sinon.stub();
118-
const wrapper =
119-
shallow(<DateRangePickerInputController onDatesChange={onDatesChangeStub} />);
120-
wrapper.instance().onEndDateChange(validFutureDateString);
121-
expect(onDatesChangeStub.callCount).to.equal(1);
116+
describe('when props.startDate is not provided', () => {
117+
it('calls props.onDatesChange with provided end date', () => {
118+
const onDatesChangeStub = sinon.stub();
119+
const wrapper =
120+
shallow(<DateRangePickerInputController onDatesChange={onDatesChangeStub} />);
121+
wrapper.instance().onEndDateChange(validFutureDateString);
122+
expect(onDatesChangeStub.callCount).to.equal(1);
123+
124+
const onDatesChangeArgs = onDatesChangeStub.getCall(0).args[0];
125+
expect(onDatesChangeArgs.startDate).to.equal(wrapper.props().startDate);
126+
expect(
127+
isSameDay(onDatesChangeArgs.endDate, moment(validFutureDateString))).to.equal(true);
128+
});
129+
130+
describe('props.onFocusChange', () => {
131+
it('is called once', () => {
132+
const onFocusChangeStub = sinon.stub();
133+
const wrapper =
134+
shallow(<DateRangePickerInputController onFocusChange={onFocusChangeStub} />);
135+
wrapper.instance().onEndDateChange(validFutureDateString);
136+
expect(onFocusChangeStub.callCount).to.equal(1);
137+
});
122138

123-
const onDatesChangeArgs = onDatesChangeStub.getCall(0).args[0];
124-
expect(onDatesChangeArgs.startDate).to.equal(wrapper.props().startDate);
125-
expect(isSameDay(onDatesChangeArgs.endDate, moment(validFutureDateString))).to.equal(true);
139+
it('is called with null arg', () => {
140+
const onFocusChangeStub = sinon.stub();
141+
const wrapper =
142+
shallow(<DateRangePickerInputController onFocusChange={onFocusChangeStub} />);
143+
wrapper.instance().onEndDateChange(validFutureDateString);
144+
expect(onFocusChangeStub.calledWith(null)).to.equal(true);
145+
});
146+
});
126147
});
127148

128-
describe('props.onFocusChange', () => {
129-
it('is called once', () => {
130-
const onFocusChangeStub = sinon.stub();
131-
const wrapper =
132-
shallow(<DateRangePickerInputController onFocusChange={onFocusChangeStub} />);
149+
describe('is before props.startDate', () => {
150+
const startDate = moment(today).add(15, 'days');
151+
it('calls props.onDatesChange with props.startDate and null end date', () => {
152+
const onDatesChangeStub = sinon.stub();
153+
const wrapper = shallow(
154+
<DateRangePickerInputController
155+
onDatesChange={onDatesChangeStub}
156+
startDate={startDate}
157+
/>);
133158
wrapper.instance().onEndDateChange(validFutureDateString);
134-
expect(onFocusChangeStub.callCount).to.equal(1);
159+
expect(onDatesChangeStub.callCount).to.equal(1);
160+
161+
const onDatesChangeArgs = onDatesChangeStub.getCall(0).args[0];
162+
expect(onDatesChangeArgs.startDate).to.equal(startDate);
163+
expect(onDatesChangeArgs.endDate).to.equal(null);
135164
});
136165

137-
it('is called with null arg', () => {
138-
const onFocusChangeStub = sinon.stub();
139-
const wrapper =
140-
shallow(<DateRangePickerInputController onFocusChange={onFocusChangeStub} />);
166+
describe('props.onFocusChange', () => {
167+
it('is called once', () => {
168+
const onFocusChangeStub = sinon.stub();
169+
const wrapper =
170+
shallow(<DateRangePickerInputController onFocusChange={onFocusChangeStub} />);
171+
wrapper.instance().onEndDateChange(validFutureDateString);
172+
expect(onFocusChangeStub.callCount).to.equal(1);
173+
});
174+
175+
it('is called with null arg', () => {
176+
const onFocusChangeStub = sinon.stub();
177+
const wrapper =
178+
shallow(<DateRangePickerInputController onFocusChange={onFocusChangeStub} />);
179+
wrapper.instance().onEndDateChange(validFutureDateString);
180+
expect(onFocusChangeStub.calledWith(null)).to.equal(true);
181+
});
182+
});
183+
});
184+
185+
describe('is after props.startDate', () => {
186+
const startDate = moment(today);
187+
it('calls props.onDatesChange with props.startDate and provided end date', () => {
188+
const onDatesChangeStub = sinon.stub();
189+
const wrapper = shallow(
190+
<DateRangePickerInputController
191+
onDatesChange={onDatesChangeStub}
192+
startDate={startDate}
193+
/>);
141194
wrapper.instance().onEndDateChange(validFutureDateString);
142-
expect(onFocusChangeStub.calledWith(null)).to.equal(true);
195+
expect(onDatesChangeStub.callCount).to.equal(1);
196+
197+
const onDatesChangeArgs = onDatesChangeStub.getCall(0).args[0];
198+
const futureDate = moment(validFutureDateString);
199+
expect(onDatesChangeArgs.startDate).to.equal(startDate);
200+
expect(isSameDay(onDatesChangeArgs.endDate, futureDate)).to.equal(true);
201+
});
202+
203+
describe('props.onFocusChange', () => {
204+
it('is called once', () => {
205+
const onFocusChangeStub = sinon.stub();
206+
const wrapper =
207+
shallow(<DateRangePickerInputController onFocusChange={onFocusChangeStub} />);
208+
wrapper.instance().onEndDateChange(validFutureDateString);
209+
expect(onFocusChangeStub.callCount).to.equal(1);
210+
});
211+
212+
it('is called with null arg', () => {
213+
const onFocusChangeStub = sinon.stub();
214+
const wrapper =
215+
shallow(<DateRangePickerInputController onFocusChange={onFocusChangeStub} />);
216+
wrapper.instance().onEndDateChange(validFutureDateString);
217+
expect(onFocusChangeStub.calledWith(null)).to.equal(true);
218+
});
219+
});
220+
});
221+
222+
describe('is the same day as props.startDate', () => {
223+
const startDate = moment(today).add(10, 'days');
224+
225+
describe('props.minimumNights is 0', () => {
226+
it('calls props.onDatesChange with props.startDate and provided end date', () => {
227+
const onDatesChangeStub = sinon.stub();
228+
const wrapper = shallow(
229+
<DateRangePickerInputController
230+
onDatesChange={onDatesChangeStub}
231+
startDate={startDate}
232+
minimumNights={0}
233+
/>);
234+
wrapper.instance().onEndDateChange(validFutureDateString);
235+
expect(onDatesChangeStub.callCount).to.equal(1);
236+
237+
const onDatesChangeArgs = onDatesChangeStub.getCall(0).args[0];
238+
const futureDate = moment(validFutureDateString);
239+
expect(onDatesChangeArgs.startDate).to.equal(startDate);
240+
expect(isSameDay(onDatesChangeArgs.endDate, futureDate)).to.equal(true);
241+
});
242+
});
243+
244+
describe('props.minimumNights is greater than 0', () => {
245+
it('calls props.onDatesChange with props.startDate and null end date', () => {
246+
const onDatesChangeStub = sinon.stub();
247+
const wrapper = shallow(
248+
<DateRangePickerInputController
249+
onDatesChange={onDatesChangeStub}
250+
startDate={startDate}
251+
minimumNights={1}
252+
/>);
253+
wrapper.instance().onEndDateChange(validFutureDateString);
254+
expect(onDatesChangeStub.callCount).to.equal(1);
255+
256+
const onDatesChangeArgs = onDatesChangeStub.getCall(0).args[0];
257+
expect(onDatesChangeArgs.startDate).to.equal(startDate);
258+
expect(onDatesChangeArgs.endDate).to.equal(null);
259+
});
260+
});
261+
262+
describe('props.onFocusChange', () => {
263+
it('is called once', () => {
264+
const onFocusChangeStub = sinon.stub();
265+
const wrapper =
266+
shallow(<DateRangePickerInputController onFocusChange={onFocusChangeStub} />);
267+
wrapper.instance().onEndDateChange(validFutureDateString);
268+
expect(onFocusChangeStub.callCount).to.equal(1);
269+
});
270+
271+
it('is called with null arg', () => {
272+
const onFocusChangeStub = sinon.stub();
273+
const wrapper =
274+
shallow(<DateRangePickerInputController onFocusChange={onFocusChangeStub} />);
275+
wrapper.instance().onEndDateChange(validFutureDateString);
276+
expect(onFocusChangeStub.calledWith(null)).to.equal(true);
277+
});
143278
});
144279
});
145280
});
@@ -315,7 +450,7 @@ describe('DateRangePickerInputController', () => {
315450
const validFutureDateString = moment(today).add(5, 'days').format('YYYY-MM-DD');
316451
describe('is before props.endDate', () => {
317452
const endDate = moment(today).add(10, 'days');
318-
it('calls props.onDatesChange with correct arguments', () => {
453+
it('calls props.onDatesChange provided start date and props.endDate', () => {
319454
const onDatesChangeStub = sinon.stub();
320455
const wrapper = shallow(
321456
<DateRangePickerInputController onDatesChange={onDatesChangeStub} endDate={endDate} />,
@@ -358,7 +493,7 @@ describe('DateRangePickerInputController', () => {
358493

359494
describe('is after props.endDate', () => {
360495
const endDate = moment(today);
361-
it('calls props.onDatesChange with correct arguments', () => {
496+
it('calls props.onDatesChange with provided start date and null end date', () => {
362497
const onDatesChangeStub = sinon.stub();
363498
const wrapper = shallow(
364499
<DateRangePickerInputController
@@ -401,6 +536,74 @@ describe('DateRangePickerInputController', () => {
401536
});
402537
});
403538
});
539+
540+
describe('is the same day as props.endDate', () => {
541+
const endDate = moment(today).add(5, 'days');
542+
543+
describe('props.minimumNights is 0', () => {
544+
it('calls props.onDatesChange with provided start date and props.endDate', () => {
545+
const onDatesChangeStub = sinon.stub();
546+
const wrapper = shallow(
547+
<DateRangePickerInputController
548+
onDatesChange={onDatesChangeStub}
549+
endDate={endDate}
550+
minimumNights={0}
551+
/>);
552+
wrapper.instance().onStartDateChange(validFutureDateString);
553+
expect(onDatesChangeStub.callCount).to.equal(1);
554+
555+
const onDatesChangeArgs = onDatesChangeStub.getCall(0).args[0];
556+
const futureDate = moment(validFutureDateString);
557+
expect(isSameDay(onDatesChangeArgs.startDate, futureDate)).to.equal(true);
558+
expect(onDatesChangeArgs.endDate).to.equal(endDate);
559+
});
560+
});
561+
562+
describe('props.minimumNights is greater than 0', () => {
563+
it('calls props.onDatesChange with provided start date and null end date', () => {
564+
const onDatesChangeStub = sinon.stub();
565+
const wrapper = shallow(
566+
<DateRangePickerInputController
567+
onDatesChange={onDatesChangeStub}
568+
endDate={endDate}
569+
minimumNights={1}
570+
/>);
571+
wrapper.instance().onStartDateChange(validFutureDateString);
572+
expect(onDatesChangeStub.callCount).to.equal(1);
573+
574+
const onDatesChangeArgs = onDatesChangeStub.getCall(0).args[0];
575+
const futureDate = moment(validFutureDateString);
576+
expect(isSameDay(onDatesChangeArgs.startDate, futureDate)).to.equal(true);
577+
expect(onDatesChangeArgs.endDate).to.equal(null);
578+
});
579+
});
580+
581+
describe('props.onFocusChange', () => {
582+
it('is called once', () => {
583+
const onFocusChangeStub = sinon.stub();
584+
const wrapper = shallow(
585+
<DateRangePickerInputController
586+
onFocusChange={onFocusChangeStub}
587+
endDate={endDate}
588+
/>,
589+
);
590+
wrapper.instance().onStartDateChange(validFutureDateString);
591+
expect(onFocusChangeStub.callCount).to.equal(1);
592+
});
593+
594+
it('is called with END_DATE arg', () => {
595+
const onFocusChangeStub = sinon.stub();
596+
const wrapper = shallow(
597+
<DateRangePickerInputController
598+
onFocusChange={onFocusChangeStub}
599+
endDate={endDate}
600+
/>,
601+
);
602+
wrapper.instance().onStartDateChange(validFutureDateString);
603+
expect(onFocusChangeStub.calledWith(END_DATE)).to.equal(true);
604+
});
605+
});
606+
});
404607
});
405608

406609
describe('matches custom display format', () => {

0 commit comments

Comments
 (0)