Skip to content

Commit

Permalink
🐛 Fix navigation issue with disabled dates in date picker
Browse files Browse the repository at this point in the history
- Resolved the issue where navigating to a month with a previously selected date that is now disabled prevents using arrow keys (Tab) to navigate
- Fixed by pre-selecting the first enabled date in the same month.

Closes Hacker0x01#5147
  • Loading branch information
Balaji Sridharan committed Oct 9, 2024
1 parent 76fea24 commit 9fa76c2
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 7 deletions.
44 changes: 37 additions & 7 deletions src/calendar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { clsx } from "clsx";
import { differenceInDays } from "date-fns";
import React, { Component, createRef } from "react";

import CalendarContainer from "./calendar_container";
Expand Down Expand Up @@ -40,6 +41,9 @@ import {
DEFAULT_YEAR_ITEM_NUMBER,
getMonthInLocale,
type Locale,
getStartOfMonth,
getEndOfMonth,
isDayDisabled,
} from "./date_utils";
import InputTime from "./input_time";
import Month from "./month";
Expand Down Expand Up @@ -379,14 +383,42 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
this.props.setPreSelection && this.props.setPreSelection(date);
};

getEnabledPreSelectionDateForMonth = (date: Date) => {
if (!isDayDisabled(date, this.props)) {
return date;
}

const startOfMonth = getStartOfMonth(date);
const endOfMonth = getEndOfMonth(date);

const totalDays = differenceInDays(endOfMonth, startOfMonth);

let preSelectedDate = null;

for (let dayIdx = 0; dayIdx <= totalDays; dayIdx++) {
const processingDate = addDays(startOfMonth, dayIdx);

if (!isDayDisabled(processingDate, this.props)) {
preSelectedDate = processingDate;
break;
}
}

return preSelectedDate;
};

handleMonthChange = (date: Date): void => {
this.handleCustomMonthChange(date);
const enabledPreSelectionDate =
this.getEnabledPreSelectionDateForMonth(date) ?? date;

this.handleCustomMonthChange(enabledPreSelectionDate);
if (this.props.adjustDateOnChange) {
this.props.onSelect(date);
this.props.onSelect(enabledPreSelectionDate);
this.props.setOpen?.(true);
}

this.props.setPreSelection && this.props.setPreSelection(date);
this.props.setPreSelection &&
this.props.setPreSelection(enabledPreSelectionDate);
};

handleCustomMonthChange = (date: Date): void => {
Expand Down Expand Up @@ -478,8 +510,7 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
date: subYears(
date,
this.props.showYearPicker
? (this.props.yearItemNumber ??
Calendar.defaultProps.yearItemNumber)
? this.props.yearItemNumber ?? Calendar.defaultProps.yearItemNumber
: 1,
),
}),
Expand Down Expand Up @@ -593,8 +624,7 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
date: addYears(
date,
this.props.showYearPicker
? (this.props.yearItemNumber ??
Calendar.defaultProps.yearItemNumber)
? this.props.yearItemNumber ?? Calendar.defaultProps.yearItemNumber
: 1,
),
}),
Expand Down
109 changes: 109 additions & 0 deletions src/test/calendar_test.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

import { render, fireEvent, act, waitFor } from "@testing-library/react";
import { setDate, startOfMonth, eachDayOfInterval, endOfMonth } from "date-fns";
import { endOfYear } from "date-fns/endOfYear";
import { isSunday } from "date-fns/isSunday";
import { eo } from "date-fns/locale/eo";
Expand All @@ -28,6 +29,8 @@ import {
isSameDay,
subMonths,
subYears,
addDays,
getDate,
} from "../date_utils";
import DatePicker from "../index";

Expand Down Expand Up @@ -2257,6 +2260,112 @@ describe("Calendar", () => {
currentMonth === 0 ? 11 : currentMonth - 1,
);
});

describe("pre-selection & disabled dates", () => {
it("should update the pre-selected dates to the first enabled day in a month when the next month is selected", () => {
const selected = new Date("2024-06-01");
const excludeDate = addMonths(selected, 1);

const { container } = render(
<DatePicker selected={selected} excludeDates={[excludeDate]} />,
);
const input = safeQuerySelector<HTMLInputElement>(container, "input");
fireEvent.focus(input);

const nextButton = safeQuerySelector<HTMLButtonElement>(
container,
".react-datepicker__navigation--next",
);
fireEvent.click(nextButton);

const preSelectedNewDate = safeQuerySelector(
container,
".react-datepicker__day--keyboard-selected",
).textContent;
const expectedPreSelectedNewDate = addDays(excludeDate, 1);

expect(Number(preSelectedNewDate)).toBe(
getDate(expectedPreSelectedNewDate),
);
});

it("should update the pre-selected dates to the first enabled day in a month when the previous month is selected", () => {
const selected = new Date("2024-06-08");
const excludeDate = addMonths(selected, 1);

const { container } = render(
<DatePicker selected={selected} excludeDates={[excludeDate]} />,
);
const input = safeQuerySelector<HTMLInputElement>(container, "input");
fireEvent.focus(input);

const nextButton = safeQuerySelector<HTMLButtonElement>(
container,
".react-datepicker__navigation--next",
);
fireEvent.click(nextButton);

const preSelectedNewDate = safeQuerySelector(
container,
".react-datepicker__day--keyboard-selected",
).textContent;
const expectedPreSelectedNewDate = setDate(excludeDate, 1);

expect(Number(preSelectedNewDate)).toBe(
getDate(expectedPreSelectedNewDate),
);
});

it("shouldn't set pre-select any date if all dates of a next month is disabled", () => {
const selected = new Date("2024-06-08");
const nextMonth = addMonths(selected, 1);
const excludeDates = eachDayOfInterval({
start: startOfMonth(nextMonth),
end: endOfMonth(nextMonth),
});

const { container } = render(
<DatePicker selected={selected} excludeDates={excludeDates} />,
);
const input = safeQuerySelector<HTMLInputElement>(container, "input");
fireEvent.focus(input);

const nextButton = safeQuerySelector<HTMLButtonElement>(
container,
".react-datepicker__navigation--next",
);
fireEvent.click(nextButton);

expect(
container.querySelector(".react-datepicker__day--keyboard-selected"),
).toBeNull();
});

it("shouldn't set pre-select any date if all dates of a last month is disabled", () => {
const selected = new Date("2024-06-08");
const lastMonth = subMonths(selected, 1);
const excludeDates = eachDayOfInterval({
start: startOfMonth(lastMonth),
end: endOfMonth(lastMonth),
});

const { container } = render(
<DatePicker selected={selected} excludeDates={excludeDates} />,
);
const input = safeQuerySelector<HTMLInputElement>(container, "input");
fireEvent.focus(input);

const nextButton = safeQuerySelector<HTMLButtonElement>(
container,
".react-datepicker__navigation--previous",
);
fireEvent.click(nextButton);

expect(
container.querySelector(".react-datepicker__day--keyboard-selected"),
).toBeNull();
});
});
});

describe("showTimeSelect", () => {
Expand Down

0 comments on commit 9fa76c2

Please sign in to comment.