diff --git a/src/index.jsx b/src/index.jsx index 69e3cf960..a7f1384a8 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -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, @@ -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 @@ -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 = { @@ -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, @@ -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, @@ -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() {}, @@ -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', }; } @@ -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 ( @@ -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, }; }; @@ -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 @@ -330,7 +335,7 @@ 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; @@ -338,7 +343,7 @@ export default class DatePicker extends React.Component { } 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) { @@ -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; }); @@ -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, }); } } @@ -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, }); } }; @@ -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); @@ -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); @@ -481,7 +485,7 @@ export default class DatePicker extends React.Component { this.setOpen(false); } - } else if (eventKey === "Escape") { + } else if (eventKey === 'Escape') { event.preventDefault(); this.input.blur(); @@ -489,37 +493,45 @@ export default class DatePicker extends React.Component { 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) { @@ -628,11 +640,11 @@ export default class DatePicker extends React.Component { }); const customInput = this.props.customInput || ; - 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); @@ -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, }); }; @@ -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'; diff --git a/test/datepicker_test.js b/test/datepicker_test.js index 1dfe8f0e4..b473b49f2 100644 --- a/test/datepicker_test.js +++ b/test/datepicker_test.js @@ -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( - + ); var dateInput = datePicker.input; var nodeInput = ReactDOM.findDOMNode(dateInput); @@ -493,6 +500,7 @@ describe("DatePicker", () => { testFormat, exactishFormat, callback, + onInputErrorCallback, datePicker, dateInput, nodeInput @@ -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({ @@ -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"));