Repository: Hacker0x01/react-datepicker
Pull request #4295 (2023-10-07 02:45:04)

feat: add customizable calendar icon
#### **🚀 Proposal:**
Enhance the customization capabilities of the calendar icon in the `DatePicker` component, allowing the use of custom icons and not solely the default provided icon.

#### **🏗 Changes Made:**
1. **New `icon` Property:**
   - Introduced an `icon` prop that accepts either a string (icon class name) or a React component (e.g., a custom SVG).
   - If `icon` is a string, an `<i>` element is rendered with that string as its class name.
   - If `icon` is a React component, it is rendered as-is.

2. **New `calendarIconClassName` Property:**
   - Introduced an additional `calendarIconClassName` prop for further icon styling customization.
   - The value of `calendarIconClassName` will be added as an additional CSS class to the calendar icon, allowing enhanced style customization.

3. **Code Refactor:**
   - Extracted the calendar icon rendering into a separate component (`CalendarIcon`) for improved code readability and maintainability.
   - Updated the rendering logic in the `renderInputContainer` method to utilize the new `CalendarIcon` component.

#### **🎯 Motivation:**
The inspiration behind these changes is to bestow developers with greater control over the `DatePicker`'s display. By enabling calendar icon customization, we enhance the developers' UI control over the `DatePicker` without sacrificing functionality or user experience.

#### **👀 How to Use the New Feature:**
You can now utilize various renowned icon libraries, such as Font Awesome, Material Icons, or your custom SVGs, with the new `icon` prop. Here are examples of how to implement this:

```jsx
// Using an icon class name from Font Awesome.
<DatePicker
  showIcon={true}
  icon="fa fa-calendar-alt"
/>

// Using an icon class name from Material Icons.
<DatePicker
  showIcon={true}
  icon="material-icons calendar-icon"
/>

// Using a React component or custom SVG.
import MyCustomIcon from './path-to-icons/MyCustomIcon';

<DatePicker
  showIcon={true}
  icon={<MyCustomIcon />}
/>
```

#### **🔄 Backward Compatibility:**
It is pivotal to note that these changes have been implemented to ensure backward compatibility. Existing implementations of the `DatePicker` using the `showIcon` prop will continue to function as before, displaying the default calendar icon if no `icon` prop is provided.

---

### 📝 Final Notes:
These proposed changes aim to deliver a flexible solution to accommodate various needs without introducing significant overhead to the codebase or altering the API properties in a way that disrupts existing implementations. I am open to feedback and suggestions to refine this proposal and hope that this change brings valuable additions to the `react-datepicker` user community.

---


Patch:
diff --git a/docs-site/public/index.html b/docs-site/public/index.html
index 1055c50b..39469bb8 100644
--- a/docs-site/public/index.html
+++ b/docs-site/public/index.html
@@ -9,6 +9,9 @@
   <meta name="keywords" content="React, HTML, CSS, JavaScript, JSX, date, datepicker">
   <title>React Datepicker crafted by HackerOne</title>
   <link href="https://fonts.googleapis.com/css?family=Open+Sans&display=swap" rel="stylesheet">
+  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"
+    integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA=="
+    crossorigin="anonymous" referrerpolicy="no-referrer" />
 </head>
 
 <body>
diff --git a/docs-site/src/components/Examples/index.js b/docs-site/src/components/Examples/index.js
index d777e61e..e4f98d67 100644
--- a/docs-site/src/components/Examples/index.js
+++ b/docs-site/src/components/Examples/index.js
@@ -99,6 +99,8 @@ import selectsRangeWithDisabledDates from "../../examples/selectsRangeWithDisabl
 import CalendarStartDay from "../../examples/calendarStartDay";
 import ExternalForm from "../../examples/externalForm";
 import CalendarIcon from "../../examples/calendarIcon";
+import CalendarIconExternal from "../../examples/calendarIconExternal";
+import CalendarIconSvgIcon from "../../examples/calendarIconSvgIcon";
 
 import "./style.scss";
 import "react-datepicker/dist/react-datepicker.css";
@@ -118,6 +120,14 @@ export default class exampleComponents extends React.Component {
       title: "Calendar Icon",
       component: CalendarIcon,
     },
+    {
+      title: "Calendar Icon using React Svg Component",
+      component: CalendarIconSvgIcon,
+    },
+    {
+      title: "Calendar Icon using External Lib",
+      component: CalendarIconExternal,
+    },
     {
       title: "Calendar container",
       component: CalendarContainer,
diff --git a/docs-site/src/examples/calendarIconExternal.js b/docs-site/src/examples/calendarIconExternal.js
new file mode 100644
index 00000000..f3e6f6b2
--- /dev/null
+++ b/docs-site/src/examples/calendarIconExternal.js
@@ -0,0 +1,11 @@
+() => {
+  const [startDate, setStartDate] = useState(new Date());
+  return (
+    <DatePicker
+      showIcon
+      selected={startDate}
+      onChange={(date) => setStartDate(date)}
+      icon="fa fa-calendar"
+    />
+  );
+};
diff --git a/docs-site/src/examples/calendarIconSvgIcon.js b/docs-site/src/examples/calendarIconSvgIcon.js
new file mode 100644
index 00000000..ffd88f8b
--- /dev/null
+++ b/docs-site/src/examples/calendarIconSvgIcon.js
@@ -0,0 +1,33 @@
+() => {
+  const [startDate, setStartDate] = useState(new Date());
+  return (
+    <DatePicker
+      showIcon
+      selected={startDate}
+      onChange={(date) => setStartDate(date)}
+      icon={
+        <svg
+          xmlns="http://www.w3.org/2000/svg"
+          width="1em"
+          height="1em"
+          viewBox="0 0 48 48"
+        >
+          <mask id="ipSApplication0">
+            <g fill="none" stroke="#fff" strokeLinejoin="round" strokeWidth="4">
+              <path strokeLinecap="round" d="M40.04 22v20h-32V22"></path>
+              <path
+                fill="#fff"
+                d="M5.842 13.777C4.312 17.737 7.263 22 11.51 22c3.314 0 6.019-2.686 6.019-6a6 6 0 0 0 6 6h1.018a6 6 0 0 0 6-6c0 3.314 2.706 6 6.02 6c4.248 0 7.201-4.265 5.67-8.228L39.234 6H8.845l-3.003 7.777Z"
+              ></path>
+            </g>
+          </mask>
+          <path
+            fill="currentColor"
+            d="M0 0h48v48H0z"
+            mask="url(#ipSApplication0)"
+          ></path>
+        </svg>
+      }
+    />
+  );
+};
diff --git a/docs/datepicker.md b/docs/datepicker.md
index 948b98fd..171e2eb0 100644
--- a/docs/datepicker.md
+++ b/docs/datepicker.md
@@ -45,7 +45,6 @@ General datepicker component.
 | `injectTimes`                | `array`                        |                 |                                                                                                                                                                                                                                                                                                                      |
 | `inline`                     | `bool`                         |                 |                                                                                                                                                                                                                                                                                                                      |
 | `isClearable`                | `bool`                         |                 |                                                                                                                                                                                                                                                                                                                      |
-| `showIcon`                   | `bool`                         |                 |                                                                                                                                                                                                                                                                                                                      |
 | `locale`                     | `string`                       |                 |                                                                                                                                                                                                                                                                                                                      |
 | `maxDate`                    | `instanceOf(Date)`             |                 |                                                                                                                                                                                                                                                                                                                      |
 | `maxTime`                    | `instanceOf(Date)`             |                 |                                                                                                                                                                                                                                                                                                                      |
@@ -69,7 +68,8 @@ General datepicker component.
 | `peekNextMonth`              | `bool`                         |                 |                                                                                                                                                                                                                                                                                                                      |
 | `placeholderText`            | `string`                       |                 |                                                                                                                                                                                                                                                                                                                      |
 | `popperClassName`            | `string`                       |                 |                                                                                                                                                                                                                                                                                                                      |
-| `popperContainer`            | `func`                         |                 |                                                                                                                                                                                                                                                                                                                      |
+| `popperContainer`            | `func`                         |                 |
+|                              |
 | `popperModifiers`            | `object`                       |                 |                                                                                                                                                                                                                                                                                                                      |
 | `popperPlacement`            | `enumpopperPlacementPositions` |                 |                                                                                                                                                                                                                                                                                                                      |
 | `preventOpenOnFocus`         | `bool`                         | false           | When this is true, the datepicker will not automatically open when the date field is focussed                                                                                                                                                                                                                        |
@@ -103,3 +103,6 @@ General datepicker component.
 |                              |
 | `yearItemNumber`             | `number`                       | `12`            |                                                                                                                                                                                                                                                                                                                      |
 | `yearDropdownItemNumber`     | `number`                       |                 |                                                                                                                                                                                                                                                                                                                      |
+| `icon`                       | `string` or `element`          |                 | Allows using a custom calendar icon. Accepts a string (icon class name) or a React component (e.g., custom SVG). If a string is passed, an `<i>` element is rendered with that string as its class name. If a React component is passed, it is rendered as-is.                                                       |
+| `calendarIconClassName`      | `string`                       |                 | Accepts a string that will be added as an additional CSS class to the calendar icon, allowing further styling customization.                                                                                                                                                                                         |
+| `showIcon`                   | `bool`                         | `true`          | Determines whether the calendar icon is displayed. Set to `true` to display the icon, and `false` to hide it. If `icon` prop is also provided, the custom icon will be displayed when `showIcon` is `true`.                                                                                                          |
diff --git a/src/calendar_icon.jsx b/src/calendar_icon.jsx
new file mode 100644
index 00000000..23264d5a
--- /dev/null
+++ b/src/calendar_icon.jsx
@@ -0,0 +1,43 @@
+import React from "react";
+import PropTypes from "prop-types";
+
+const CalendarIcon = ({ icon, className }) => {
+  const defaultClass = "react-datepicker__calendar-icon";
+
+  if (React.isValidElement(icon)) {
+    return React.cloneElement(icon, {
+      className: `${icon.props.className || ""} ${defaultClass} ${className}`,
+    });
+  }
+
+  if (typeof icon === "string") {
+    return (
+      <i
+        className={`${defaultClass} ${icon} ${className}`}
+        aria-hidden="true"
+      />
+    );
+  }
+
+  // Default SVG Icon
+  return (
+    <svg
+      className="react-datepicker__calendar-icon"
+      xmlns="http://www.w3.org/2000/svg"
+      viewBox="0 0 448 512"
+    >
+      <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>
+  );
+};
+
+CalendarIcon.propTypes = {
+  icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+  className: PropTypes.string,
+};
+
+CalendarIcon.defaultProps = {
+  className: "",
+};
+
+export default CalendarIcon;
diff --git a/src/index.jsx b/src/index.jsx
index 9e1c86ba..bc859495 100644
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -1,6 +1,7 @@
 import React from "react";
 import PropTypes from "prop-types";
 import Calendar from "./calendar";
+import CalendarIcon from "./calendar_icon";
 import Portal from "./portal";
 import PopperComponent, { popperPlacementPositions } from "./popper_component";
 import classnames from "classnames";
@@ -181,6 +182,8 @@ export default class DatePicker extends React.Component {
     inline: PropTypes.bool,
     isClearable: PropTypes.bool,
     showIcon: PropTypes.bool,
+    icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+    calendarIconClassname: PropTypes.string,
     locale: PropTypes.oneOfType([
       PropTypes.string,
       PropTypes.shape({ locale: PropTypes.object }),
@@ -1212,7 +1215,7 @@ export default class DatePicker extends React.Component {
   };
 
   renderInputContainer() {
-    const { showIcon } = this.props;
+    const { showIcon, icon, calendarIconClassname } = this.props;
     return (
       <div
         className={`react-datepicker__input-container${
@@ -1220,13 +1223,7 @@ export default class DatePicker extends React.Component {
         }`}
       >
         {showIcon && (
-          <svg
-            className="react-datepicker__calendar-icon"
-            xmlns="http://www.w3.org/2000/svg"
-            viewBox="0 0 448 512"
-          >
-            <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>
+          <CalendarIcon icon={icon} className={calendarIconClassname} />
         )}
         {this.state.isRenderAriaLiveMessage && this.renderAriaLiveRegion()}
         {this.renderDateInput()}


Test patch:
diff --git a/test/calendar_icon.test.js b/test/calendar_icon.test.js
new file mode 100644
index 00000000..d38dea76
--- /dev/null
+++ b/test/calendar_icon.test.js
@@ -0,0 +1,33 @@
+import React from "react";
+import { mount } from "enzyme";
+import CalendarIcon from "../src/calendar_icon";
+import { IconParkSolidApplication } from "./helper_components/calendar_icon";
+
+beforeAll(() => {
+  jest.spyOn(console, "error").mockImplementation(() => {});
+});
+
+afterAll(() => {
+  console.error.mockRestore();
+});
+
+describe("CalendarIcon", () => {
+  it("renders a custom SVG icon when provided", () => {
+    const wrapper = mount(
+      <CalendarIcon showIcon icon={<IconParkSolidApplication />} />,
+    );
+    expect(
+      wrapper.find('[data-testid="icon-park-solid-application"]'),
+    ).toHaveLength(1);
+  });
+
+  it("renders a FontAwesome icon when provided", () => {
+    const wrapper = mount(<CalendarIcon showIcon icon="fa-example-icon" />);
+    expect(wrapper.find("i.fa-example-icon")).toHaveLength(1);
+  });
+
+  it("does not render an icon when none is provided", () => {
+    const wrapper = mount(<CalendarIcon showIcon />);
+    expect(wrapper.find("svg.react-datepicker__calendar-icon")).toHaveLength(1);
+  });
+});
diff --git a/test/helper_components/calendar_icon.jsx b/test/helper_components/calendar_icon.jsx
new file mode 100644
index 00000000..1f29561f
--- /dev/null
+++ b/test/helper_components/calendar_icon.jsx
@@ -0,0 +1,29 @@
+import React from "react";
+
+export function IconParkSolidApplication(props) {
+  return (
+    <svg
+      data-testid="icon-park-solid-application"
+      xmlns="http://www.w3.org/2000/svg"
+      width="1em"
+      height="1em"
+      viewBox="0 0 48 48"
+      {...props}
+    >
+      <mask id="ipSApplication0">
+        <g fill="none" stroke="#fff" strokeLinejoin="round" strokeWidth="4">
+          <path strokeLinecap="round" d="M40.04 22v20h-32V22" />
+          <path
+            fill="#fff"
+            d="M5.842 13.777C4.312 17.737 7.263 22 11.51 22c3.314 0 6.019-2.686 6.019-6a6 6 0 0 0 6 6h1.018a6 6 0 0 0 6-6c0 3.314 2.706 6 6.02 6c4.248 0 7.201-4.265 5.67-8.228L39.234 6H8.845l-3.003 7.777Z"
+          />
+        </g>
+      </mask>
+      <path
+        fill="currentColor"
+        d="M0 0h48v48H0z"
+        mask="url(#ipSApplication0)"
+      />
+    </svg>
+  );
+}