diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 427e5aa90759a..7d0368940c27b 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -13,6 +13,7 @@ - `Dropdown` now has a `focusOnMount` prop which is passed directly to the contained `Popover`. - `DatePicker` has new prop `isInvalidDate` exposing react-dates' `isOutsideRange`. +- `DatePicker` allows `null` as accepted value for `currentDate` prop to signify no date selection. ## 7.0.5 (2019-01-03) diff --git a/packages/components/src/date-time/README.md b/packages/components/src/date-time/README.md index 1366be8312635..f0fa63c69b6ce 100644 --- a/packages/components/src/date-time/README.md +++ b/packages/components/src/date-time/README.md @@ -41,10 +41,11 @@ The component accepts the following props: ### currentDate -The current date and time at initialization. +The current date and time at initialization. Optionally pass in a `null` value to specify no date is currently selected. - Type: `string` -- Required: Yes +- Required: No +- Default: today's date ### onChange diff --git a/packages/components/src/date-time/date.js b/packages/components/src/date-time/date.js index 92be3a3970a90..a8e4130e739c8 100644 --- a/packages/components/src/date-time/date.js +++ b/packages/components/src/date-time/date.js @@ -25,6 +25,7 @@ class DatePicker extends Component { onChangeMoment( newDate ) { const { currentDate, onChange } = this.props; + // If currentDate is null, use now as momentTime to designate hours, minutes, seconds. const momentDate = currentDate ? moment( currentDate ) : moment(); const momentTime = { hours: momentDate.hours(), @@ -35,10 +36,24 @@ class DatePicker extends Component { onChange( newDate.set( momentTime ).format( TIMEZONELESS_FORMAT ) ); } + /** + * Create a Moment object from a date string. With no currentDate supplied, default to a Moment + * object representing now. If a null value is passed, return a null value. + * + * @param {?string} currentDate Date representing the currently selected date or null to signify no selection. + * @return {?Moment} Moment object for selected date or null. + */ + getMomentDate( currentDate ) { + if ( null === currentDate ) { + return null; + } + return currentDate ? moment( currentDate ) : moment(); + } + render() { const { currentDate, isInvalidDate } = this.props; - const momentDate = currentDate ? moment( currentDate ) : moment(); + const momentDate = this.getMomentDate( currentDate ); return (
@@ -49,7 +64,7 @@ class DatePicker extends Component { hideKeyboardShortcutsPanel // This is a hack to force the calendar to update on month or year change // https://github.com/airbnb/react-dates/issues/240#issuecomment-361776665 - key={ `datepicker-controller-${ momentDate.format( 'MM-YYYY' ) }` } + key={ `datepicker-controller-${ momentDate ? momentDate.format( 'MM-YYYY' ) : 'null' }` } noBorder numberOfMonths={ 1 } onDateChange={ this.onChangeMoment } diff --git a/packages/components/src/date-time/test/date.js b/packages/components/src/date-time/test/date.js new file mode 100644 index 0000000000000..a12999dbbc784 --- /dev/null +++ b/packages/components/src/date-time/test/date.js @@ -0,0 +1,110 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; +import moment from 'moment'; + +/** + * Internal dependencies + */ +import DatePicker from '../date'; + +const TIMEZONELESS_FORMAT = 'YYYY-MM-DDTHH:mm:ss'; + +describe( 'DatePicker', () => { + it( 'should pass down a moment object for currentDate', () => { + const currentDate = '1986-10-18T23:00:00'; + const wrapper = shallow( ); + const date = wrapper.children().props().date; + expect( moment.isMoment( date ) ).toBe( true ); + expect( date.isSame( moment( currentDate ) ) ).toBe( true ); + } ); + + it( 'should pass down a null date when currentDate is set to null', () => { + const wrapper = shallow( ); + expect( wrapper.children().props().date ).toBeNull(); + } ); + + it( 'should pass down a moment object for now when currentDate is undefined', () => { + const wrapper = shallow( ); + const date = wrapper.children().props().date; + expect( moment.isMoment( date ) ).toBe( true ); + expect( date.isSame( moment(), 'second' ) ).toBe( true ); + } ); + + describe( 'getMomentDate', () => { + it( 'should return a Moment object representing a given date string', () => { + const currentDate = '1986-10-18T23:00:00'; + const wrapper = shallow( ); + const momentDate = wrapper.instance().getMomentDate( currentDate ); + + expect( moment.isMoment( momentDate ) ).toBe( true ); + expect( momentDate.isSame( moment( currentDate ) ) ).toBe( true ); + } ); + + it( 'should return null when given a null agrument', () => { + const currentDate = null; + const wrapper = shallow( ); + const momentDate = wrapper.instance().getMomentDate( currentDate ); + + expect( momentDate ).toBeNull(); + } ); + + it( 'should return a Moment object representing now when given an undefined argument', () => { + const wrapper = shallow( ); + const momentDate = wrapper.instance().getMomentDate(); + + expect( moment.isMoment( momentDate ) ).toBe( true ); + expect( momentDate.isSame( moment(), 'second' ) ).toBe( true ); + } ); + } ); + + describe( 'onChangeMoment', () => { + it( 'should call onChange with a formated date of the input', () => { + const onChangeSpy = jest.fn(); + const currentDate = '1986-10-18T11:00:00'; + const wrapper = shallow( ); + const newDate = moment(); + + wrapper.instance().onChangeMoment( newDate ); + + expect( onChangeSpy ).toHaveBeenCalledWith( newDate.format( TIMEZONELESS_FORMAT ) ); + } ); + + it( 'should call onChange with hours, minutes, seconds of the current time when currentDate is undefined', () => { + let onChangeSpyArgument; + const onChangeSpy = ( arg ) => onChangeSpyArgument = arg; + const wrapper = shallow( ); + const newDate = moment( '1986-10-18T11:00:00' ); + const current = moment(); + const newDateWithCurrentTime = newDate + .clone() + .set( { + hours: current.hours(), + minutes: current.minutes(), + seconds: current.seconds(), + } ); + wrapper.instance().onChangeMoment( newDate ); + + expect( moment( onChangeSpyArgument ).isSame( newDateWithCurrentTime, 'minute' ) ).toBe( true ); + } ); + + it( 'should call onChange with hours, minutes, seconds of the current time when currentDate is null', () => { + let onChangeSpyArgument; + const onChangeSpy = ( arg ) => onChangeSpyArgument = arg; + const wrapper = shallow( ); + const newDate = moment( '1986-10-18T11:00:00' ); + const current = moment(); + const newDateWithCurrentTime = newDate + .clone() + .set( { + hours: current.hours(), + minutes: current.minutes(), + seconds: current.seconds(), + } ); + wrapper.instance().onChangeMoment( newDate ); + + expect( moment( onChangeSpyArgument ).isSame( newDateWithCurrentTime, 'minute' ) ).toBe( true ); + } ); + } ); +} );