Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature #4091 - Make the Calendar Icon clickable #4417

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading