Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add error emitter for user input errors #1354

Merged
merged 5 commits into from
Oct 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 63 additions & 51 deletions src/index.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Calendar from "./calendar";
import React from "react";
import PropTypes from "prop-types";
import PopperComponent, { popperPlacementPositions } from "./popper_component";
import classnames from "classnames";
import Calendar from './calendar';
import React from 'react';
import PropTypes from 'prop-types';
import PopperComponent, { popperPlacementPositions } from './popper_component';
import classnames from 'classnames';
import {
newDate,
now,
Expand Down Expand Up @@ -34,13 +34,13 @@ import {
safeDateFormat,
getHightLightDaysMap,
getYear,
getMonth
} from "./date_utils";
import onClickOutside from "react-onclickoutside";
getMonth,
} from './date_utils';
import onClickOutside from 'react-onclickoutside';

export { default as CalendarContainer } from "./calendar_container";
export { default as CalendarContainer } from './calendar_container';

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

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

export default class DatePicker extends React.Component {
static propTypes = {
Expand All @@ -84,7 +85,7 @@ export default class DatePicker extends React.Component {
dayClassName: PropTypes.func,
disabled: PropTypes.bool,
disabledKeyboardNavigation: PropTypes.bool,
dropdownMode: PropTypes.oneOf(["scroll", "select"]).isRequired,
dropdownMode: PropTypes.oneOf(['scroll', 'select']).isRequired,
endDate: PropTypes.object,
excludeDates: PropTypes.array,
filterDate: PropTypes.func,
Expand Down Expand Up @@ -113,6 +114,7 @@ export default class DatePicker extends React.Component {
onKeyDown: PropTypes.func,
onMonthChange: PropTypes.func,
onYearChange: PropTypes.func,
onInputError: PropTypes.func,
open: PropTypes.bool,
openToDate: PropTypes.object,
peekNextMonth: PropTypes.bool,
Expand Down Expand Up @@ -166,12 +168,12 @@ export default class DatePicker extends React.Component {
static get defaultProps() {
return {
allowSameDay: false,
dateFormat: "L",
dateFormatCalendar: "MMMM YYYY",
dateFormat: 'L',
dateFormatCalendar: 'MMMM YYYY',
onChange() {},
disabled: false,
disabledKeyboardNavigation: false,
dropdownMode: "scroll",
dropdownMode: 'scroll',
onFocus() {},
onBlur() {},
onKeyDown() {},
Expand All @@ -181,15 +183,16 @@ export default class DatePicker extends React.Component {
onMonthChange() {},
preventOpenOnFocus: false,
onYearChange() {},
onInputError() {},
monthsShown: 1,
readOnly: false,
withPortal: false,
shouldCloseOnSelect: true,
showTimeSelect: false,
timeIntervals: 30,
timeCaption: "Time",
previousMonthButtonLabel: "Previous Month",
nextMonthButtonLabel: "Next month"
timeCaption: 'Time',
previousMonthButtonLabel: 'Previous Month',
nextMonthButtonLabel: 'Next month',
};
}

Expand All @@ -207,7 +210,7 @@ export default class DatePicker extends React.Component {
}
if (prevProps.highlightDates !== this.props.highlightDates) {
this.setState({
highlightDates: getHightLightDaysMap(this.props.highlightDates)
highlightDates: getHightLightDaysMap(this.props.highlightDates),
});
}
if (
Expand Down Expand Up @@ -250,7 +253,7 @@ export default class DatePicker extends React.Component {
// transforming highlighted days (perhaps nested array)
// to flat Map for faster access in day.jsx
highlightDates: getHightLightDaysMap(this.props.highlightDates),
focused: false
focused: false,
};
};

Expand All @@ -273,9 +276,11 @@ export default class DatePicker extends React.Component {
open && this.state.open
? this.state.preSelection
: this.calcInitialState().preSelection,
lastPreSelectChange: PRESELECT_CHANGE_VIA_NAVIGATE
lastPreSelectChange: PRESELECT_CHANGE_VIA_NAVIGATE,
});
};
inputOk = () =>
isMoment(this.state.preSelection) || isDate(this.state.preSelection);

isCalendarOpen = () =>
this.props.open === undefined
Expand Down Expand Up @@ -330,15 +335,15 @@ export default class DatePicker extends React.Component {
if (this.props.onChangeRaw) {
this.props.onChangeRaw.apply(this, allArgs);
if (
typeof event.isDefaultPrevented !== "function" ||
typeof event.isDefaultPrevented !== 'function' ||
event.isDefaultPrevented()
) {
return;
}
}
this.setState({
inputValue: event.target.value,
lastPreSelectChange: PRESELECT_CHANGE_VIA_INPUT
lastPreSelectChange: PRESELECT_CHANGE_VIA_INPUT,
});
const date = parseDate(event.target.value, this.props);
if (date || !event.target.value) {
Expand All @@ -352,7 +357,7 @@ export default class DatePicker extends React.Component {
this.setState({ preventFocus: true }, () => {
this.preventFocusTimeout = setTimeout(
() => this.setState({ preventFocus: false }),
50
50,
);
return this.preventFocusTimeout;
});
Expand Down Expand Up @@ -391,12 +396,12 @@ export default class DatePicker extends React.Component {
changedDate = setTime(newDate(changedDate), {
hour: getHour(selected),
minute: getMinute(selected),
second: getSecond(selected)
second: getSecond(selected),
});
}
if (!this.props.inline) {
this.setState({
preSelection: changedDate
preSelection: changedDate,
});
}
}
Expand All @@ -412,15 +417,15 @@ export default class DatePicker extends React.Component {

setPreSelection = date => {
const isDateRangePresent =
typeof this.props.minDate !== "undefined" &&
typeof this.props.maxDate !== "undefined";
typeof this.props.minDate !== 'undefined' &&
typeof this.props.maxDate !== 'undefined';
const isValidDateSelection =
isDateRangePresent && date
? isDayInRange(date, this.props.minDate, this.props.maxDate)
: true;
if (isValidDateSelection) {
this.setState({
preSelection: date
preSelection: date,
});
}
};
Expand All @@ -431,11 +436,11 @@ export default class DatePicker extends React.Component {
: this.getPreSelection();
let changedDate = setTime(cloneDate(selected), {
hour: getHour(time),
minute: getMinute(time)
minute: getMinute(time),
});

this.setState({
preSelection: changedDate
preSelection: changedDate,
});

this.props.onChange(changedDate);
Expand All @@ -459,17 +464,16 @@ export default class DatePicker extends React.Component {
!this.props.inline &&
!this.props.preventOpenOnFocus
) {
if (eventKey === "ArrowDown" || eventKey === "ArrowUp") {
if (eventKey === 'ArrowDown' || eventKey === 'ArrowUp') {
this.onInputClick();
}
return;
}
const copy = newDate(this.state.preSelection);
if (eventKey === "Enter") {
if (eventKey === 'Enter') {
event.preventDefault();
if (
(isMoment(this.state.preSelection) ||
isDate(this.state.preSelection)) &&
this.inputOk() &&
this.state.lastPreSelectChange === PRESELECT_CHANGE_VIA_NAVIGATE
) {
this.handleSelect(copy, event);
Expand All @@ -481,45 +485,53 @@ export default class DatePicker extends React.Component {

this.setOpen(false);
}
} else if (eventKey === "Escape") {
} else if (eventKey === 'Escape') {
event.preventDefault();

this.input.blur();
this.props.onBlur(copy);
this.cancelFocusInput();

this.setOpen(false);
} else if (eventKey === "Tab") {
if (!this.inputOk()) {
this.props.onInputError({ code: 1, msg: INPUT_ERR_1 });
}
} else if (eventKey === 'Tab') {
this.setOpen(false);
} else if (!this.props.disabledKeyboardNavigation) {
let newSelection;
switch (eventKey) {
case "ArrowLeft":
case 'ArrowLeft':
newSelection = subtractDays(copy, 1);
break;
case "ArrowRight":
case 'ArrowRight':
newSelection = addDays(copy, 1);
break;
case "ArrowUp":
case 'ArrowUp':
newSelection = subtractWeeks(copy, 1);
break;
case "ArrowDown":
case 'ArrowDown':
newSelection = addWeeks(copy, 1);
break;
case "PageUp":
case 'PageUp':
newSelection = subtractMonths(copy, 1);
break;
case "PageDown":
case 'PageDown':
newSelection = addMonths(copy, 1);
break;
case "Home":
case 'Home':
newSelection = subtractYears(copy, 1);
break;
case "End":
case 'End':
newSelection = addYears(copy, 1);
break;
}
if (!newSelection) return; // Let the input component handle this keydown
if (!newSelection) {
if (this.props.onInputError) {
this.props.onInputError({ code: 1, msg: INPUT_ERR_1 });
}
return; // Let the input component handle this keydown
}
event.preventDefault();
this.setState({ lastPreSelectChange: PRESELECT_CHANGE_VIA_NAVIGATE });
if (this.props.adjustDateOnChange) {
Expand Down Expand Up @@ -628,11 +640,11 @@ export default class DatePicker extends React.Component {
});

const customInput = this.props.customInput || <input type="text" />;
const customInputRef = this.props.customInputRef || "ref";
const customInputRef = this.props.customInputRef || 'ref';
const inputValue =
typeof this.props.value === "string"
typeof this.props.value === 'string'
? this.props.value
: typeof this.state.inputValue === "string"
: typeof this.state.inputValue === 'string'
? this.state.inputValue
: safeDateFormat(this.props.selected, this.props);

Expand All @@ -656,7 +668,7 @@ export default class DatePicker extends React.Component {
title: this.props.title,
readOnly: this.props.readOnly,
required: this.props.required,
tabIndex: this.props.tabIndex
tabIndex: this.props.tabIndex,
});
};

Expand Down Expand Up @@ -718,5 +730,5 @@ export default class DatePicker extends React.Component {
}
}

const PRESELECT_CHANGE_VIA_INPUT = "input";
const PRESELECT_CHANGE_VIA_NAVIGATE = "navigate";
const PRESELECT_CHANGE_VIA_INPUT = 'input';
const PRESELECT_CHANGE_VIA_NAVIGATE = 'navigate';
33 changes: 32 additions & 1 deletion test/datepicker_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -481,8 +481,15 @@ describe("DatePicker", () => {
var testFormat = "YYYY-MM-DD";
var exactishFormat = "YYYY-MM-DD HH: ZZ";
var callback = sandbox.spy();
var onInputErrorCallback = sandbox.spy();

var datePicker = TestUtils.renderIntoDocument(
<DatePicker selected={m} onChange={callback} {...opts} />
<DatePicker
selected={m}
onChange={callback}
onInputError={onInputErrorCallback}
{...opts}
/>
);
var dateInput = datePicker.input;
var nodeInput = ReactDOM.findDOMNode(dateInput);
Expand All @@ -493,6 +500,7 @@ describe("DatePicker", () => {
testFormat,
exactishFormat,
callback,
onInputErrorCallback,
datePicker,
dateInput,
nodeInput
Expand Down Expand Up @@ -611,6 +619,7 @@ describe("DatePicker", () => {
});
TestUtils.Simulate.keyDown(data.nodeInput, getKey("Enter"));
expect(data.callback.calledOnce).to.be.false;
expect(data.onInputErrorCallback.calledOnce).to.be.true;
});
it("should not select excludeDates", () => {
var data = getOnInputKeyDownStuff({
Expand All @@ -631,6 +640,28 @@ describe("DatePicker", () => {
expect(data.callback.calledOnce).to.be.false;
});
});
describe("onInputKeyDown Escape", () => {
it("should not update the selected date if the date input manually it has something wrong", () => {
var data = getOnInputKeyDownStuff();
TestUtils.Simulate.keyDown(data.nodeInput, {
key: "ArrowDown",
keyCode: 40,
which: 40
});
TestUtils.Simulate.keyDown(data.nodeInput, {
key: "Backspace",
keyCode: 8,
which: 8
});
TestUtils.Simulate.keyDown(data.nodeInput, {
key: "Escape",
keyCode: 27,
which: 27
});
expect(data.callback.calledOnce).to.be.false;
expect(data.onInputErrorCallback.calledOnce).to.be.true;
});
});
it("should reset the keyboard selection when closed", () => {
var data = getOnInputKeyDownStuff();
TestUtils.Simulate.keyDown(data.nodeInput, getKey("ArrowLeft"));
Expand Down