diff --git a/.travis.yml b/.travis.yml index b55536c174..f8c4163fc0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ script: - 'if [ -n "${KARMA-}" ]; then npm run tests-karma ; fi' - 'if [ -n "${COVERAGE-}" ] && [ "${TRAVIS_BRANCH-}" = "master" ]; then npm run cover; cat ./coverage/lcov.info | ./node_modules/.bin/coveralls ; fi' install: - - 'if [ -n "${LINT-}" ]; then npm install --legacy-bundling ; else npm install ; fi' + - 'if [ -n "${LINT-}" ]; then NPM_CONFIG_LEGACY_PEER_DEPS=true npm install --legacy-bundling ; else npm install ; fi' env: global: - TEST=true diff --git a/examples/DateRangePickerWrapper.jsx b/examples/DateRangePickerWrapper.jsx index 620a286cdd..96154f4d06 100644 --- a/examples/DateRangePickerWrapper.jsx +++ b/examples/DateRangePickerWrapper.jsx @@ -14,6 +14,7 @@ import { HORIZONTAL_ORIENTATION, ANCHOR_LEFT, NAV_POSITION_TOP, + OPEN_DOWN, } from '../src/constants'; import isInclusivelyAfterDay from '../src/utils/isInclusivelyAfterDay'; @@ -70,6 +71,7 @@ const defaultProps = { keepOpenOnDateSelect: false, reopenPickerOnClearDates: false, isRTL: false, + openDirection: OPEN_DOWN, // navigation related props navPosition: NAV_POSITION_TOP, diff --git a/examples/DayPickerSingleDateControllerWrapper.jsx b/examples/DayPickerSingleDateControllerWrapper.jsx index 02d94e931b..44a0a5e1fb 100644 --- a/examples/DayPickerSingleDateControllerWrapper.jsx +++ b/examples/DayPickerSingleDateControllerWrapper.jsx @@ -19,6 +19,7 @@ const propTypes = forbidExtraProps({ initialDate: momentPropTypes.momentObj, showInput: PropTypes.bool, + allowUnselect: PropTypes.bool, keepOpenOnDateSelect: PropTypes.bool, isOutsideRange: PropTypes.func, isDayBlocked: PropTypes.func, @@ -56,6 +57,7 @@ const defaultProps = { showInput: false, // day presentation and interaction related props + allowUnselect: false, renderCalendarDay: undefined, renderDayContents: null, isDayBlocked: () => false, diff --git a/examples/SingleDatePickerWrapper.jsx b/examples/SingleDatePickerWrapper.jsx index 92a309f7a8..0dcf491462 100644 --- a/examples/SingleDatePickerWrapper.jsx +++ b/examples/SingleDatePickerWrapper.jsx @@ -8,7 +8,7 @@ import SingleDatePicker from '../src/components/SingleDatePicker'; import { SingleDatePickerPhrases } from '../src/defaultPhrases'; import SingleDatePickerShape from '../src/shapes/SingleDatePickerShape'; -import { HORIZONTAL_ORIENTATION, ANCHOR_LEFT } from '../src/constants'; +import { HORIZONTAL_ORIENTATION, ANCHOR_LEFT, OPEN_DOWN } from '../src/constants'; import isInclusivelyAfterDay from '../src/utils/isInclusivelyAfterDay'; const propTypes = { @@ -56,6 +56,7 @@ const defaultProps = { keepOpenOnDateSelect: false, reopenPickerOnClearDate: false, isRTL: false, + openDirection: OPEN_DOWN, // navigation related props navPrev: null, diff --git a/package.json b/package.json index d5f62e238c..7798b21855 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "lint": "eslint --ext .js,.jsx src test", "mocha": "mocha ./test/_helpers", "storybook:uninstall": "npm uninstall --no-save @storybook/react && rimraf node_modules/@storybook node_modules/react-modal node_modules/react-dom-factories", - "react": "enzyme-adapter-react-install 16", + "react": "NPM_CONFIG_LEGACY_PEER_DEPS=true enzyme-adapter-react-install 16", "pretest": "npm run --silent lint", "pretests-only": "npm run react", "tests-only": "npm run mocha --silent", diff --git a/src/components/DayPicker.jsx b/src/components/DayPicker.jsx index f39aa8ec01..34345f7259 100644 --- a/src/components/DayPicker.jsx +++ b/src/components/DayPicker.jsx @@ -391,10 +391,19 @@ class DayPicker extends React.PureComponent { monthTitleHeight, } = this.state; + let shouldAdjustHeight = false; + if (numberOfMonths !== prevProps.numberOfMonths) { + this.setCalendarMonthWeeks(currentMonth); + shouldAdjustHeight = true; + } if ( this.isHorizontal() && (orientation !== prevProps.orientation || daySize !== prevProps.daySize) ) { + shouldAdjustHeight = true; + } + + if (shouldAdjustHeight) { const visibleCalendarWeeks = this.calendarMonthWeeks.slice(1, numberOfMonths + 1); const calendarMonthWeeksHeight = Math.max(0, ...visibleCalendarWeeks) * (daySize - 1); const newMonthHeight = monthTitleHeight + calendarMonthWeeksHeight + 1; diff --git a/src/components/DayPickerSingleDateController.jsx b/src/components/DayPickerSingleDateController.jsx index 89be9f6ed8..de492c3a24 100644 --- a/src/components/DayPickerSingleDateController.jsx +++ b/src/components/DayPickerSingleDateController.jsx @@ -34,12 +34,18 @@ import { import DayPicker from './DayPicker'; import getPooledMoment from '../utils/getPooledMoment'; +// Default value of the date property. Represents the state +// when there is no date selected. +// TODO: use null +const DATE_UNSET_VALUE = undefined; + const propTypes = forbidExtraProps({ date: momentPropTypes.momentObj, minDate: momentPropTypes.momentObj, maxDate: momentPropTypes.momentObj, onDateChange: PropTypes.func, + allowUnselect: PropTypes.bool, focused: PropTypes.bool, onFocusChange: PropTypes.func, onClose: PropTypes.func, @@ -102,11 +108,12 @@ const propTypes = forbidExtraProps({ }); const defaultProps = { - date: undefined, // TODO: use null + date: DATE_UNSET_VALUE, minDate: null, maxDate: null, onDateChange() {}, + allowUnselect: false, focused: false, onFocusChange() {}, onClose() {}, @@ -352,16 +359,19 @@ export default class DayPickerSingleDateController extends React.PureComponent { if (e) e.preventDefault(); if (this.isBlocked(day)) return; const { + allowUnselect, onDateChange, keepOpenOnDateSelect, onFocusChange, onClose, } = this.props; - onDateChange(day); + const clickedDay = allowUnselect && this.isSelected(day) ? DATE_UNSET_VALUE : day; + + onDateChange(clickedDay); if (!keepOpenOnDateSelect) { onFocusChange({ focused: false }); - onClose({ date: day }); + onClose({ date: clickedDay }); } } diff --git a/stories/DayPickerSingleDateController.js b/stories/DayPickerSingleDateController.js index e7fd81a5f0..ef05c08305 100644 --- a/stories/DayPickerSingleDateController.js +++ b/stories/DayPickerSingleDateController.js @@ -145,6 +145,14 @@ storiesOf('DayPickerSingleDateController', module) onNextMonthClick={action('DayPickerSingleDateController::onNextMonthClick')} /> ))) + .add('with day unselection', withInfo()(() => ( + + ))) .add('with custom input', withInfo()(() => ( { expect(onDateChangeStub.callCount).to.equal(1); }); + it('props.onDateChange receives undefined when day selected', () => { + const date = moment(); + const onDateChangeStub = sinon.stub(); + const wrapper = shallow(( + {}} + date={date} + allowUnselect + /> + )); + // Click same day as the provided date. + wrapper.instance().onDayClick(date); + expect(onDateChangeStub.callCount).to.equal(1); + expect(onDateChangeStub.getCall(0).args[0]).to.equal(undefined); + }); + + it('props.onDateChange receives day when allowUnselect is disabled', () => { + const date = moment(); + const onDateChangeStub = sinon.stub(); + const wrapper = shallow(( + {}} + date={date} + allowUnselect={false} + /> + )); + // Click same day as the provided date. + wrapper.instance().onDayClick(date); + expect(onDateChangeStub.callCount).to.equal(1); + expect(onDateChangeStub.getCall(0).args[0]).to.equal(date); + }); + describe('props.keepOpenOnDateSelect is false', () => { it('props.onFocusChange is called', () => { const onFocusChangeStub = sinon.stub(); diff --git a/test/components/DayPicker_spec.jsx b/test/components/DayPicker_spec.jsx index 5f98b61e9b..46d0697f90 100644 --- a/test/components/DayPicker_spec.jsx +++ b/test/components/DayPicker_spec.jsx @@ -12,6 +12,7 @@ import CalendarMonthGrid from '../../src/components/CalendarMonthGrid'; import DayPickerNavigation from '../../src/components/DayPickerNavigation'; import DayPickerKeyboardShortcuts from '../../src/components/DayPickerKeyboardShortcuts'; import { + DAY_SIZE, HORIZONTAL_ORIENTATION, VERTICAL_ORIENTATION, VERTICAL_SCROLLABLE, @@ -22,8 +23,9 @@ const today = moment().locale('en'); const event = { preventDefault() {}, stopPropagation() {} }; describe('DayPicker', () => { + let adjustDayPickerHeightSpy; beforeEach(() => { - sinon.stub(PureDayPicker.prototype, 'adjustDayPickerHeight'); + adjustDayPickerHeightSpy = sinon.stub(PureDayPicker.prototype, 'adjustDayPickerHeight'); }); afterEach(() => { @@ -907,13 +909,8 @@ describe('DayPicker', () => { }); }); - describe.skip('life cycle methods', () => { - let adjustDayPickerHeightSpy; - beforeEach(() => { - adjustDayPickerHeightSpy = sinon.stub(PureDayPicker.prototype, 'adjustDayPickerHeight'); - }); - - describe('#componentDidMount', () => { + describe('life cycle methods', () => { + describe.skip('#componentDidMount', () => { describe('props.orientation === HORIZONTAL_ORIENTATION', () => { it('calls adjustDayPickerHeight', () => { mount(); @@ -927,7 +924,7 @@ describe('DayPicker', () => { }); }); - describe('props.orientation === VERTICAL_ORIENTATION', () => { + describe.skip('props.orientation === VERTICAL_ORIENTATION', () => { it('does not call adjustDayPickerHeight', () => { mount(); expect(adjustDayPickerHeightSpy.called).to.equal(false); @@ -949,7 +946,7 @@ describe('DayPicker', () => { }); }); - describe('#componentWillReceiveProps', () => { + describe.skip('#componentWillReceiveProps', () => { describe('props.orientation === VERTICAL_SCROLLABLE', () => { it('updates state.currentMonthScrollTop', () => { sinon.spy(DayPicker.prototype, 'setTransitionContainerRef'); @@ -966,15 +963,16 @@ describe('DayPicker', () => { describe('#componentDidUpdate', () => { let updateStateAfterMonthTransitionSpy; + beforeEach(() => { updateStateAfterMonthTransitionSpy = sinon.stub( - DayPicker.prototype, + PureDayPicker.prototype, 'updateStateAfterMonthTransition', ); }); describe('props.orientation === HORIZONTAL_ORIENTATION', () => { - it('calls adjustDayPickerHeight if state.monthTransition is truthy', () => { + it.skip('calls adjustDayPickerHeight if state.monthTransition is truthy', () => { const wrapper = mount(); wrapper.setState({ monthTransition: 'foo', @@ -982,7 +980,7 @@ describe('DayPicker', () => { expect(adjustDayPickerHeightSpy).to.have.property('callCount', 2); }); - it('does not call adjustDayPickerHeight if state.monthTransition is falsy', () => { + it.skip('does not call adjustDayPickerHeight if state.monthTransition is falsy', () => { const wrapper = mount(); wrapper.setState({ monthTransition: null, @@ -990,7 +988,7 @@ describe('DayPicker', () => { expect(adjustDayPickerHeightSpy.calledTwice).to.equal(false); }); - it('calls adjustDayPickerHeight if orientation has changed from HORIZONTAL_ORIENTATION to VERTICAL_ORIENTATION', () => { + it.skip('calls adjustDayPickerHeight if orientation has changed from HORIZONTAL_ORIENTATION to VERTICAL_ORIENTATION', () => { const wrapper = mount(); wrapper.setState({ orientation: VERTICAL_ORIENTATION, @@ -998,7 +996,7 @@ describe('DayPicker', () => { expect(adjustDayPickerHeightSpy).to.have.property('callCount', 2); }); - it('calls adjustDayPickerHeight if daySize has changed', () => { + it.skip('calls adjustDayPickerHeight if daySize has changed', () => { const wrapper = mount(); wrapper.setState({ daySize: 40, @@ -1007,7 +1005,7 @@ describe('DayPicker', () => { expect(adjustDayPickerHeightSpy).to.have.property('callCount', 2); }); - it('calls updateStateAfterMonthTransition if state.monthTransition is truthy', () => { + it.skip('calls updateStateAfterMonthTransition if state.monthTransition is truthy', () => { const wrapper = mount(); wrapper.setState({ monthTransition: 'foo', @@ -1015,16 +1013,36 @@ describe('DayPicker', () => { expect(updateStateAfterMonthTransitionSpy).to.have.property('callCount', 1); }); - it('does not call updateStateAfterMonthTransition if state.monthTransition is falsy', () => { + it.skip('does not call updateStateAfterMonthTransition if state.monthTransition is falsy', () => { const wrapper = mount(); wrapper.setState({ monthTransition: null, }); expect(updateStateAfterMonthTransitionSpy.calledOnce).to.equal(false); }); + + it('calls adjustDayPickerHeightSpy if props.numberOfMonths changes', () => { + const wrapper = shallow().dive(); + wrapper.instance().componentDidUpdate({ + daySize: DAY_SIZE, + numberOfMonths: 3, + orientation: HORIZONTAL_ORIENTATION, + }); + expect(adjustDayPickerHeightSpy.callCount).to.equal(1); + }); + + it('does not call adjustDayPickerHeightSpy if props.numberOfMonths does not change', () => { + const wrapper = shallow().dive(); + wrapper.instance().componentDidUpdate({ + daySize: DAY_SIZE, + numberOfMonths: 2, + orientation: HORIZONTAL_ORIENTATION, + }); + expect(adjustDayPickerHeightSpy.called).to.equal(false); + }); }); - describe('props.orientation === VERTICAL_ORIENTATION', () => { + describe.skip('props.orientation === VERTICAL_ORIENTATION', () => { it('does not call adjustDayPickerHeight if state.monthTransition is truthy', () => { const wrapper = mount(); wrapper.setState({ @@ -1075,7 +1093,7 @@ describe('DayPicker', () => { }); }); - describe('props.orientation === VERTICAL_SCROLLABLE', () => { + describe.skip('props.orientation === VERTICAL_SCROLLABLE', () => { it('does not update transitionContainer ref`s scrollTop currentMonth stays the same', () => { sinon.spy(DayPicker.prototype, 'setTransitionContainerRef'); const wrapper = mount(); @@ -1097,7 +1115,7 @@ describe('DayPicker', () => { }); }); - describe('when isFocused is updated to true', () => { + describe.skip('when isFocused is updated to true', () => { const prevProps = { isFocused: false }; const newProps = { isFocused: true };