Repository: Hacker0x01/react-datepicker (ts)

Issue:
onCalendarClose is not called when selecting the same day as start&end date in selectsRange mode
**Describe the bug**
A clear and concise description of what the bug is.
As per title, the calendar is not closed, nor is `onCalendarClose` being called if the startDate and endDate is the same day.

**To Reproduce**
try it this way:

````
function DateRangePicker() {
  const [startDate, setStartDate] = React.useState(Date.now());
  const [endDate, setEndDate] = React.useState(Date.now());
  const onChange = (dates) => {
    const [start, end] = dates;
    setStartDate(start);
    setEndDate(end);
  };
  const handleCalendarClose = () => console.log("Calendar closed");

  return (
    <DatePicker
      selected={startDate}
      onChange={onChange}
      startDate={startDate}
      endDate={endDate}
      shouldCloseOnSelect={true}
      onCalendarClose={handleCalendarClose}
      selectsRange
    />
  );
}
````
1. Open Calendar
2. select some date as startDate
3. select the same date as endDate
4. the calendar is not automatically closed, nor does the "Calendar closed" log showing up in the console. 

**Expected behavior**
Expect calendar closed and onCalendarClose being called, just as if the startDate and endDate is not the same day.

**Desktop (please complete the following information):**

- OS: ubuntu 22.04
- Browser firefox
- Version 111


Pull request:
Fix #4076: Trigger onCalendarClose event and onChange even when the same date is selected as the start and the end date in a date range
Closes: #4076

This ticket fixes the following issues

1. Previously we were checking whether to emit the onCalendarClose close or not using the isBefore function but the issue is the startDate has the default time added, but the endDate we get from the calendar component doesn't have any time added to it, hence the `isBefore` check was failing as the startDate with some time is always ahead of the same date (endDate) without time
    2. Similarly we were using the same isBefore function to decide whether to consider the date as startDate or endDate for `onChange` event handler, as the isBefore will fail because of the few milli-sec difference and the onChange call will fail.  As a result, the end date (the currently selected date) is being considered as the startDate, hence we need to once again select the endDate value

I fixed the above issues by only comparing the date and not considering the time.

Patch:
diff --git a/src/date_utils.js b/src/date_utils.js
index 95301b6a..fa76b5b2 100644
--- a/src/date_utils.js
+++ b/src/date_utils.js
@@ -862,3 +862,42 @@ export function startOfMinute(d) {
 export function isSameMinute(d1, d2) {
   return startOfMinute(d1).getTime() === startOfMinute(d2).getTime();
 }
+
+/**
+ * Returns a cloned date with midnight time (00:00:00)
+ *
+ * @param {Date} date The date for which midnight time is required
+ * @param {Date} dateToCompare the date to compare with
+ * @returns {Date} A new datetime object representing the input date with midnight time
+ */
+export function getMidnightDate(date) {
+  if (!isDate(date)) {
+    throw new Error("Invalid date");
+  }
+
+  const dateWithoutTime = new Date(date);
+  dateWithoutTime.setHours(0, 0, 0, 0);
+  return dateWithoutTime;
+}
+
+/**
+ * Is the first date before the second one?
+ *
+ * @param {Date} date The date that should be before the other one to return true
+ * @param {Date} dateToCompare The date to compare with
+ * @returns {boolean} The first date is before the second date
+ *
+ * Note:
+ *  This function considers the mid-night of the given dates for comparison.
+ *  It evaluates whether date is before dateToCompare based on their mid-night timestamps.
+ */
+export function isDateBefore(date, dateToCompare) {
+  if (!isDate(date) || !isDate(dateToCompare)) {
+    throw new Error("Invalid date received");
+  }
+
+  const midnightDate = getMidnightDate(date);
+  const midnightDateToCompare = getMidnightDate(dateToCompare);
+
+  return isBefore(midnightDate, midnightDateToCompare);
+}
diff --git a/src/index.jsx b/src/index.jsx
index f0eac76e..b595e3ee 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -46,6 +46,7 @@ import {
   isMonthDisabled,
   isYearDisabled,
   getHolidaysMap,
+  isDateBefore,
 } from "./date_utils";
 import TabLoop from "./tab_loop";
 import onClickOutside from "react-onclickoutside";
@@ -591,8 +592,10 @@ export default class DatePicker extends React.Component {
       if (!this.props.selectsRange) {
         this.setOpen(false);
       }
+
       const { startDate, endDate } = this.props;
-      if (startDate && !endDate && !isBefore(date, startDate)) {
+
+      if (startDate && !endDate && !isDateBefore(date, startDate)) {
         this.setOpen(false);
       }
     }
@@ -655,7 +658,7 @@ export default class DatePicker extends React.Component {
         if (noRanges) {
           onChange([changedDate, null], event);
         } else if (hasStartRange) {
-          if (isBefore(changedDate, startDate)) {
+          if (isDateBefore(changedDate, startDate)) {
             onChange([changedDate, null], event);
           } else {
             onChange([startDate, changedDate], event);


Test patch:
diff --git a/test/date_utils_test.test.js b/test/date_utils_test.test.js
index 815f7487..f965b289 100644
--- a/test/date_utils_test.test.js
+++ b/test/date_utils_test.test.js
@@ -39,6 +39,8 @@ import {
   getHolidaysMap,
   arraysAreEqual,
   startOfMinute,
+  isDateBefore,
+  getMidnightDate,
 } from "../src/date_utils";
 import setMinutes from "date-fns/setMinutes";
 import setHours from "date-fns/setHours";
@@ -1248,4 +1250,51 @@ describe("date_utils", () => {
       expect(startOfMinute(d)).toEqual(expected);
     });
   });
+
+  describe("getMidnightDate", () => {
+    it("should return a date with midnight time when a valid date is provided", () => {
+      const inputDate = new Date(2023, 0, 1, 12, 30, 45); // January 1, 2023, 12:30:45 PM
+
+      const result = getMidnightDate(inputDate);
+
+      expect(result).toEqual(new Date(2023, 0, 1, 0, 0, 0, 0)); // January 1, 2023, 00:00:00.000
+    });
+
+    it("should throw an error when an invalid date is provided", () => {
+      const invalidDate = "not a date";
+
+      expect(() => {
+        getMidnightDate(invalidDate);
+      }).toThrowError("Invalid date");
+    });
+  });
+
+  describe("isDateBefore", () => {
+    it("should return true when date is before dateToCompare", () => {
+      const date = new Date(2022, 11, 31); // December 31, 2022
+      const dateToCompare = new Date(2023, 0, 1); // January 1, 2023
+
+      const result = isDateBefore(date, dateToCompare);
+
+      expect(result).toBe(true);
+    });
+
+    it("should return false when date is not before dateToCompare", () => {
+      const date = new Date(2023, 0, 1); // January 1, 2023
+      const dateToCompare = new Date(2022, 11, 31); // December 31, 2022
+
+      const result = isDateBefore(date, dateToCompare);
+
+      expect(result).toBe(false);
+    });
+
+    it("should throw an error when either date or dateToCompare is not a valid date", () => {
+      expect(() => {
+        const invalidDate = "not a date";
+        const validDate = new Date(2023, 0, 1); // January 1, 2023
+
+        isDateBefore(invalidDate, validDate);
+      }).toThrowError();
+    });
+  });
 });
diff --git a/test/datepicker_test.test.js b/test/datepicker_test.test.js
index e846097e..061caa13 100644
--- a/test/datepicker_test.test.js
+++ b/test/datepicker_test.test.js
@@ -4,7 +4,7 @@ import { findDOMNode } from "react-dom";
 import TestUtils from "react-dom/test-utils";
 import { enUS, enGB } from "date-fns/locale";
 import { mount } from "enzyme";
-import { render, fireEvent } from "@testing-library/react";
+import { render, act, waitFor, fireEvent } from "@testing-library/react";
 import defer from "lodash/defer";
 import DatePicker, { registerLocale } from "../src/index.jsx";
 import Day from "../src/day.jsx";
@@ -44,6 +44,18 @@ function goToLastMonth(datePicker) {
   TestUtils.Simulate.click(findDOMNode(lastMonthButton));
 }
 
+function formatDayWithZeros(day) {
+  const dayString = day.toString();
+
+  if (dayString.length === 1) {
+    return `00${dayString}`;
+  }
+  if (dayString.length === 2) {
+    return `0${dayString}`;
+  }
+  return dayString;
+}
+
 describe("DatePicker", () => {
   afterEach(() => {
     jest.resetAllMocks();
@@ -2019,6 +2031,94 @@ describe("DatePicker", () => {
       expect(onChangeSpy.mock.calls[0][0][0]).toBeNull();
       expect(onChangeSpy.mock.calls[0][0][1]).toBeNull();
     });
+
+    it("should call the onChange even when the startDate and the endDate is same in the range (case when we programmatically set the startDate, but set the same endDate through UI)", async () => {
+      let startDate = new Date();
+      let endDate = null;
+
+      const onChangeSpy = jest.fn();
+
+      const { container } = render(
+        <DatePicker
+          startDate={startDate}
+          endDate={endDate}
+          onChange={onChangeSpy}
+          shouldCloseOnSelect
+          selectsRange
+        />,
+      );
+
+      const input = container.querySelector("input");
+      expect(input).toBeTruthy();
+      fireEvent.click(input);
+
+      let calendar = container.querySelector(".react-datepicker");
+      expect(calendar).toBeTruthy();
+
+      // Select the same date as the start date
+      const startDatePrefixedWithZeros = formatDayWithZeros(
+        startDate.getDate(),
+      );
+      const endDateElement = container.querySelector(
+        `.react-datepicker__day--${startDatePrefixedWithZeros}`,
+      );
+      fireEvent.click(endDateElement);
+
+      await act(async () => {
+        await waitFor(() => {
+          expect(onChangeSpy).toHaveBeenCalled();
+        });
+      });
+    });
+
+    it("should hide the calendar even when the startDate and the endDate is same in the range", async () => {
+      let startDate = new Date();
+      let endDate = null;
+
+      const onCalendarCloseSpy = jest.fn();
+
+      const onChange = (dates) => {
+        const [start, end] = dates;
+        startDate = start;
+        endDate = end;
+      };
+
+      const { container } = render(
+        <DatePicker
+          startDate={startDate}
+          endDate={endDate}
+          onChange={onChange}
+          onCalendarClose={onCalendarCloseSpy}
+          shouldCloseOnSelect
+          selectsRange
+        />,
+      );
+
+      const input = container.querySelector("input");
+      expect(input).toBeTruthy();
+      fireEvent.click(input);
+
+      let calendar = container.querySelector(".react-datepicker");
+      expect(calendar).toBeTruthy();
+
+      // Select the same date as the start date
+      const startDatePrefixedWithZeros = formatDayWithZeros(
+        startDate.getDate(),
+      );
+      const endDateElement = container.querySelector(
+        `.react-datepicker__day--${startDatePrefixedWithZeros}`,
+      );
+      fireEvent.click(endDateElement);
+
+      await act(async () => {
+        await waitFor(() => {
+          calendar = container.querySelector(".react-datepicker");
+          expect(calendar).toBeFalsy();
+
+          expect(onCalendarCloseSpy).toHaveBeenCalled();
+        });
+      });
+    });
   });
 
   describe("duplicate dates when multiple months", () => {