From 52dc7e63b7af7c7e4a3202c5a81f7e27434683de Mon Sep 17 00:00:00 2001 From: GtAntoine Date: Wed, 15 Mar 2017 11:43:30 +0100 Subject: [PATCH 1/4] add checkOnblur property --- src/components/input/date/index.js | 31 +++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/components/input/date/index.js b/src/components/input/date/index.js index 219ed3ea0..dcb53f0b2 100644 --- a/src/components/input/date/index.js +++ b/src/components/input/date/index.js @@ -13,12 +13,15 @@ import closest from 'closest'; const isISOString = value => moment.utc(value, moment.ISO_8601, true).isValid(); const propTypes = { + beforeValueGetter: PropTypes.func.isRequired, + checkOnBlur: PropTypes.bool, drops: PropTypes.oneOf(['up', 'down']).isRequired, error: PropTypes.string, locale: PropTypes.string.isRequired, + minDate: PropTypes.string, + maxDate: PropTypes.string, name: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, - beforeValueGetter: PropTypes.func.isRequired, placeholder: PropTypes.string, showDropdowns: PropTypes.bool.isRequired, validate: PropTypes.func, @@ -27,16 +30,15 @@ const propTypes = { if (prop && !isISOString(prop)) { throw new Error(`The date ${prop} provided to the component ${componentName} is not an ISO date. Please provide a valid date string.`); } - }, - minDate: PropTypes.string, - maxDate: PropTypes.string + } }; const defaultProps = { + beforeValueGetter: value => value, + checkOnBlur: false, drops: 'down', - locale: 'en', format: 'MM/DD/YYYY', - beforeValueGetter: value => value, + locale: 'en', /** * Default onChange prop, that will log an error. */ @@ -66,9 +68,8 @@ class InputDate extends Component { document.addEventListener('click', this._onDocumentClick); } - componentDidMount() { - const {drops, showDropdowns} = this.props; + const {checkOnBlur, drops, showDropdowns} = this.props; const {inputDate: startDate} = this.state; } @@ -105,12 +106,16 @@ class InputDate extends Component { _onInputChange = (inputDate, fromBlur) => { const isCorrect = this._isInputFormatCorrect(inputDate); const dropDownDate = isCorrect ? this._parseInputDate(inputDate) : null; - if (isCorrect) { - this.setState({ dropDownDate, inputDate }); - } else { - this.setState({ inputDate }); + let {checkOnBlur} = this.props; + // si le checkOnBlur est désactivé (ce qui est la cas par défaut) ou sinon quand on sort du champ + if ((checkOnBlur && fromBlur) || checkOnBlur !== true){ + if (isCorrect) { + this.setState({ dropDownDate, inputDate }); + } else { + this.setState({ inputDate }); + } } - if (fromBlur !== true && isCorrect) { + if (fromBlur !== true && isCorrect && checkOnBlur !== true) { this.props.onChange(dropDownDate.toISOString()); } }; From adeac3946d8ebad7e5546bffee687f58b5aa27de Mon Sep 17 00:00:00 2001 From: GtAntoine Date: Wed, 15 Mar 2017 11:43:32 +0100 Subject: [PATCH 2/4] add checkOnblur property --- src/components/input/date/index.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/input/date/index.js b/src/components/input/date/index.js index dcb53f0b2..14c61ffc1 100644 --- a/src/components/input/date/index.js +++ b/src/components/input/date/index.js @@ -14,7 +14,7 @@ const isISOString = value => moment.utc(value, moment.ISO_8601, true).isValid(); const propTypes = { beforeValueGetter: PropTypes.func.isRequired, - checkOnBlur: PropTypes.bool, + checkOnlyOnBlur: PropTypes.bool, drops: PropTypes.oneOf(['up', 'down']).isRequired, error: PropTypes.string, locale: PropTypes.string.isRequired, @@ -35,7 +35,7 @@ const propTypes = { const defaultProps = { beforeValueGetter: value => value, - checkOnBlur: false, + checkOnlyOnBlur: false, drops: 'down', format: 'MM/DD/YYYY', locale: 'en', @@ -69,7 +69,7 @@ class InputDate extends Component { } componentDidMount() { - const {checkOnBlur, drops, showDropdowns} = this.props; + const {checkOnlyOnBlur, drops, showDropdowns} = this.props; const {inputDate: startDate} = this.state; } @@ -106,16 +106,16 @@ class InputDate extends Component { _onInputChange = (inputDate, fromBlur) => { const isCorrect = this._isInputFormatCorrect(inputDate); const dropDownDate = isCorrect ? this._parseInputDate(inputDate) : null; - let {checkOnBlur} = this.props; - // si le checkOnBlur est désactivé (ce qui est la cas par défaut) ou sinon quand on sort du champ - if ((checkOnBlur && fromBlur) || checkOnBlur !== true){ + let {checkOnlyOnBlur} = this.props; + // si le checkOnlyOnBlur est désactivé (ce qui est la cas par défaut) ou sinon quand on sort du champ + if ((checkOnlyOnBlur && fromBlur) || checkOnlyOnBlur !== true){ if (isCorrect) { this.setState({ dropDownDate, inputDate }); } else { this.setState({ inputDate }); } } - if (fromBlur !== true && isCorrect && checkOnBlur !== true) { + if (fromBlur !== true && isCorrect && checkOnlyOnBlur !== true) { this.props.onChange(dropDownDate.toISOString()); } }; From 85333fb1929781e1c0d7cffe0eb05ad2d7c199e4 Mon Sep 17 00:00:00 2001 From: fconstantin Date: Fri, 17 Mar 2017 11:12:12 +0100 Subject: [PATCH 3/4] Correct, test checkOnlyOnBlur property. Comment component. Suppress tests warnings --- src/components/input/date/__tests__/index.js | 124 +++++++++++++++---- src/components/input/date/index.js | 99 ++++++++++++--- 2 files changed, 182 insertions(+), 41 deletions(-) diff --git a/src/components/input/date/__tests__/index.js b/src/components/input/date/__tests__/index.js index b95b60604..f2aa553a0 100644 --- a/src/components/input/date/__tests__/index.js +++ b/src/components/input/date/__tests__/index.js @@ -13,7 +13,7 @@ describe('The input date', () => { let reactComponent, domNode, inputNode; const onChangeSpy = sinon.spy(); before(() => { - reactComponent = TestUtils.renderIntoDocument(); + reactComponent = TestUtils.renderIntoDocument(); domNode = ReactDOM.findDOMNode(reactComponent); }); it('should render a node with data-focus attribute', () => { @@ -31,13 +31,12 @@ describe('The input date', () => { }); }); - describe('when mounted with a disabled props', () => { const now = new Date().toISOString(); let reactComponent, inputNode; const onChangeSpy = sinon.spy(); before(() => { - reactComponent = TestUtils.renderIntoDocument(); + reactComponent = TestUtils.renderIntoDocument(); inputNode = ReactDOM.findDOMNode(reactComponent.refs.input.refs.htmlInput); }); it('should render a node with disabled attribute', () => { @@ -49,7 +48,7 @@ describe('The input date', () => { let renderedTest; const onChangeSpy = sinon.spy(); before(() => { - renderedTest = TestUtils.renderIntoDocument(); + renderedTest = TestUtils.renderIntoDocument(); }); it('should give a null value', () => { @@ -71,7 +70,7 @@ describe('The input date', () => { const onChangeSpy = sinon.spy(); const invalidDateString = 'invalid date'; before(() => { - renderedTest = TestUtils.renderIntoDocument(); + renderedTest = TestUtils.renderIntoDocument(); }); it('should display the invalid value in the input', () => { @@ -89,7 +88,7 @@ describe('The input date', () => { describe('when the value given has a prop changes', () => { const now = new Date().toISOString(); - const past = new Date('01/10/1995').toISOString() /*'1995-01-09T23:00:00.000Z'*/; + const past = new Date('01/10/1995').toISOString(); let renderedTest; const onChangeSpy = sinon.spy(); class TestComponent extends Component { @@ -101,7 +100,7 @@ describe('The input date', () => { } render() { - return ; + return ; } } @@ -120,7 +119,7 @@ describe('The input date', () => { let renderedTest; const onChangeSpy = sinon.spy(); before(() => { - renderedTest = TestUtils.renderIntoDocument(); + renderedTest = TestUtils.renderIntoDocument(); const input = ReactDOM.findDOMNode(renderedTest.refs.input.refs.htmlInput); TestUtils.Simulate.change(input, {target: {value: ''}}); }); @@ -145,22 +144,79 @@ describe('The input date', () => { }; render() { - return ; + return ; } } before(() => { renderedTest = TestUtils.renderIntoDocument(); const input = ReactDOM.findDOMNode(renderedTest.refs.date.refs.input.refs.htmlInput); TestUtils.Simulate.change(input, {target: {value: validDateString}}); - //TestUtils.Simulate.click(document); }); it('should give the provided value', () => { expect(moment(renderedTest.refs.date.getValue()).isSame(moment.utc(validDateString, 'MM/DD/YYYY').toISOString())).to.be.true; }); }); - describe('when the user enters an invalid input', () => { - const invalidDateString = 'Lol invalid'; + describe('when the user enters a valid input with multiples formats', () => { + const validDateString = '02/03/2010'; + let renderedTest; + class TestComponent extends Component { + constructor() { + super(); + this.state = { + value: null + }; + } + + onDateChange = (value) => { + this.setState({value}); + }; + + render() { + return ; + } + } + before(() => { + renderedTest = TestUtils.renderIntoDocument(); + const input = ReactDOM.findDOMNode(renderedTest.refs.date.refs.input.refs.htmlInput); + TestUtils.Simulate.change(input, {target: {value: validDateString}}); + }); + it('should give the provided value', () => { + expect(moment(renderedTest.refs.date.getValue()).isSame(moment.utc(validDateString, 'DD/MM/YYYY').toISOString())).to.be.true; + }); + }); + + describe('when the user enters a partially valid input with multiples formats', () => { + const validDateString = '020320'; + let renderedTest; + class TestComponent extends Component { + constructor() { + super(); + this.state = { + value: null + }; + } + + onDateChange = (value) => { + this.setState({value}); + }; + + render() { + return ; + } + } + before(() => { + renderedTest = TestUtils.renderIntoDocument(); + const input = ReactDOM.findDOMNode(renderedTest.refs.date.refs.input.refs.htmlInput); + TestUtils.Simulate.change(input, {target: {value: validDateString}}); + }); + it('should give the provided value', () => { + expect(moment(renderedTest.refs.date.getValue()).isSame(moment.utc(validDateString, 'DDMMYY').toISOString())).to.be.true; + }); + }); + + describe('when the user enters a partially valid input with multiples formats and checkOnlyOnBlur', () => { + const invalidDateString = '020320'; let renderedTest; class TestComponent extends Component { constructor() { @@ -175,7 +231,7 @@ describe('The input date', () => { }; render() { - return ; + return ; } } before(() => { @@ -183,34 +239,48 @@ describe('The input date', () => { const input = ReactDOM.findDOMNode(renderedTest.refs.date.refs.input.refs.htmlInput); TestUtils.Simulate.change(input, {target: {value: invalidDateString}}); }); - it('should give a null value', () => { - expect(renderedTest.refs.date.getValue()).to.be.null; + it('should give the provided value', () => { + expect(moment(renderedTest.refs.date.getValue()).isSame(moment.utc(invalidDateString, 'DDMMYY').toISOString())).to.be.true; }); it('but still let the invalid value in the input', () => { expect(ReactDOM.findDOMNode(renderedTest.refs.date.refs.input.refs.htmlInput).value).to.equal(invalidDateString); }); }); - // The onBlur function is no more used on the input-date component - describe.skip('when blurred with a valid date', () => { - const validDate = moment.utc('10/10/2015').toISOString(); - const onChangeSpy = sinon.spy(); + + describe('when the user enters an invalid input', () => { + const invalidDateString = 'Lol invalid'; + let renderedTest; class TestComponent extends Component { + constructor() { + super(); + this.state = { + value: null + }; + } + + onDateChange = (value) => { + this.setState({value}); + }; + render() { - return console.log('BLUR',value)} ref='date' value={validDate} />; + return ; } } - let renderedTest; before(() => { renderedTest = TestUtils.renderIntoDocument(); const input = ReactDOM.findDOMNode(renderedTest.refs.date.refs.input.refs.htmlInput); - TestUtils.Simulate.blur(input); + TestUtils.Simulate.change(input, {target: {value: invalidDateString}}); }); - it('should call the onChange prop with the corresponding ISOString', () => { - expect(onChangeSpy).to.have.been.calledWith(validDate); + it('should give a null value', () => { + expect(renderedTest.refs.date.getValue()).to.be.null; + }); + it('but still let the invalid value in the input', () => { + expect(ReactDOM.findDOMNode(renderedTest.refs.date.refs.input.refs.htmlInput).value).to.equal(invalidDateString); }); }); + describe('when a date is chosen in the date picker', () => { - const validDate = moment.utc('10/10/2015').toISOString(); + const validDate = moment.utc('1995-01-01T00:00:00.000Z').toISOString(); const onChangeSpy = sinon.spy(); let renderedTest; before(done => { @@ -222,7 +292,7 @@ describe('The input date', () => { }; class TestComponent extends Component { render() { - return ; + return ; } } renderedTest = TestUtils.renderIntoDocument(); @@ -232,7 +302,7 @@ describe('The input date', () => { TestUtils.Simulate.click(firstDay); }); it('should call the onChange prop with the corresponding ISOString', () => { - expect(onChangeSpy).to.have.been.calledWith((moment.utc('09/27/2015')).toISOString()); + expect(onChangeSpy).to.have.been.calledWith(validDate); }); }); }); diff --git a/src/components/input/date/index.js b/src/components/input/date/index.js index 14c61ffc1..2b0d42d0f 100644 --- a/src/components/input/date/index.js +++ b/src/components/input/date/index.js @@ -2,14 +2,13 @@ import React, { Component, PropTypes } from 'react'; import ReactDOM from 'react-dom'; import moment from 'moment'; -import Base from '../../../behaviours/component-base'; -import InputText from '../text'; import DatePicker from 'react-date-picker'; -import compose from 'lodash/function/compose'; -import isArray from 'lodash/lang/isArray'; -import uniqueId from 'lodash/utility/uniqueId'; +import { isArray, uniqueId } from 'lodash'; import closest from 'closest'; +import Base from '../../../behaviours/component-base'; +import InputText from '../text'; + const isISOString = value => moment.utc(value, moment.ISO_8601, true).isValid(); const propTypes = { @@ -49,8 +48,16 @@ const defaultProps = { validate: isISOString }; +/** + * Date input component with text input and date picker. + * Validate user input at each change in the text input. + */ @Base class InputDate extends Component { + /** + * Create a new component. + * @param {*} props Props. + */ constructor(props) { super(props); const {value} = props; @@ -63,16 +70,25 @@ class InputDate extends Component { this._inputDateId = uniqueId('input-date-'); } + /** + * Before component mount. + */ componentWillMount() { - // moment.locale(this.props.locale); document.addEventListener('click', this._onDocumentClick); } + /** + * After component mount. + */ componentDidMount() { const {checkOnlyOnBlur, drops, showDropdowns} = this.props; const {inputDate: startDate} = this.state; } + /** + * Receive component props. + * @param {*} param0 + */ componentWillReceiveProps({value}) { this.setState({ dropDownDate: isISOString(value) ? moment.utc(value, moment.ISO_8601) : moment.utc(), @@ -80,17 +96,33 @@ class InputDate extends Component { }); } + /** + * Before component unmount. + * + */ componentWillUnmount() { document.removeEventListener('click', this._onDocumentClick); } + /** + * Check if input value is a valid date. + */ _isInputFormatCorrect = value => this._parseInputDate(value).isValid(); + /** + * Parse input value and try convertion to date. + * Formats could be define with the format props. + * The default format is 'MM/DD/YYYY'. + */ _parseInputDate = inputDate => { const {format} = this.props; return moment.utc(inputDate, format, true); }; + /** + * Format date to the first format is the format props (if array). + * The default format is 'MM/DD/YYYY'. + */ _formatDate = isoDate => { let {format} = this.props; if (isISOString(isoDate)) { @@ -103,28 +135,49 @@ class InputDate extends Component { } }; + /** + * Handle changes. + */ _onInputChange = (inputDate, fromBlur) => { + let {checkOnlyOnBlur} = this.props; + // When checkOnlyOnBlur is true skip all checks. + if (checkOnlyOnBlur === true && fromBlur !== true) { + // Use case : incompatibles date formats (DD/MM/YY, DD/MM/YYYY) + this.setState({ inputDate }); + return; + } + const isCorrect = this._isInputFormatCorrect(inputDate); const dropDownDate = isCorrect ? this._parseInputDate(inputDate) : null; - let {checkOnlyOnBlur} = this.props; - // si le checkOnlyOnBlur est désactivé (ce qui est la cas par défaut) ou sinon quand on sort du champ - if ((checkOnlyOnBlur && fromBlur) || checkOnlyOnBlur !== true){ + if (isCorrect) { + this.setState({ dropDownDate, inputDate }); + } else { + this.setState({ inputDate }); + } + + // Fire onChange event + if (checkOnlyOnBlur === true) { if (isCorrect) { - this.setState({ dropDownDate, inputDate }); - } else { - this.setState({ inputDate }); + this.props.onChange(dropDownDate.toISOString()); } + return; } - if (fromBlur !== true && isCorrect && checkOnlyOnBlur !== true) { + if (fromBlur !== true && isCorrect) { this.props.onChange(dropDownDate.toISOString()); } }; + /** + * Handle input text blur. + */ _onInputBlur = () => { const {inputDate} = this.state; this._onInputChange(inputDate, true); }; + /** + * Handle calendar changes. + */ _onDropDownChange = (text, date) => { if (date._isValid) { this.setState({ displayPicker: false }, () => { @@ -135,10 +188,16 @@ class InputDate extends Component { } }; + /** + * Handle input text focus. + */ _onInputFocus = () => { this.setState({ displayPicker: true }); }; + /** + * Handle document click to close the calendar. + */ _onDocumentClick = ({target}) => { const targetClassAttr = target.getAttribute('class'); const isTriggeredFromPicker = targetClassAttr ? targetClassAttr.includes('dp-cell') : false; //this is the only way to check the target comes from picker cause at this stage, month and year div are unmounted by React. @@ -150,18 +209,27 @@ class InputDate extends Component { } }; + /** + * Handle Tab and Enter keys to close the calendar. + */ _handleKeyDown = ({key}) => { if (key === 'Tab' || key === 'Enter') { this.setState({ displayPicker: false }, () => this._onInputBlur()); } }; + /** + * Return value in a valid date format. + */ getValue = () => { const {inputDate} = this.state; const rawValue = this._isInputFormatCorrect(inputDate) ? this._parseInputDate(inputDate).toISOString() : null; return this.props.beforeValueGetter(rawValue); }; + /** + * Validate the input. + */ validate = () => { const {inputDate} = this.state; const {isRequired} = this.props; @@ -178,6 +246,9 @@ class InputDate extends Component { } }; + /** + * Render text input and datepicker. + */ render() { const {error, locale, name, placeholder, disabled, minDate, maxDate} = this.props; const {dropDownDate, inputDate, displayPicker} = this.state; @@ -196,7 +267,7 @@ class InputDate extends Component { ref='picker' minDate={minDate} maxDate={maxDate} - /> + /> } From bb2eef3527286f9e5763fd631cd236c1a46131cd Mon Sep 17 00:00:00 2001 From: fconstantin Date: Fri, 17 Mar 2017 13:54:05 +0100 Subject: [PATCH 4/4] Correct comments --- src/components/input/date/__tests__/index.js | 4 ++-- src/components/input/date/index.js | 20 +++++++------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/components/input/date/__tests__/index.js b/src/components/input/date/__tests__/index.js index f2aa553a0..93f1898e0 100644 --- a/src/components/input/date/__tests__/index.js +++ b/src/components/input/date/__tests__/index.js @@ -280,7 +280,7 @@ describe('The input date', () => { }); describe('when a date is chosen in the date picker', () => { - const validDate = moment.utc('1995-01-01T00:00:00.000Z').toISOString(); + const validDate = moment.utc('2015-10-10T00:00:00.000Z').toISOString(); const onChangeSpy = sinon.spy(); let renderedTest; before(done => { @@ -302,7 +302,7 @@ describe('The input date', () => { TestUtils.Simulate.click(firstDay); }); it('should call the onChange prop with the corresponding ISOString', () => { - expect(onChangeSpy).to.have.been.calledWith(validDate); + expect(onChangeSpy).to.have.been.calledWith( moment.utc('2015-09-27T00:00:00.000Z').toISOString()); }); }); }); diff --git a/src/components/input/date/index.js b/src/components/input/date/index.js index 2b0d42d0f..3016af800 100644 --- a/src/components/input/date/index.js +++ b/src/components/input/date/index.js @@ -3,7 +3,8 @@ import React, { Component, PropTypes } from 'react'; import ReactDOM from 'react-dom'; import moment from 'moment'; import DatePicker from 'react-date-picker'; -import { isArray, uniqueId } from 'lodash'; +import isArray from 'lodash/lang/isArray'; +import uniqueId from 'lodash/utility/uniqueId'; import closest from 'closest'; import Base from '../../../behaviours/component-base'; @@ -77,14 +78,6 @@ class InputDate extends Component { document.addEventListener('click', this._onDocumentClick); } - /** - * After component mount. - */ - componentDidMount() { - const {checkOnlyOnBlur, drops, showDropdowns} = this.props; - const {inputDate: startDate} = this.state; - } - /** * Receive component props. * @param {*} param0 @@ -98,7 +91,6 @@ class InputDate extends Component { /** * Before component unmount. - * */ componentWillUnmount() { document.removeEventListener('click', this._onDocumentClick); @@ -110,8 +102,8 @@ class InputDate extends Component { _isInputFormatCorrect = value => this._parseInputDate(value).isValid(); /** - * Parse input value and try convertion to date. - * Formats could be define with the format props. + * Parse input value and try converting it to date. + * Formats could be defined with the format props. * The default format is 'MM/DD/YYYY'. */ _parseInputDate = inputDate => { @@ -120,7 +112,7 @@ class InputDate extends Component { }; /** - * Format date to the first format is the format props (if array). + * Format the date to the first format in the format props (if array). * The default format is 'MM/DD/YYYY'. */ _formatDate = isoDate => { @@ -177,6 +169,7 @@ class InputDate extends Component { /** * Handle calendar changes. + * @memberOf InputDate */ _onDropDownChange = (text, date) => { if (date._isValid) { @@ -197,6 +190,7 @@ class InputDate extends Component { /** * Handle document click to close the calendar. + * @memberOf InputDate */ _onDocumentClick = ({target}) => { const targetClassAttr = target.getAttribute('class');