Repository: react-dates/react-dates (language: javascript)

Issue:
adjustDayPickerHeight not calculating predictably when setting numberOfMonths prop dynamically
On our project, we're using a grid system that optionally passes us a `screenClass` prop to tell us what breakpoint we're currently in. We're using that breakpoint to pass a `numberOfMonths` prop to the `DayPickerRangeController` to render either two months for desktop or one month for mobile. 

When we're doing this, our function initializes as `sm`, which initially passes `numberOfMonths={1}`. It's changed to `numberOfMonths={2}` shortly after once `window.innerWidth` is captured and the method is run. 

Setting it to 1 on initial load breaks `adjustDayPickerHeight` for a month either 2 or 3 greater than the current one. As it's calculated based off of `calendarMonthWeeks`, which is only set on `componentDidMount` right now, that value becomes stale until it's triggered again.

![adjustmonthheightissues](https://user-images.githubusercontent.com/16218249/46836493-ecef4300-cd6e-11e8-8b77-1f8563a6dd29.gif)
## Possible Fix

Adding a check to `componentDidUpdate` for a variation in `numberOfMonths` and then triggering `setCalendarMonthWeeks` again fixes the issue. 



Discussion / hints:
I am also running into this issue currently on v21.8.0
@haocow I put in a PR three years ago that fixed the problem but unfortunately the reviewer wanted me to fix their entire test suite in addition to the problem I identified. I never had time to do that, nor do I agree that I should have had to do that. We ended up going in a different direction because of it as it was a blocker for us.

Feel free to review #1395 and see if that would still apply to this library–you may be able to fix it on a local fork or attempt another PR.

EDIT: Though in reviewing some of this repo's history, it looks like this isn't really maintained anymore. Last merge to this repo was in 2019. 
@LMulvey the repo is still maintained; the latest commit was *authored* in 2019, but it was merged 29 days ago (if you click on the commit, and to the linked PR, you'll see that).

As for #1395, I don't think that's a fair characterization of what I asked you, and there were no updates after https://github.com/airbnb/react-dates/pull/1415#issuecomment-430089543 to respond to.
@ljharb You're right on the commit part for sure, apologies for that. As for #1395, perhaps I misunderstood your comment but my PR involved adding updated tests to reflect the fix I had made. This, however, revealed that an entire test block had been skipped for some time and was not functional. Your comment sounded like you were asking me to fix that test suite before my PR could be merged–something that I did not have the capacity to do at the time. 

If my comment was mischaracterizing the situation, I apologize! I'm definitely not trying to stir up any issues with anyone. 
All I wanted to ensure is that your new test passes, even if the other tests next to it don't. You implied that you'd be able to get them all passing, so I asked if you'd mind doing that in a separate PR, but #1415 deletes existing tests and it wasn't clear to me that it was the proper approach. After that it seems like neither of us revisited #1395. If you're willing to rebase it, I can get it landed.
Sounds like this was a misunderstanding on my part and for that, I apologize. Thanks for stepping in and clearing it up. Looks like #2095 was created by someone following this thread and has been rebased!

Fix patch:
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/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;


Test patch:
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(<DayPicker orientation={HORIZONTAL_ORIENTATION} />);
@@ -927,7 +924,7 @@ describe('DayPicker', () => {
         });
       });
 
-      describe('props.orientation === VERTICAL_ORIENTATION', () => {
+      describe.skip('props.orientation === VERTICAL_ORIENTATION', () => {
         it('does not call adjustDayPickerHeight', () => {
           mount(<DayPicker orientation={VERTICAL_ORIENTATION} />);
           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(<DayPicker orientation={HORIZONTAL_ORIENTATION} />);
           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(<DayPicker orientation={HORIZONTAL_ORIENTATION} />);
           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(<DayPicker orientation={HORIZONTAL_ORIENTATION} />);
           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(<DayPicker daySize={39} orientation={HORIZONTAL_ORIENTATION} />);
           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(<DayPicker orientation={HORIZONTAL_ORIENTATION} />);
           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(<DayPicker orientation={HORIZONTAL_ORIENTATION} />);
           wrapper.setState({
             monthTransition: null,
           });
           expect(updateStateAfterMonthTransitionSpy.calledOnce).to.equal(false);
         });
+
+        it('calls adjustDayPickerHeightSpy if props.numberOfMonths changes', () => {
+          const wrapper = shallow(<DayPicker numberOfMonths={2} />).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(<DayPicker numberOfMonths={2} />).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(<DayPicker orientation={VERTICAL_ORIENTATION} />);
           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(<DayPicker orientation={VERTICAL_SCROLLABLE} />);
@@ -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 };