Skip to content
Open
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
3 changes: 2 additions & 1 deletion src/calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ type CalendarProps = React.PropsWithChildren<
YearDropdownProps,
"date" | "onChange" | "year" | "minDate" | "maxDate"
> &
Omit<MonthDropdownProps, "month" | "onChange"> &
Omit<MonthDropdownProps, "month" | "onChange" | "date"> &
Omit<MonthYearDropdownProps, "date" | "onChange" | "minDate" | "maxDate"> &
Omit<
YearProps,
Expand Down Expand Up @@ -875,6 +875,7 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
{...Calendar.defaultProps}
{...this.props}
month={getMonth(this.state.date)}
date={this.state.date}
onChange={this.changeMonth}
/>
);
Expand Down
13 changes: 11 additions & 2 deletions src/month_dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import React, { Component } from "react";
import {
getMonthShortInLocale,
getMonthInLocale,
isMonthDisabled,
setMonth,
type Locale,
} from "./date_utils";
import MonthDropdownOptions from "./month_dropdown_options";
Expand Down Expand Up @@ -36,7 +38,11 @@ export default class MonthDropdown extends Component<
renderSelectOptions = (monthNames: string[]): React.ReactElement[] =>
monthNames.map<React.ReactElement>(
(m: string, i: number): React.ReactElement => (
<option key={m} value={i}>
<option
key={m}
value={i}
disabled={isMonthDisabled(setMonth(this.props.date, i), this.props)}
>
{m}
</option>
),
Expand Down Expand Up @@ -91,7 +97,10 @@ export default class MonthDropdown extends Component<

onChange = (month: number): void => {
this.toggleDropdown();
if (month !== this.props.month) {
if (
month !== this.props.month &&
!isMonthDisabled(setMonth(this.props.date, month), this.props)
) {
this.props.onChange(month);
}
};
Expand Down
84 changes: 54 additions & 30 deletions src/month_dropdown_options.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
import { clsx } from "clsx";
import React, { Component } from "react";

import { ClickOutsideWrapper } from "./click_outside_wrapper";
import {
isMonthDisabled,
setMonth,
type DateFilterOptions,
} from "./date_utils";

interface MonthDropdownOptionsProps {
interface MonthDropdownOptionsProps extends Pick<
DateFilterOptions,
"minDate" | "maxDate" | "excludeDates" | "includeDates" | "filterDate"
> {
onCancel: VoidFunction;
onChange: (month: number) => void;
month: number;
monthNames: string[];
date: Date;
}

export default class MonthDropdownOptions extends Component<MonthDropdownOptionsProps> {
monthOptionButtonsRef: Record<number, HTMLDivElement | null> = {};

isSelectedMonth = (i: number): boolean => this.props.month === i;

isDisabledMonth = (i: number): boolean =>
isMonthDisabled(setMonth(this.props.date, i), this.props);

handleOptionKeyDown = (i: number, e: React.KeyboardEvent): void => {
switch (e.key) {
case "Enter":
Expand Down Expand Up @@ -41,38 +54,49 @@ export default class MonthDropdownOptions extends Component<MonthDropdownOptions
this.monthOptionButtonsRef = {};

return this.props.monthNames.map<React.ReactElement>(
(month: string, i: number): React.ReactElement => (
<div
ref={(el) => {
this.monthOptionButtonsRef[i] = el;
if (this.isSelectedMonth(i)) {
el?.focus();
}
}}
role="button"
tabIndex={0}
className={
this.isSelectedMonth(i)
? "react-datepicker__month-option react-datepicker__month-option--selected_month"
: "react-datepicker__month-option"
}
key={month}
onClick={this.onChange.bind(this, i)}
onKeyDown={this.handleOptionKeyDown.bind(this, i)}
aria-selected={this.isSelectedMonth(i) ? "true" : undefined}
>
{this.isSelectedMonth(i) ? (
<span className="react-datepicker__month-option--selected">✓</span>
) : (
""
)}
{month}
</div>
),
(month: string, i: number): React.ReactElement => {
const isDisabled = this.isDisabledMonth(i);
return (
<div
ref={(el) => {
this.monthOptionButtonsRef[i] = el;
if (this.isSelectedMonth(i)) {
el?.focus();
}
}}
role="button"
tabIndex={0}
className={clsx("react-datepicker__month-option", {
"react-datepicker__month-option--selected_month":
this.isSelectedMonth(i),
"react-datepicker__month-option--disabled": isDisabled,
})}
key={month}
onClick={this.onChange.bind(this, i)}
onKeyDown={this.handleOptionKeyDown.bind(this, i)}
aria-selected={this.isSelectedMonth(i) ? "true" : undefined}
aria-disabled={isDisabled ? "true" : undefined}
>
{this.isSelectedMonth(i) ? (
<span className="react-datepicker__month-option--selected">
</span>
) : (
""
)}
{month}
</div>
);
},
);
};

onChange = (month: number): void => this.props.onChange(month);
onChange = (month: number): void => {
if (this.isDisabledMonth(month)) {
return;
}
this.props.onChange(month);
};

handleClickOutside = (): void => this.props.onCancel();

Expand Down
10 changes: 10 additions & 0 deletions src/stylesheets/datepicker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,16 @@ h2.react-datepicker__current-month {
}
}

.react-datepicker__month-option--disabled {
cursor: default;
color: $datepicker__muted-color;

&:hover {
cursor: default;
background-color: transparent;
}
}

.react-datepicker__close-icon {
cursor: pointer;
background-color: transparent;
Expand Down
85 changes: 83 additions & 2 deletions src/test/month_dropdown_test.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ describe("MonthDropdown", () => {

function getMonthDropdown(
overrideProps?: Partial<
Pick<MonthDropdownProps, "dropdownMode" | "month" | "onChange">
Pick<MonthDropdownProps, "dropdownMode" | "month" | "onChange" | "date">
> &
Omit<MonthDropdownProps, "dropdownMode" | "month" | "onChange">,
Omit<MonthDropdownProps, "dropdownMode" | "month" | "onChange" | "date">,
) {
return render(
<MonthDropdown
dropdownMode="scroll"
month={11}
date={new Date(2025, 11, 1)}
onChange={mockHandleChange}
{...overrideProps}
/>,
Expand Down Expand Up @@ -138,6 +139,7 @@ describe("MonthDropdown", () => {
onChange={onCancelSpy}
month={11}
monthNames={monthNames}
date={new Date(2025, 11, 1)}
/>,
);
fireEvent.mouseDown(document.body);
Expand Down Expand Up @@ -291,6 +293,58 @@ describe("MonthDropdown", () => {
const firstMonthOption = monthOptions[0];
expect(document.activeElement).toEqual(firstMonthOption);
});

it("does not call onChange when clicking a month outside of minDate/maxDate range", () => {
monthDropdown = getMonthDropdown({
month: 3,
date: new Date(2025, 3, 1),
minDate: new Date(2025, 0, 1),
maxDate: new Date(2025, 5, 30),
});
const monthReadView = safeQuerySelector(
monthDropdown,
".react-datepicker__month-read-view",
);
fireEvent.click(monthReadView);

const monthOptions = safeQuerySelectorAll(
monthDropdown,
".react-datepicker__month-option",
);

// August (index 7) is outside the Jan-Jun 2025 range
const disabledMonthOption = monthOptions[7]!;
expect(disabledMonthOption.getAttribute("aria-disabled")).toEqual("true");

fireEvent.click(disabledMonthOption);
expect(handleChangeResult).toBeNull();
});

it("calls onChange when clicking a month inside of minDate/maxDate range", () => {
monthDropdown = getMonthDropdown({
month: 3,
date: new Date(2025, 3, 1),
minDate: new Date(2025, 0, 1),
maxDate: new Date(2025, 5, 30),
});
const monthReadView = safeQuerySelector(
monthDropdown,
".react-datepicker__month-read-view",
);
fireEvent.click(monthReadView);

const monthOptions = safeQuerySelectorAll(
monthDropdown,
".react-datepicker__month-option",
);

// May (index 4) is inside the Jan-Jun 2025 range
const enabledMonthOption = monthOptions[4]!;
expect(enabledMonthOption.getAttribute("aria-disabled")).toBeNull();

fireEvent.click(enabledMonthOption);
expect(handleChangeResult).toEqual(4);
});
});

describe("select mode", () => {
Expand Down Expand Up @@ -381,5 +435,32 @@ describe("MonthDropdown", () => {
});
expect(handleChangeResult).toEqual(9);
});

it("disables options for months outside of minDate/maxDate range", () => {
monthDropdown = getMonthDropdown({
dropdownMode: "select",
month: 3,
date: new Date(2025, 3, 1),
minDate: new Date(2025, 0, 1),
maxDate: new Date(2025, 5, 30),
});
const options = Array.from(
monthDropdown.querySelectorAll<HTMLOptionElement>("option"),
);
expect(options.map((o) => o.disabled)).toEqual([
false, // Jan
false, // Feb
false, // Mar
false, // Apr
false, // May
false, // Jun
true, // Jul
true, // Aug
true, // Sep
true, // Oct
true, // Nov
true, // Dec
]);
});
});
});
Loading