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

Marks holidays in the calendar #4203

Merged
merged 23 commits into from
Aug 24, 2023
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 @@ -31,6 +31,7 @@ import ExcludeDates from "../../examples/excludeDates";
import ExcludeDateIntervals from "../../examples/excludeDateIntervals";
import ExcludeDatesMonthPicker from "../../examples/excludeDatesMonthPicker";
import HighlightDates from "../../examples/highlightDates";
import HolidayDates from "../../examples/holidayDates";
import HighlightDatesRanges from "../../examples/highlightDatesRanges";
import IncludeDates from "../../examples/includeDates";
import IncludeDateIntervals from "../../examples/includeDateIntervals";
Expand Down Expand Up @@ -289,6 +290,10 @@ export default class exampleComponents extends React.Component {
title: "Highlight dates with custom class names and ranges",
component: HighlightDatesRanges,
},
{
title: "Holiday dates",
component: HolidayDates,
},
{
title: "Include dates",
component: IncludeDates,
Expand Down
18 changes: 18 additions & 0 deletions docs-site/src/examples/holidayDates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
() => {
const [startDate, setStartDate] = useState(new Date());
return (
<DatePicker
selected={startDate}
onChange={(date) => setStartDate(date)}
holidays={[
{ date: "2023-08-15", holidayName: "India's Independence Day" },
{ date: "2023-12-31", holidayName: "New Year's Eve" },
{ date: "2023-12-25", holidayName: "Christmas" },
{ date: "2024-01-01", holidayName: "New Year's Day" },
{ date: "2023-11-23", holidayName: "Thanksgiving Day" },
{ date: "2023-12-25", holidayName: "Fake holiday" },
]}
placeholderText="This display holidays"
/>
);
};
197 changes: 99 additions & 98 deletions docs/datepicker.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
| `formatWeekDay` | `func` | | |
| `formatWeekNumber` | `func` | | |
| `highlightDates` | `array` | | |
| `holidays` | `array` | | |
| `id` | `string` | | |
| `includeDateIntervals` | `array` | | |
| `includeDates` | `array` | | |
Expand Down
2 changes: 2 additions & 0 deletions src/calendar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export default class Calendar extends React.Component {
fixedHeight: PropTypes.bool,
formatWeekNumber: PropTypes.func,
highlightDates: PropTypes.instanceOf(Map),
holidays: PropTypes.instanceOf(Map),
includeDates: PropTypes.array,
includeDateIntervals: PropTypes.arrayOf(
PropTypes.shape({
Expand Down Expand Up @@ -904,6 +905,7 @@ export default class Calendar extends React.Component {
excludeDates={this.props.excludeDates}
excludeDateIntervals={this.props.excludeDateIntervals}
highlightDates={this.props.highlightDates}
holidays={this.props.holidays}
selectingDate={this.state.selectingDate}
includeDates={this.props.includeDates}
includeDateIntervals={this.props.includeDateIntervals}
Expand Down
50 changes: 50 additions & 0 deletions src/date_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,57 @@ export function getHightLightDaysMap(
}
}
}
return dateClasses;
}

/**
* Compare the two arrays
* @param {Array} array1
* @param {Array} array2
* @returns {Boolean} true, if the passed array are equal, false otherwise
*/
export function arraysAreEqual(array1, array2) {
if (array1.length !== array2.length) {
return false;
}

return array1.every((value, index) => value === array2[index]);
}

/**
* Assign the custom class to each date
* @param {Array} holidayDates array of object containing date and name of the holiday
* @param {string} classname to be added.
* @returns {Map} Map containing date as key and array of classname and holiday name as value
*/
export function getHolidaysMap(
holidayDates = [],
defaultClassName = "react-datepicker__day--holidays"
) {
const dateClasses = new Map();
holidayDates.forEach((holiday) => {
const { date: dateObj, holidayName } = holiday;
if (!isDate(dateObj)) {
return;
}

const key = formatDate(dateObj, "MM.dd.yyyy");
const classNamesObj = dateClasses.get(key) || {};
if (
"className" in classNamesObj &&
classNamesObj["className"] === defaultClassName &&
arraysAreEqual(classNamesObj["holidayNames"], [holidayName])
) {
return;
}

classNamesObj["className"] = defaultClassName;
const holidayNameArr = classNamesObj["holidayNames"];
classNamesObj["holidayNames"] = holidayNameArr
? [...holidayNameArr, holidayName]
: [holidayName];
dateClasses.set(key, classNamesObj);
});
return dateClasses;
}

Expand Down
33 changes: 32 additions & 1 deletion src/day.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default class Day extends React.Component {
dayClassName: PropTypes.func,
endDate: PropTypes.instanceOf(Date),
highlightDates: PropTypes.instanceOf(Map),
holidays: PropTypes.instanceOf(Map),
inline: PropTypes.bool,
shouldFocusDayInline: PropTypes.bool,
month: PropTypes.number,
Expand Down Expand Up @@ -108,6 +109,19 @@ export default class Day extends React.Component {
return highlightDates.get(dayStr);
};

// Function to return the array containing classname associated to the date
getHolidaysClass = () => {
const { day, holidays } = this.props;
if (!holidays) {
return false;
}
const dayStr = formatDate(day, "MM.dd.yyyy");
// Looking for className in the Map of {day string: {className, holidayName}}
if (holidays.has(dayStr)) {
return [holidays.get(dayStr).className];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: This appears to return an array of size 1, that includes the className. I just want to confirm that this is intentional, as the comment above the function is not explicit about returning an array.

🔹 Bug (Nice to have)

Image of Luciano C Luciano C

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes this is intentional. I have updated the comment.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Thanks for updating the comment 👍

Image of Luciano C Luciano C

}
};

isInRange = () => {
const { day, startDate, endDate } = this.props;
if (!startDate || !endDate) {
Expand Down Expand Up @@ -260,7 +274,8 @@ export default class Day extends React.Component {
"react-datepicker__day--outside-month":
this.isAfterMonth() || this.isBeforeMonth(),
},
this.getHighLightedClass("react-datepicker__day--highlighted")
this.getHighLightedClass("react-datepicker__day--highlighted"),
this.getHolidaysClass()
);
};

Expand All @@ -279,6 +294,18 @@ export default class Day extends React.Component {
return `${prefix} ${formatDate(day, "PPPP", this.props.locale)}`;
};

// A function to return the holiday's name as title's content
getTitle = () => {
const { day, holidays = new Map() } = this.props;
const compareDt = formatDate(day, "MM.dd.yyyy");
if (holidays.has(compareDt)) {
return holidays.get(compareDt).holidayNames.length > 0
? holidays.get(compareDt).holidayNames.join(", ")
: "";
}
return "";
};

getTabIndex = (selected, preSelection) => {
const selectedDay = selected || this.props.selected;
const preSelectionDay = preSelection || this.props.preSelection;
Expand Down Expand Up @@ -355,11 +382,15 @@ export default class Day extends React.Component {
tabIndex={this.getTabIndex()}
aria-label={this.getAriaLabel()}
role="option"
title={this.getTitle()}
aria-disabled={this.isDisabled()}
aria-current={this.isCurrentDay() ? "date" : undefined}
aria-selected={this.isSelected() || this.isInRange()}
>
{this.renderDayContents()}
{this.getTitle() !== "" && (
<span className="holiday-overlay">{this.getTitle()}</span>
)}
</div>
);
}
18 changes: 18 additions & 0 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import classnames from "classnames";
import set from "date-fns/set";
import startOfDay from "date-fns/startOfDay";
import endOfDay from "date-fns/endOfDay";
import isValid from "date-fns/isValid";
import {
newDate,
isDate,
Expand Down Expand Up @@ -42,6 +43,7 @@ import {
isSameDay,
isMonthDisabled,
isYearDisabled,
getHolidaysMap,
} from "./date_utils";
import TabLoop from "./tab_loop";
import onClickOutside from "react-onclickoutside";
Expand Down Expand Up @@ -170,6 +172,7 @@ export default class DatePicker extends React.Component {
form: PropTypes.string,
formatWeekNumber: PropTypes.func,
highlightDates: PropTypes.array,
holidays: PropTypes.array,
id: PropTypes.string,
includeDates: PropTypes.array,
includeDateIntervals: PropTypes.array,
Expand Down Expand Up @@ -356,6 +359,19 @@ export default class DatePicker extends React.Component {
: newDate();

calcInitialState = () => {
// Convert the date from string format to standard Date format
const modifiedHolidays = this.props.holidays?.reduce(
(accumulator, holiday) => {
const date = new Date(holiday.date);
if (!isValid(date)) {
return accumulator;
}

return [...accumulator, { ...holiday, date }];
},
[]
);

const defaultPreSelection = this.getPreSelection();
const minDate = getEffectiveMinDate(this.props);
const maxDate = getEffectiveMaxDate(this.props);
Expand All @@ -375,6 +391,7 @@ export default class DatePicker extends React.Component {
// transforming highlighted days (perhaps nested array)
// to flat Map for faster access in day.jsx
highlightDates: getHightLightDaysMap(this.props.highlightDates),
holidays: getHolidaysMap(modifiedHolidays),
focused: false,
// used to focus day in inline version after month has changed, but not on
// initial render
Expand Down Expand Up @@ -954,6 +971,7 @@ export default class DatePicker extends React.Component {
onClickOutside={this.handleCalendarClickOutside}
formatWeekNumber={this.props.formatWeekNumber}
highlightDates={this.state.highlightDates}
holidays={this.state.holidays}
includeDates={this.props.includeDates}
includeDateIntervals={this.props.includeDateIntervals}
includeTimes={this.props.includeTimes}
Expand Down
2 changes: 2 additions & 0 deletions src/month.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export default class Month extends React.Component {
fixedHeight: PropTypes.bool,
formatWeekNumber: PropTypes.func,
highlightDates: PropTypes.instanceOf(Map),
holidays: PropTypes.PropTypes.instanceOf(Map),
includeDates: PropTypes.array,
includeDateIntervals: PropTypes.array,
inline: PropTypes.bool,
Expand Down Expand Up @@ -319,6 +320,7 @@ export default class Month extends React.Component {
inline={this.props.inline}
shouldFocusDayInline={this.props.shouldFocusDayInline}
highlightDates={this.props.highlightDates}
holidays={this.props.holidays}
selectingDate={this.props.selectingDate}
filterDate={this.props.filterDate}
preSelection={this.props.preSelection}
Expand Down
33 changes: 33 additions & 0 deletions src/stylesheets/datepicker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,39 @@
}
}

&--holidays {
position: relative;
border-radius: $datepicker__border-radius;
background-color: $datepicker__holidays-color;
color: #fff;

.holiday-overlay {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background-color: #333;
color: #fff;
padding: 4px;
border-radius: 4px;
white-space: nowrap;
visibility: hidden;
opacity: 0;
transition:
visibility 0s,
opacity 0.3s ease-in-out;
}

&:hover {
background-color: darken($datepicker__holidays-color, 10%);
}

&:hover .holiday-overlay {
visibility: visible;
opacity: 1;
}
}

&--selected,
&--in-selecting-range,
&--in-range {
Expand Down
1 change: 1 addition & 0 deletions src/stylesheets/variables.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
$datepicker__background-color: #f0f0f0 !default;
$datepicker__border-color: #aeaeae !default;
$datepicker__highlighted-color: #3dcc4a !default;
$datepicker__holidays-color: #ff6803 !default;
$datepicker__muted-color: #ccc !default;
$datepicker__selected-color: #216ba5 !default;
$datepicker__text-color: #000 !default;
Expand Down
4 changes: 3 additions & 1 deletion src/week.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default class Week extends React.Component {
filterDate: PropTypes.func,
formatWeekNumber: PropTypes.func,
highlightDates: PropTypes.instanceOf(Map),
holidays: PropTypes.instanceOf(Map),
includeDates: PropTypes.array,
includeDateIntervals: PropTypes.array,
inline: PropTypes.bool,
Expand Down Expand Up @@ -97,7 +98,7 @@ export default class Week extends React.Component {
const startOfWeek = utils.getStartOfWeek(
this.props.day,
this.props.locale,
this.props.calendarStartDay
this.props.calendarStartDay,
);
const days = [];
const weekNumber = this.formatWeekNumber(startOfWeek);
Expand Down Expand Up @@ -133,6 +134,7 @@ export default class Week extends React.Component {
includeDates={this.props.includeDates}
includeDateIntervals={this.props.includeDateIntervals}
highlightDates={this.props.highlightDates}
holidays={this.props.holidays}
selectingDate={this.props.selectingDate}
filterDate={this.props.filterDate}
preSelection={this.props.preSelection}
Expand Down
Loading
Loading