Skip to content

Commit

Permalink
Merge pull request #4417 from qburst/issue-4091/fix/clickable-calenda…
Browse files Browse the repository at this point in the history
…r-icon

Feature #4091 - Make the Calendar Icon clickable
  • Loading branch information
martijnrusschen authored Dec 19, 2023
2 parents 804b7ca + d4790be commit e4c1d20
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 18 deletions.
5 changes: 5 additions & 0 deletions docs-site/src/components/Examples/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions docs-site/src/examples/toggleCalendarOnIconClick.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
() => {
const [selectedDate, setSelectedDate] = useState(new Date());
return (
<DatePicker
showIcon
toggleCalendarOnIconClick
selected={selectedDate}
onChange={(date) => setSelectedDate(date)}
/>
);
};
14 changes: 13 additions & 1 deletion src/calendar_icon.jsx
Original file line number Diff line number Diff line change
@@ -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);
}
},
});
}

Expand All @@ -15,6 +24,7 @@ const CalendarIcon = ({ icon, className = "" }) => {
<i
className={`${defaultClass} ${icon} ${className}`}
aria-hidden="true"
onClick={onClick}
/>
);
}
Expand All @@ -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>
Expand All @@ -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;
51 changes: 35 additions & 16 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export default class DatePicker extends React.Component {
excludeScrollbar: true,
customTimeInput: null,
calendarStartDay: undefined,
toggleCalendarOnIconClick: false,
};
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 = () =>
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -1249,15 +1255,28 @@ 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${
showIcon ? " react-datepicker__view-calendar-icon" : ""
}`}
>
{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()}
Expand Down
56 changes: 56 additions & 0 deletions test/calendar_icon.test.js
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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 />} />,
Expand All @@ -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);
});
});
80 changes: 79 additions & 1 deletion test/datepicker_test.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 />,
Expand Down Expand Up @@ -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", () => {
Expand Down

0 comments on commit e4c1d20

Please sign in to comment.