Skip to content

Commit f63972d

Browse files
Amwammartijnrusschen
authored andcommitted
Add error emitter for user input errors (Hacker0x01#1354)
* Add error emitter for user input errors * Fixed tests
1 parent df59d1d commit f63972d

File tree

2 files changed

+95
-52
lines changed

2 files changed

+95
-52
lines changed

src/index.jsx

Lines changed: 63 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import Calendar from "./calendar";
2-
import React from "react";
3-
import PropTypes from "prop-types";
4-
import PopperComponent, { popperPlacementPositions } from "./popper_component";
5-
import classnames from "classnames";
1+
import Calendar from './calendar';
2+
import React from 'react';
3+
import PropTypes from 'prop-types';
4+
import PopperComponent, { popperPlacementPositions } from './popper_component';
5+
import classnames from 'classnames';
66
import {
77
newDate,
88
now,
@@ -34,13 +34,13 @@ import {
3434
safeDateFormat,
3535
getHightLightDaysMap,
3636
getYear,
37-
getMonth
38-
} from "./date_utils";
39-
import onClickOutside from "react-onclickoutside";
37+
getMonth,
38+
} from './date_utils';
39+
import onClickOutside from 'react-onclickoutside';
4040

41-
export { default as CalendarContainer } from "./calendar_container";
41+
export { default as CalendarContainer } from './calendar_container';
4242

43-
const outsideClickIgnoreClass = "react-datepicker-ignore-onclickoutside";
43+
const outsideClickIgnoreClass = 'react-datepicker-ignore-onclickoutside';
4444
const WrappedCalendar = onClickOutside(Calendar);
4545

4646
// Compares dates year+month combinations
@@ -65,6 +65,7 @@ function hasSelectionChanged(date1, date2) {
6565
/**
6666
* General datepicker component.
6767
*/
68+
const INPUT_ERR_1 = 'Date input not valid.';
6869

6970
export default class DatePicker extends React.Component {
7071
static propTypes = {
@@ -84,7 +85,7 @@ export default class DatePicker extends React.Component {
8485
dayClassName: PropTypes.func,
8586
disabled: PropTypes.bool,
8687
disabledKeyboardNavigation: PropTypes.bool,
87-
dropdownMode: PropTypes.oneOf(["scroll", "select"]).isRequired,
88+
dropdownMode: PropTypes.oneOf(['scroll', 'select']).isRequired,
8889
endDate: PropTypes.object,
8990
excludeDates: PropTypes.array,
9091
filterDate: PropTypes.func,
@@ -113,6 +114,7 @@ export default class DatePicker extends React.Component {
113114
onKeyDown: PropTypes.func,
114115
onMonthChange: PropTypes.func,
115116
onYearChange: PropTypes.func,
117+
onInputError: PropTypes.func,
116118
open: PropTypes.bool,
117119
openToDate: PropTypes.object,
118120
peekNextMonth: PropTypes.bool,
@@ -166,12 +168,12 @@ export default class DatePicker extends React.Component {
166168
static get defaultProps() {
167169
return {
168170
allowSameDay: false,
169-
dateFormat: "L",
170-
dateFormatCalendar: "MMMM YYYY",
171+
dateFormat: 'L',
172+
dateFormatCalendar: 'MMMM YYYY',
171173
onChange() {},
172174
disabled: false,
173175
disabledKeyboardNavigation: false,
174-
dropdownMode: "scroll",
176+
dropdownMode: 'scroll',
175177
onFocus() {},
176178
onBlur() {},
177179
onKeyDown() {},
@@ -181,15 +183,16 @@ export default class DatePicker extends React.Component {
181183
onMonthChange() {},
182184
preventOpenOnFocus: false,
183185
onYearChange() {},
186+
onInputError() {},
184187
monthsShown: 1,
185188
readOnly: false,
186189
withPortal: false,
187190
shouldCloseOnSelect: true,
188191
showTimeSelect: false,
189192
timeIntervals: 30,
190-
timeCaption: "Time",
191-
previousMonthButtonLabel: "Previous Month",
192-
nextMonthButtonLabel: "Next month"
193+
timeCaption: 'Time',
194+
previousMonthButtonLabel: 'Previous Month',
195+
nextMonthButtonLabel: 'Next month',
193196
};
194197
}
195198

@@ -207,7 +210,7 @@ export default class DatePicker extends React.Component {
207210
}
208211
if (prevProps.highlightDates !== this.props.highlightDates) {
209212
this.setState({
210-
highlightDates: getHightLightDaysMap(this.props.highlightDates)
213+
highlightDates: getHightLightDaysMap(this.props.highlightDates),
211214
});
212215
}
213216
if (
@@ -250,7 +253,7 @@ export default class DatePicker extends React.Component {
250253
// transforming highlighted days (perhaps nested array)
251254
// to flat Map for faster access in day.jsx
252255
highlightDates: getHightLightDaysMap(this.props.highlightDates),
253-
focused: false
256+
focused: false,
254257
};
255258
};
256259

@@ -273,9 +276,11 @@ export default class DatePicker extends React.Component {
273276
open && this.state.open
274277
? this.state.preSelection
275278
: this.calcInitialState().preSelection,
276-
lastPreSelectChange: PRESELECT_CHANGE_VIA_NAVIGATE
279+
lastPreSelectChange: PRESELECT_CHANGE_VIA_NAVIGATE,
277280
});
278281
};
282+
inputOk = () =>
283+
isMoment(this.state.preSelection) || isDate(this.state.preSelection);
279284

280285
isCalendarOpen = () =>
281286
this.props.open === undefined
@@ -330,15 +335,15 @@ export default class DatePicker extends React.Component {
330335
if (this.props.onChangeRaw) {
331336
this.props.onChangeRaw.apply(this, allArgs);
332337
if (
333-
typeof event.isDefaultPrevented !== "function" ||
338+
typeof event.isDefaultPrevented !== 'function' ||
334339
event.isDefaultPrevented()
335340
) {
336341
return;
337342
}
338343
}
339344
this.setState({
340345
inputValue: event.target.value,
341-
lastPreSelectChange: PRESELECT_CHANGE_VIA_INPUT
346+
lastPreSelectChange: PRESELECT_CHANGE_VIA_INPUT,
342347
});
343348
const date = parseDate(event.target.value, this.props);
344349
if (date || !event.target.value) {
@@ -352,7 +357,7 @@ export default class DatePicker extends React.Component {
352357
this.setState({ preventFocus: true }, () => {
353358
this.preventFocusTimeout = setTimeout(
354359
() => this.setState({ preventFocus: false }),
355-
50
360+
50,
356361
);
357362
return this.preventFocusTimeout;
358363
});
@@ -391,12 +396,12 @@ export default class DatePicker extends React.Component {
391396
changedDate = setTime(newDate(changedDate), {
392397
hour: getHour(selected),
393398
minute: getMinute(selected),
394-
second: getSecond(selected)
399+
second: getSecond(selected),
395400
});
396401
}
397402
if (!this.props.inline) {
398403
this.setState({
399-
preSelection: changedDate
404+
preSelection: changedDate,
400405
});
401406
}
402407
}
@@ -412,15 +417,15 @@ export default class DatePicker extends React.Component {
412417

413418
setPreSelection = date => {
414419
const isDateRangePresent =
415-
typeof this.props.minDate !== "undefined" &&
416-
typeof this.props.maxDate !== "undefined";
420+
typeof this.props.minDate !== 'undefined' &&
421+
typeof this.props.maxDate !== 'undefined';
417422
const isValidDateSelection =
418423
isDateRangePresent && date
419424
? isDayInRange(date, this.props.minDate, this.props.maxDate)
420425
: true;
421426
if (isValidDateSelection) {
422427
this.setState({
423-
preSelection: date
428+
preSelection: date,
424429
});
425430
}
426431
};
@@ -431,11 +436,11 @@ export default class DatePicker extends React.Component {
431436
: this.getPreSelection();
432437
let changedDate = setTime(cloneDate(selected), {
433438
hour: getHour(time),
434-
minute: getMinute(time)
439+
minute: getMinute(time),
435440
});
436441

437442
this.setState({
438-
preSelection: changedDate
443+
preSelection: changedDate,
439444
});
440445

441446
this.props.onChange(changedDate);
@@ -459,17 +464,16 @@ export default class DatePicker extends React.Component {
459464
!this.props.inline &&
460465
!this.props.preventOpenOnFocus
461466
) {
462-
if (eventKey === "ArrowDown" || eventKey === "ArrowUp") {
467+
if (eventKey === 'ArrowDown' || eventKey === 'ArrowUp') {
463468
this.onInputClick();
464469
}
465470
return;
466471
}
467472
const copy = newDate(this.state.preSelection);
468-
if (eventKey === "Enter") {
473+
if (eventKey === 'Enter') {
469474
event.preventDefault();
470475
if (
471-
(isMoment(this.state.preSelection) ||
472-
isDate(this.state.preSelection)) &&
476+
this.inputOk() &&
473477
this.state.lastPreSelectChange === PRESELECT_CHANGE_VIA_NAVIGATE
474478
) {
475479
this.handleSelect(copy, event);
@@ -481,45 +485,53 @@ export default class DatePicker extends React.Component {
481485

482486
this.setOpen(false);
483487
}
484-
} else if (eventKey === "Escape") {
488+
} else if (eventKey === 'Escape') {
485489
event.preventDefault();
486490

487491
this.input.blur();
488492
this.props.onBlur(copy);
489493
this.cancelFocusInput();
490494

491495
this.setOpen(false);
492-
} else if (eventKey === "Tab") {
496+
if (!this.inputOk()) {
497+
this.props.onInputError({ code: 1, msg: INPUT_ERR_1 });
498+
}
499+
} else if (eventKey === 'Tab') {
493500
this.setOpen(false);
494501
} else if (!this.props.disabledKeyboardNavigation) {
495502
let newSelection;
496503
switch (eventKey) {
497-
case "ArrowLeft":
504+
case 'ArrowLeft':
498505
newSelection = subtractDays(copy, 1);
499506
break;
500-
case "ArrowRight":
507+
case 'ArrowRight':
501508
newSelection = addDays(copy, 1);
502509
break;
503-
case "ArrowUp":
510+
case 'ArrowUp':
504511
newSelection = subtractWeeks(copy, 1);
505512
break;
506-
case "ArrowDown":
513+
case 'ArrowDown':
507514
newSelection = addWeeks(copy, 1);
508515
break;
509-
case "PageUp":
516+
case 'PageUp':
510517
newSelection = subtractMonths(copy, 1);
511518
break;
512-
case "PageDown":
519+
case 'PageDown':
513520
newSelection = addMonths(copy, 1);
514521
break;
515-
case "Home":
522+
case 'Home':
516523
newSelection = subtractYears(copy, 1);
517524
break;
518-
case "End":
525+
case 'End':
519526
newSelection = addYears(copy, 1);
520527
break;
521528
}
522-
if (!newSelection) return; // Let the input component handle this keydown
529+
if (!newSelection) {
530+
if (this.props.onInputError) {
531+
this.props.onInputError({ code: 1, msg: INPUT_ERR_1 });
532+
}
533+
return; // Let the input component handle this keydown
534+
}
523535
event.preventDefault();
524536
this.setState({ lastPreSelectChange: PRESELECT_CHANGE_VIA_NAVIGATE });
525537
if (this.props.adjustDateOnChange) {
@@ -628,11 +640,11 @@ export default class DatePicker extends React.Component {
628640
});
629641

630642
const customInput = this.props.customInput || <input type="text" />;
631-
const customInputRef = this.props.customInputRef || "ref";
643+
const customInputRef = this.props.customInputRef || 'ref';
632644
const inputValue =
633-
typeof this.props.value === "string"
645+
typeof this.props.value === 'string'
634646
? this.props.value
635-
: typeof this.state.inputValue === "string"
647+
: typeof this.state.inputValue === 'string'
636648
? this.state.inputValue
637649
: safeDateFormat(this.props.selected, this.props);
638650

@@ -656,7 +668,7 @@ export default class DatePicker extends React.Component {
656668
title: this.props.title,
657669
readOnly: this.props.readOnly,
658670
required: this.props.required,
659-
tabIndex: this.props.tabIndex
671+
tabIndex: this.props.tabIndex,
660672
});
661673
};
662674

@@ -718,5 +730,5 @@ export default class DatePicker extends React.Component {
718730
}
719731
}
720732

721-
const PRESELECT_CHANGE_VIA_INPUT = "input";
722-
const PRESELECT_CHANGE_VIA_NAVIGATE = "navigate";
733+
const PRESELECT_CHANGE_VIA_INPUT = 'input';
734+
const PRESELECT_CHANGE_VIA_NAVIGATE = 'navigate';

test/datepicker_test.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,8 +481,15 @@ describe("DatePicker", () => {
481481
var testFormat = "YYYY-MM-DD";
482482
var exactishFormat = "YYYY-MM-DD HH: ZZ";
483483
var callback = sandbox.spy();
484+
var onInputErrorCallback = sandbox.spy();
485+
484486
var datePicker = TestUtils.renderIntoDocument(
485-
<DatePicker selected={m} onChange={callback} {...opts} />
487+
<DatePicker
488+
selected={m}
489+
onChange={callback}
490+
onInputError={onInputErrorCallback}
491+
{...opts}
492+
/>
486493
);
487494
var dateInput = datePicker.input;
488495
var nodeInput = ReactDOM.findDOMNode(dateInput);
@@ -493,6 +500,7 @@ describe("DatePicker", () => {
493500
testFormat,
494501
exactishFormat,
495502
callback,
503+
onInputErrorCallback,
496504
datePicker,
497505
dateInput,
498506
nodeInput
@@ -611,6 +619,7 @@ describe("DatePicker", () => {
611619
});
612620
TestUtils.Simulate.keyDown(data.nodeInput, getKey("Enter"));
613621
expect(data.callback.calledOnce).to.be.false;
622+
expect(data.onInputErrorCallback.calledOnce).to.be.true;
614623
});
615624
it("should not select excludeDates", () => {
616625
var data = getOnInputKeyDownStuff({
@@ -631,6 +640,28 @@ describe("DatePicker", () => {
631640
expect(data.callback.calledOnce).to.be.false;
632641
});
633642
});
643+
describe("onInputKeyDown Escape", () => {
644+
it("should not update the selected date if the date input manually it has something wrong", () => {
645+
var data = getOnInputKeyDownStuff();
646+
TestUtils.Simulate.keyDown(data.nodeInput, {
647+
key: "ArrowDown",
648+
keyCode: 40,
649+
which: 40
650+
});
651+
TestUtils.Simulate.keyDown(data.nodeInput, {
652+
key: "Backspace",
653+
keyCode: 8,
654+
which: 8
655+
});
656+
TestUtils.Simulate.keyDown(data.nodeInput, {
657+
key: "Escape",
658+
keyCode: 27,
659+
which: 27
660+
});
661+
expect(data.callback.calledOnce).to.be.false;
662+
expect(data.onInputErrorCallback.calledOnce).to.be.true;
663+
});
664+
});
634665
it("should reset the keyboard selection when closed", () => {
635666
var data = getOnInputKeyDownStuff();
636667
TestUtils.Simulate.keyDown(data.nodeInput, getKey("ArrowLeft"));

0 commit comments

Comments
 (0)