Repository: Hacker0x01/react-datepicker (ts)

Issue:
Make Calendar Icon Clickable
When the calendar icon is displayed, clicking it does not display the calendar/time selectors.  Most datetime pickers that display a calendar will display the calendar when the icon is clicked.


Pull request:
Feature #4091 - Make the Calendar Icon clickable
Closes #4091 

### Description
This PR introduces a feature to the `<DatePicker />` component, allowing the open status of the calendar to be toggled upon clicking the calendar icon.  The behavior is controlled by a new prop `toggleCalendarOnIconClick`, which can be set to enable or disable this functionality.  By default, this functionality is disabled.

### Changes Made
1. Added the `toggleCalendarOnIconClick` prop to control the calendar toggle behavior.
2. Added the `onClick` event handler for <CalendarIcon /> based on the existence of the `toggleCalendarOnIconClick` prop
3. Added the `onClick` event even on the custom icon component without overriding the user set `onClick` listener of it based on the existence of `toggleCalendarOnIconClick` 
4. Added the test cases
5. Modified the documentation to include information on how to use this feature

![image](https://github.com/Hacker0x01/react-datepicker/assets/145029139/091b9913-d164-4989-848b-a4687d608961)

### Motivation and Context
The motivation behind this change is to provide users with the ability to control the calendar's open status directly from the calendar icon, offering more flexibility in the component's usage.

Patch:
diff --git a/docs-site/src/components/Examples/index.js b/docs-site/src/components/Examples/index.js
index 0f3606fd..052ce886 100644
--- a/docs-site/src/components/Examples/index.js
+++ b/docs-site/src/components/Examples/index.js
@@ -102,6 +102,7 @@ import ExternalForm from "../../examples/externalForm";
 import CalendarIcon from "../../examples/calendarIcon";
 import CalendarIconExternal from "../../examples/calendarIconExternal";
 import CalendarIconSvgIcon from "../../examples/calendarIconSvgIcon";
+import ToggleCalendarOnIconClick from "../../examples/toggleCalendarOnIconClick";
 
 import "./style.scss";
 import "react-datepicker/dist/react-datepicker.css";
@@ -129,6 +130,10 @@ export default class exampleComponents extends React.Component {
       title: "Calendar Icon using External Lib",
       component: CalendarIconExternal,
     },
+    {
+      title: "Toggle Calendar open status on click of the calendar icon",
+      component: ToggleCalendarOnIconClick,
+    },
     {
       title: "Calendar container",
       component: CalendarContainer,
diff --git a/docs-site/src/examples/toggleCalendarOnIconClick.js b/docs-site/src/examples/toggleCalendarOnIconClick.js
new file mode 100644
index 00000000..aea108c7
--- /dev/null
+++ b/docs-site/src/examples/toggleCalendarOnIconClick.js
@@ -0,0 +1,11 @@
+() => {
+  const [selectedDate, setSelectedDate] = useState(new Date());
+  return (
+    <DatePicker
+      showIcon
+      toggleCalendarOnIconClick
+      selected={selectedDate}
+      onChange={(date) => setSelectedDate(date)}
+    />
+  );
+};
diff --git a/src/calendar_icon.jsx b/src/calendar_icon.jsx
index 20961ce2..b214958a 100644
--- a/src/calendar_icon.jsx
+++ b/src/calendar_icon.jsx
@@ -1,12 +1,21 @@
 import React from "react";
 import PropTypes from "prop-types";
 
-const CalendarIcon = ({ icon, className = "" }) => {
+const CalendarIcon = ({ icon, className = "", onClick }) => {
   const defaultClass = "react-datepicker__calendar-icon";
 
   if (React.isValidElement(icon)) {
     return React.cloneElement(icon, {
       className: `${icon.props.className || ""} ${defaultClass} ${className}`,
+      onClick: (e) => {
+        if (typeof icon.props.onClick === "function") {
+          icon.props.onClick(e);
+        }
+
+        if (typeof onClick === "function") {
+          onClick(e);
+        }
+      },
     });
   }
 
@@ -15,6 +24,7 @@ const CalendarIcon = ({ icon, className = "" }) => {
       <i
         className={`${defaultClass} ${icon} ${className}`}
         aria-hidden="true"
+        onClick={onClick}
       />
     );
   }
@@ -25,6 +35,7 @@ const CalendarIcon = ({ icon, className = "" }) => {
       className={`${defaultClass} ${className}`}
       xmlns="http://www.w3.org/2000/svg"
       viewBox="0 0 448 512"
+      onClick={onClick}
     >
       <path d="M96 32V64H48C21.5 64 0 85.5 0 112v48H448V112c0-26.5-21.5-48-48-48H352V32c0-17.7-14.3-32-32-32s-32 14.3-32 32V64H160V32c0-17.7-14.3-32-32-32S96 14.3 96 32zM448 192H0V464c0 26.5 21.5 48 48 48H400c26.5 0 48-21.5 48-48V192z" />
     </svg>
@@ -34,6 +45,7 @@ const CalendarIcon = ({ icon, className = "" }) => {
 CalendarIcon.propTypes = {
   icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
   className: PropTypes.string,
+  onClick: PropTypes.func,
 };
 
 export default CalendarIcon;
diff --git a/src/index.jsx b/src/index.jsx
index 8f150bf6..f0eac76e 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -129,6 +129,7 @@ export default class DatePicker extends React.Component {
       excludeScrollbar: true,
       customTimeInput: null,
       calendarStartDay: undefined,
+      toggleCalendarOnIconClick: false,
     };
   }
 
@@ -183,6 +184,7 @@ export default class DatePicker extends React.Component {
     injectTimes: PropTypes.array,
     inline: PropTypes.bool,
     isClearable: PropTypes.bool,
+    toggleCalendarOnIconClick: PropTypes.func,
     showIcon: PropTypes.bool,
     icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
     calendarIconClassname: PropTypes.string,
@@ -360,10 +362,10 @@ export default class DatePicker extends React.Component {
     this.props.openToDate
       ? this.props.openToDate
       : this.props.selectsEnd && this.props.startDate
-        ? this.props.startDate
-        : this.props.selectsStart && this.props.endDate
-          ? this.props.endDate
-          : newDate();
+      ? this.props.startDate
+      : this.props.selectsStart && this.props.endDate
+      ? this.props.endDate
+      : newDate();
 
   // Convert the date from string format to standard Date format
   modifyHolidays = () =>
@@ -384,8 +386,8 @@ export default class DatePicker extends React.Component {
       minDate && isBefore(defaultPreSelection, startOfDay(minDate))
         ? minDate
         : maxDate && isAfter(defaultPreSelection, endOfDay(maxDate))
-          ? maxDate
-          : defaultPreSelection;
+        ? maxDate
+        : defaultPreSelection;
     return {
       open: this.props.startOpen || false,
       preventFocus: false,
@@ -713,6 +715,10 @@ export default class DatePicker extends React.Component {
     }
   };
 
+  toggleCalendar = () => {
+    this.setOpen(!this.state.open);
+  };
+
   handleTimeChange = (time) => {
     const selected = this.props.selected
       ? this.props.selected
@@ -1175,14 +1181,14 @@ export default class DatePicker extends React.Component {
       typeof this.props.value === "string"
         ? this.props.value
         : typeof this.state.inputValue === "string"
-          ? this.state.inputValue
-          : this.props.selectsRange
-            ? safeDateRangeFormat(
-                this.props.startDate,
-                this.props.endDate,
-                this.props,
-              )
-            : safeDateFormat(this.props.selected, this.props);
+        ? this.state.inputValue
+        : this.props.selectsRange
+        ? safeDateRangeFormat(
+            this.props.startDate,
+            this.props.endDate,
+            this.props,
+          )
+        : safeDateFormat(this.props.selected, this.props);
 
     return React.cloneElement(customInput, {
       [customInputRef]: (input) => {
@@ -1249,7 +1255,10 @@ export default class DatePicker extends React.Component {
   };
 
   renderInputContainer() {
-    const { showIcon, icon, calendarIconClassname } = this.props;
+    const { showIcon, icon, calendarIconClassname, toggleCalendarOnIconClick } =
+      this.props;
+    const { open } = this.state;
+
     return (
       <div
         className={`react-datepicker__input-container${
@@ -1257,7 +1266,17 @@ export default class DatePicker extends React.Component {
         }`}
       >
         {showIcon && (
-          <CalendarIcon icon={icon} className={calendarIconClassname} />
+          <CalendarIcon
+            icon={icon}
+            className={`${calendarIconClassname} ${
+              open && "react-datepicker-ignore-onclickoutside"
+            }`}
+            {...(toggleCalendarOnIconClick
+              ? {
+                  onClick: this.toggleCalendar,
+                }
+              : null)}
+          />
         )}
         {this.state.isRenderAriaLiveMessage && this.renderAriaLiveRegion()}
         {this.renderDateInput()}


Test patch:
diff --git a/test/calendar_icon.test.js b/test/calendar_icon.test.js
index d38dea76..151febbe 100644
--- a/test/calendar_icon.test.js
+++ b/test/calendar_icon.test.js
@@ -1,5 +1,6 @@
 import React from "react";
 import { mount } from "enzyme";
+import { render, fireEvent } from "@testing-library/react";
 import CalendarIcon from "../src/calendar_icon";
 import { IconParkSolidApplication } from "./helper_components/calendar_icon";
 
@@ -12,6 +13,14 @@ afterAll(() => {
 });
 
 describe("CalendarIcon", () => {
+  let onClickMock;
+  beforeEach(() => {
+    onClickMock = jest.fn();
+  });
+  afterEach(() => {
+    onClickMock.mockClear();
+  });
+
   it("renders a custom SVG icon when provided", () => {
     const wrapper = mount(
       <CalendarIcon showIcon icon={<IconParkSolidApplication />} />,
@@ -30,4 +39,51 @@ describe("CalendarIcon", () => {
     const wrapper = mount(<CalendarIcon showIcon />);
     expect(wrapper.find("svg.react-datepicker__calendar-icon")).toHaveLength(1);
   });
+
+  it("should fire onClick event when the icon is clicked", () => {
+    const { container } = render(
+      <CalendarIcon showIcon onClick={onClickMock} />,
+    );
+
+    const icon = container.querySelector("svg.react-datepicker__calendar-icon");
+    fireEvent.click(icon);
+
+    expect(onClickMock).toHaveBeenCalledTimes(1);
+  });
+
+  it("should fire onClick event on the click of font-awesome icon when provided", () => {
+    const { container } = render(
+      <CalendarIcon showIcon icon="fa-example-icon" onClick={onClickMock} />,
+    );
+
+    const icon = container.querySelector("i.fa-example-icon");
+    fireEvent.click(icon);
+
+    expect(onClickMock).toHaveBeenCalledTimes(1);
+  });
+
+  it("should fire onClick event on the click of custom icon component when provided", () => {
+    const onClickCustomIcon = jest.fn();
+
+    const { container } = render(
+      <CalendarIcon
+        icon={
+          <svg
+            xmlns="http://www.w3.org/2000/svg"
+            width="1em"
+            height="1em"
+            viewBox="0 0 48 48"
+            onClick={onClickCustomIcon}
+          />
+        }
+        onClick={onClickMock}
+      />,
+    );
+
+    const icon = container.querySelector("svg.react-datepicker__calendar-icon");
+    fireEvent.click(icon);
+
+    expect(onClickMock).toHaveBeenCalledTimes(1);
+    expect(onClickCustomIcon).toHaveBeenCalledTimes(1);
+  });
 });
diff --git a/test/datepicker_test.test.js b/test/datepicker_test.test.js
index 6949e7cb..e846097e 100644
--- a/test/datepicker_test.test.js
+++ b/test/datepicker_test.test.js
@@ -455,6 +455,84 @@ describe("DatePicker", () => {
     );
   });
 
+  it("should toggle the open status of calendar on click of the icon when toggleCalendarOnIconClick is set to true", () => {
+    const { container } = render(
+      <DatePicker
+        selected={utils.newDate("2023-12-17")}
+        showIcon
+        toggleCalendarOnIconClick
+      />,
+    );
+
+    const calendarIcon = container.querySelector(
+      "svg.react-datepicker__calendar-icon",
+    );
+    fireEvent.click(calendarIcon);
+
+    const reactCalendar = container.querySelector(
+      "div.react-datepicker-popper .react-datepicker",
+    );
+
+    expect(reactCalendar).not.toBeNull();
+  });
+
+  it("should not toggle the open status of calendar on click of the icon if toggleCalendarOnIconClick is set to false", () => {
+    const { container } = render(
+      <DatePicker
+        selected={utils.newDate("2023-12-17")}
+        showIcon
+        toggleCalendarOnIconClick={false}
+      />,
+    );
+
+    const calendarIcon = container.querySelector(
+      "svg.react-datepicker__calendar-icon",
+    );
+    fireEvent.click(calendarIcon);
+
+    const reactCalendar = container.querySelector(
+      "div.react-datepicker-popper .react-datepicker",
+    );
+
+    expect(reactCalendar).toBeNull();
+  });
+
+  it("should not apply the react-datepicker-ignore-onclickoutside class to calendar icon when closed", () => {
+    const { container } = render(
+      <DatePicker selected={utils.newDate("2023-12-17")} showIcon />,
+    );
+
+    const calendarIcon = container.querySelector(
+      ".react-datepicker__calendar-icon",
+    );
+    expect(
+      calendarIcon.classList.contains("react-datepicker-ignore-onclickoutside"),
+    ).toBe(false);
+  });
+
+  it("should apply the react-datepicker-ignore-onclickoutside class to calendar icon when open", () => {
+    const { container } = render(
+      <DatePicker
+        selected={utils.newDate("2023-12-17")}
+        showIcon
+        toggleCalendarOnIconClick
+      />,
+    );
+
+    let calendarIcon = container.querySelector(
+      "svg.react-datepicker__calendar-icon",
+    );
+    fireEvent.click(calendarIcon);
+
+    calendarIcon = container.querySelector(
+      "svg.react-datepicker__calendar-icon",
+    );
+
+    expect(
+      calendarIcon.classList.contains("react-datepicker-ignore-onclickoutside"),
+    ).toBe(true);
+  });
+
   it("should set the type attribute on the clear button to button", () => {
     var datePicker = TestUtils.renderIntoDocument(
       <DatePicker selected={utils.newDate("2015-12-15")} isClearable />,
@@ -2301,7 +2379,7 @@ describe("DatePicker", () => {
       datePicker,
       "react-datepicker__calendar-icon",
     ).getAttribute("class");
-    expect(showIconClass).toMatch(/^react-datepicker__calendar-icon\s?$/);
+    expect(showIconClass).toContain("react-datepicker__calendar-icon");
   });
 
   describe("Year picker", () => {