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

Issue:
Why does isOutsideRange get called multiple times for each date, and at different times (midnight vs noon)?
**react-dates version**
react-dates@21.8.0

**Describe the bug**
isOutsideRange prop callback is passing each calendar day multiple times for some reason

specifically, each date can be called at local time midnight or local time noon, which makes it hard for me to know which one I'm supposed to compare for to return a correct value to isOutSideRange

expected behavior: returns consistent time, either always midnight local time or always noon local time

**Source code (including props configuration)**
Load and see strange output
```
<DayPickerRangeController
  isOutsideRange={(date) => console.log('huh', date.format())}
/>
```

![image](https://user-images.githubusercontent.com/5566843/128083850-9895621e-bfc8-40c3-9950-54715283bff2.png)
![image](https://user-images.githubusercontent.com/5566843/128083863-b89f386d-0973-4779-9ebf-cd109274df85.png)



https://user-images.githubusercontent.com/5566843/128084224-beaf1af4-0329-4172-af0f-d4abf7aa34d8.mp4



**Desktop (please complete the following information):**
 - OS: mac
 - Browser chrome
 - Version 92.0.4515.107 (Official Build) (x86_64)





Discussion / hints:
I'd expect always noon, since midnight doesn't always exist. This seems like a bug.

Fix patch:
diff --git a/src/components/DayPicker.jsx b/src/components/DayPicker.jsx
index a56653cd23..83afc34680 100644
--- a/src/components/DayPicker.jsx
+++ b/src/components/DayPicker.jsx
@@ -203,7 +203,7 @@ class DayPicker extends React.PureComponent {
 
     const currentMonth = props.hidden ? moment() : props.initialVisibleMonth();
 
-    let focusedDate = currentMonth.clone().startOf('month');
+    let focusedDate = currentMonth.clone().startOf('month').hour(12);
     if (props.getFirstFocusableDay) {
       focusedDate = props.getFirstFocusableDay(currentMonth);
     }
@@ -487,7 +487,7 @@ class DayPicker extends React.PureComponent {
         break;
       case 'Home':
         e.preventDefault();
-        newFocusedDate.startOf('week');
+        newFocusedDate.startOf('week').hour(12);
         didTransitionMonth = this.maybeTransitionPrevMonth(newFocusedDate);
         break;
       case 'PageUp':
@@ -703,7 +703,7 @@ class DayPicker extends React.PureComponent {
     }
 
     if (newMonth && (!focusedDate || !isDayVisible(focusedDate, newMonth, numberOfMonths))) {
-      focusedDate = newMonth.clone().startOf('month');
+      focusedDate = newMonth.clone().startOf('month').hour(12);
     }
 
     return focusedDate;
diff --git a/src/components/DayPickerRangeController.jsx b/src/components/DayPickerRangeController.jsx
index 87c61f0544..988595a855 100644
--- a/src/components/DayPickerRangeController.jsx
+++ b/src/components/DayPickerRangeController.jsx
@@ -1045,7 +1045,7 @@ export default class DayPickerRangeController extends React.PureComponent {
       numberOfMonths,
     } = this.props;
 
-    let focusedDate = newMonth.clone().startOf('month');
+    let focusedDate = newMonth.clone().startOf('month').hour(12);
     if (focusedInput === START_DATE && startDate) {
       focusedDate = startDate.clone();
     } else if (focusedInput === END_DATE && !endDate && startDate) {
diff --git a/src/components/DayPickerSingleDateController.jsx b/src/components/DayPickerSingleDateController.jsx
index 36705917a6..dad08cb141 100644
--- a/src/components/DayPickerSingleDateController.jsx
+++ b/src/components/DayPickerSingleDateController.jsx
@@ -545,7 +545,7 @@ export default class DayPickerSingleDateController extends React.PureComponent {
   getFirstFocusableDay(newMonth) {
     const { date, numberOfMonths } = this.props;
 
-    let focusedDate = newMonth.clone().startOf('month');
+    let focusedDate = newMonth.clone().startOf('month').hour(12);
     if (date) {
       focusedDate = date.clone();
     }
diff --git a/src/utils/getNumberOfCalendarMonthWeeks.js b/src/utils/getNumberOfCalendarMonthWeeks.js
index 0ed4da8676..0e9482664f 100644
--- a/src/utils/getNumberOfCalendarMonthWeeks.js
+++ b/src/utils/getNumberOfCalendarMonthWeeks.js
@@ -9,7 +9,7 @@ export default function getNumberOfCalendarMonthWeeks(
   month,
   firstDayOfWeek = moment.localeData().firstDayOfWeek(),
 ) {
-  const firstDayOfMonth = month.clone().startOf('month');
+  const firstDayOfMonth = month.clone().startOf('month').hour(12);
   const numBlankDays = getBlankDaysBeforeFirstDay(firstDayOfMonth, firstDayOfWeek);
   return Math.ceil((numBlankDays + month.daysInMonth()) / 7);
 }
diff --git a/src/utils/isDayVisible.js b/src/utils/isDayVisible.js
index 34b9420339..e0d43f376c 100644
--- a/src/utils/isDayVisible.js
+++ b/src/utils/isDayVisible.js
@@ -21,7 +21,7 @@ export default function isDayVisible(day, month, numberOfMonths, enableOutsideDa
 
   if (enableOutsideDays) {
     if (!startCacheOutsideDays.has(startKey)) {
-      startCacheOutsideDays.set(startKey, month.clone().startOf('month').startOf('week'));
+      startCacheOutsideDays.set(startKey, month.clone().startOf('month').startOf('week').hour(12));
     }
 
     if (isBeforeDay(day, startCacheOutsideDays.get(startKey))) return false;
@@ -30,7 +30,8 @@ export default function isDayVisible(day, month, numberOfMonths, enableOutsideDa
       endCacheOutsideDays.set(
         endKey,
         month.clone().endOf('week').add(numberOfMonths - 1, 'months').endOf('month')
-          .endOf('week'),
+          .endOf('week')
+          .hour(12),
       );
     }
 
@@ -40,7 +41,7 @@ export default function isDayVisible(day, month, numberOfMonths, enableOutsideDa
   // !enableOutsideDays
 
   if (!startCacheInsideDays.has(startKey)) {
-    startCacheInsideDays.set(startKey, month.clone().startOf('month'));
+    startCacheInsideDays.set(startKey, month.clone().startOf('month').hour(12));
   }
 
   if (isBeforeDay(day, startCacheInsideDays.get(startKey))) return false;
@@ -48,7 +49,7 @@ export default function isDayVisible(day, month, numberOfMonths, enableOutsideDa
   if (!endCacheInsideDays.has(endKey)) {
     endCacheInsideDays.set(
       endKey,
-      month.clone().add(numberOfMonths - 1, 'months').endOf('month'),
+      month.clone().add(numberOfMonths - 1, 'months').endOf('month').hour(12),
     );
   }
 


Test patch:
diff --git a/test/components/DayPickerRangeController_spec.jsx b/test/components/DayPickerRangeController_spec.jsx
index f530b93f83..c224009c7c 100644
--- a/test/components/DayPickerRangeController_spec.jsx
+++ b/test/components/DayPickerRangeController_spec.jsx
@@ -3775,6 +3775,21 @@ describe('DayPickerRangeController', () => {
       });
     });
 
+    it('time is a noon', () => {
+      sinon.stub(DayPickerRangeController.prototype, 'isBlocked').returns(false);
+      const wrapper = shallow((
+        <DayPickerRangeController
+          focusedInput={null}
+          startDate={null}
+          endDate={null}
+          onFocusChange={sinon.stub()}
+          onDatesChange={sinon.stub()}
+        />
+      ));
+      const firstFocusableDay = wrapper.instance().getFirstFocusableDay(today);
+      expect(firstFocusableDay.hours()).to.equal(12);
+    });
+
     describe('desired day is blocked', () => {
       it('returns next unblocked visible day after desired day if exists', () => {
         const isBlockedStub = sinon.stub(DayPickerRangeController.prototype, 'isBlocked');
diff --git a/test/components/DayPickerSingleDateController_spec.jsx b/test/components/DayPickerSingleDateController_spec.jsx
index f6a62e04e5..5e6e09522f 100644
--- a/test/components/DayPickerSingleDateController_spec.jsx
+++ b/test/components/DayPickerSingleDateController_spec.jsx
@@ -1000,6 +1000,19 @@ describe('DayPickerSingleDateController', () => {
       expect(firstFocusableDay.isSame(date, 'day')).to.equal(true);
     });
 
+    it('time is a noon', () => {
+      sinon.stub(DayPickerSingleDateController.prototype, 'isBlocked').returns(false);
+      const wrapper = shallow((
+        <DayPickerSingleDateController
+          date={null}
+          onDateChange={sinon.stub()}
+          onFocusChange={sinon.stub()}
+        />
+      ));
+      const firstFocusableDay = wrapper.instance().getFirstFocusableDay(today);
+      expect(firstFocusableDay.hours()).to.equal(12);
+    });
+
     describe('desired date is blocked', () => {
       it('returns first unblocked visible day if exists', () => {
         const isBlockedStub = sinon.stub(DayPickerSingleDateController.prototype, 'isBlocked');
diff --git a/test/utils/isDayVisible_spec.js b/test/utils/isDayVisible_spec.js
index f42455b549..074c5f2e3e 100644
--- a/test/utils/isDayVisible_spec.js
+++ b/test/utils/isDayVisible_spec.js
@@ -49,4 +49,12 @@ describe('#isDayVisible', () => {
       expect(isDayVisible(test, currentMonth, 1, true)).to.equal(false);
     });
   });
+
+  // this test fails when run with the whole suite,
+  // potentially due to cache pollution from other tests
+  it.skip('works when the first day of the week that starts the month does not have a midnight', () => {
+    const march29 = moment('2020-03-29').utcOffset(-1 /* 'Atlantic/Azores' */);
+    const april2020 = moment('2020-04-02').utcOffset(-1 /* 'Atlantic/Azores' */);
+    expect(isDayVisible(march29, april2020, 1, true)).to.equal(true);
+  });
 });