Skip to content

Commit fa3eb51

Browse files
Merge pull request #4295 from frankops11/feature/customizable-calendar-icon
feat: add customizable calendar icon
2 parents 64d750a + dedb21b commit fa3eb51

9 files changed

Lines changed: 172 additions & 10 deletions

File tree

docs-site/public/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
<meta name="keywords" content="React, HTML, CSS, JavaScript, JSX, date, datepicker">
1010
<title>React Datepicker crafted by HackerOne</title>
1111
<link href="https://fonts.googleapis.com/css?family=Open+Sans&display=swap" rel="stylesheet">
12+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"
13+
integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA=="
14+
crossorigin="anonymous" referrerpolicy="no-referrer" />
1215
</head>
1316

1417
<body>

docs-site/src/components/Examples/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ import selectsRangeWithDisabledDates from "../../examples/selectsRangeWithDisabl
9999
import CalendarStartDay from "../../examples/calendarStartDay";
100100
import ExternalForm from "../../examples/externalForm";
101101
import CalendarIcon from "../../examples/calendarIcon";
102+
import CalendarIconExternal from "../../examples/calendarIconExternal";
103+
import CalendarIconSvgIcon from "../../examples/calendarIconSvgIcon";
102104

103105
import "./style.scss";
104106
import "react-datepicker/dist/react-datepicker.css";
@@ -118,6 +120,14 @@ export default class exampleComponents extends React.Component {
118120
title: "Calendar Icon",
119121
component: CalendarIcon,
120122
},
123+
{
124+
title: "Calendar Icon using React Svg Component",
125+
component: CalendarIconSvgIcon,
126+
},
127+
{
128+
title: "Calendar Icon using External Lib",
129+
component: CalendarIconExternal,
130+
},
121131
{
122132
title: "Calendar container",
123133
component: CalendarContainer,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
() => {
2+
const [startDate, setStartDate] = useState(new Date());
3+
return (
4+
<DatePicker
5+
showIcon
6+
selected={startDate}
7+
onChange={(date) => setStartDate(date)}
8+
icon="fa fa-calendar"
9+
/>
10+
);
11+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
() => {
2+
const [startDate, setStartDate] = useState(new Date());
3+
return (
4+
<DatePicker
5+
showIcon
6+
selected={startDate}
7+
onChange={(date) => setStartDate(date)}
8+
icon={
9+
<svg
10+
xmlns="http://www.w3.org/2000/svg"
11+
width="1em"
12+
height="1em"
13+
viewBox="0 0 48 48"
14+
>
15+
<mask id="ipSApplication0">
16+
<g fill="none" stroke="#fff" strokeLinejoin="round" strokeWidth="4">
17+
<path strokeLinecap="round" d="M40.04 22v20h-32V22"></path>
18+
<path
19+
fill="#fff"
20+
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"
21+
></path>
22+
</g>
23+
</mask>
24+
<path
25+
fill="currentColor"
26+
d="M0 0h48v48H0z"
27+
mask="url(#ipSApplication0)"
28+
></path>
29+
</svg>
30+
}
31+
/>
32+
);
33+
};

docs/datepicker.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ General datepicker component.
4545
| `injectTimes` | `array` | | |
4646
| `inline` | `bool` | | |
4747
| `isClearable` | `bool` | | |
48-
| `showIcon` | `bool` | | |
4948
| `locale` | `string` | | |
5049
| `maxDate` | `instanceOf(Date)` | | |
5150
| `maxTime` | `instanceOf(Date)` | | |
@@ -69,7 +68,8 @@ General datepicker component.
6968
| `peekNextMonth` | `bool` | | |
7069
| `placeholderText` | `string` | | |
7170
| `popperClassName` | `string` | | |
72-
| `popperContainer` | `func` | | |
71+
| `popperContainer` | `func` | |
72+
| |
7373
| `popperModifiers` | `object` | | |
7474
| `popperPlacement` | `enumpopperPlacementPositions` | | |
7575
| `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.
103103
| |
104104
| `yearItemNumber` | `number` | `12` | |
105105
| `yearDropdownItemNumber` | `number` | | |
106+
| `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. |
107+
| `calendarIconClassName` | `string` | | Accepts a string that will be added as an additional CSS class to the calendar icon, allowing further styling customization. |
108+
| `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`. |

src/calendar_icon.jsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React from "react";
2+
import PropTypes from "prop-types";
3+
4+
const CalendarIcon = ({ icon, className }) => {
5+
const defaultClass = "react-datepicker__calendar-icon";
6+
7+
if (React.isValidElement(icon)) {
8+
return React.cloneElement(icon, {
9+
className: `${icon.props.className || ""} ${defaultClass} ${className}`,
10+
});
11+
}
12+
13+
if (typeof icon === "string") {
14+
return (
15+
<i
16+
className={`${defaultClass} ${icon} ${className}`}
17+
aria-hidden="true"
18+
/>
19+
);
20+
}
21+
22+
// Default SVG Icon
23+
return (
24+
<svg
25+
className="react-datepicker__calendar-icon"
26+
xmlns="http://www.w3.org/2000/svg"
27+
viewBox="0 0 448 512"
28+
>
29+
<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" />
30+
</svg>
31+
);
32+
};
33+
34+
CalendarIcon.propTypes = {
35+
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
36+
className: PropTypes.string,
37+
};
38+
39+
CalendarIcon.defaultProps = {
40+
className: "",
41+
};
42+
43+
export default CalendarIcon;

src/index.jsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from "react";
22
import PropTypes from "prop-types";
33
import Calendar from "./calendar";
4+
import CalendarIcon from "./calendar_icon";
45
import Portal from "./portal";
56
import PopperComponent, { popperPlacementPositions } from "./popper_component";
67
import classnames from "classnames";
@@ -181,6 +182,8 @@ export default class DatePicker extends React.Component {
181182
inline: PropTypes.bool,
182183
isClearable: PropTypes.bool,
183184
showIcon: PropTypes.bool,
185+
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
186+
calendarIconClassname: PropTypes.string,
184187
locale: PropTypes.oneOfType([
185188
PropTypes.string,
186189
PropTypes.shape({ locale: PropTypes.object }),
@@ -1212,21 +1215,15 @@ export default class DatePicker extends React.Component {
12121215
};
12131216

12141217
renderInputContainer() {
1215-
const { showIcon } = this.props;
1218+
const { showIcon, icon, calendarIconClassname } = this.props;
12161219
return (
12171220
<div
12181221
className={`react-datepicker__input-container${
12191222
showIcon ? " react-datepicker__view-calendar-icon" : ""
12201223
}`}
12211224
>
12221225
{showIcon && (
1223-
<svg
1224-
className="react-datepicker__calendar-icon"
1225-
xmlns="http://www.w3.org/2000/svg"
1226-
viewBox="0 0 448 512"
1227-
>
1228-
<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" />
1229-
</svg>
1226+
<CalendarIcon icon={icon} className={calendarIconClassname} />
12301227
)}
12311228
{this.state.isRenderAriaLiveMessage && this.renderAriaLiveRegion()}
12321229
{this.renderDateInput()}

test/calendar_icon.test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React from "react";
2+
import { mount } from "enzyme";
3+
import CalendarIcon from "../src/calendar_icon";
4+
import { IconParkSolidApplication } from "./helper_components/calendar_icon";
5+
6+
beforeAll(() => {
7+
jest.spyOn(console, "error").mockImplementation(() => {});
8+
});
9+
10+
afterAll(() => {
11+
console.error.mockRestore();
12+
});
13+
14+
describe("CalendarIcon", () => {
15+
it("renders a custom SVG icon when provided", () => {
16+
const wrapper = mount(
17+
<CalendarIcon showIcon icon={<IconParkSolidApplication />} />,
18+
);
19+
expect(
20+
wrapper.find('[data-testid="icon-park-solid-application"]'),
21+
).toHaveLength(1);
22+
});
23+
24+
it("renders a FontAwesome icon when provided", () => {
25+
const wrapper = mount(<CalendarIcon showIcon icon="fa-example-icon" />);
26+
expect(wrapper.find("i.fa-example-icon")).toHaveLength(1);
27+
});
28+
29+
it("does not render an icon when none is provided", () => {
30+
const wrapper = mount(<CalendarIcon showIcon />);
31+
expect(wrapper.find("svg.react-datepicker__calendar-icon")).toHaveLength(1);
32+
});
33+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from "react";
2+
3+
export function IconParkSolidApplication(props) {
4+
return (
5+
<svg
6+
data-testid="icon-park-solid-application"
7+
xmlns="http://www.w3.org/2000/svg"
8+
width="1em"
9+
height="1em"
10+
viewBox="0 0 48 48"
11+
{...props}
12+
>
13+
<mask id="ipSApplication0">
14+
<g fill="none" stroke="#fff" strokeLinejoin="round" strokeWidth="4">
15+
<path strokeLinecap="round" d="M40.04 22v20h-32V22" />
16+
<path
17+
fill="#fff"
18+
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"
19+
/>
20+
</g>
21+
</mask>
22+
<path
23+
fill="currentColor"
24+
d="M0 0h48v48H0z"
25+
mask="url(#ipSApplication0)"
26+
/>
27+
</svg>
28+
);
29+
}

0 commit comments

Comments
 (0)