Skip to content

Commit e1ff9e1

Browse files
committed
🐛 fix: Disable out-of-range months in month dropdown instead of ignoring minDate/maxDate
showMonthDropdown rendered all 12 months unconditionally, letting users navigate outside minDate/maxDate even though showMonthYearDropdown already respected the range. Reuse isMonthDisabled (same helper used by the full month-grid picker) to gray out out-of-range months in both scroll and select modes, rather than filtering them out, since the displayed year is controlled externally and filtering would make the option list's length/positions shift depending on which year is in view. Fixes #6286
1 parent 548a1f3 commit e1ff9e1

5 files changed

Lines changed: 160 additions & 35 deletions

File tree

src/calendar.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ type CalendarProps = React.PropsWithChildren<
127127
YearDropdownProps,
128128
"date" | "onChange" | "year" | "minDate" | "maxDate"
129129
> &
130-
Omit<MonthDropdownProps, "month" | "onChange"> &
130+
Omit<MonthDropdownProps, "month" | "onChange" | "date"> &
131131
Omit<MonthYearDropdownProps, "date" | "onChange" | "minDate" | "maxDate"> &
132132
Omit<
133133
YearProps,
@@ -875,6 +875,7 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
875875
{...Calendar.defaultProps}
876876
{...this.props}
877877
month={getMonth(this.state.date)}
878+
date={this.state.date}
878879
onChange={this.changeMonth}
879880
/>
880881
);

src/month_dropdown.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import React, { Component } from "react";
33
import {
44
getMonthShortInLocale,
55
getMonthInLocale,
6+
isMonthDisabled,
7+
setMonth,
68
type Locale,
79
} from "./date_utils";
810
import MonthDropdownOptions from "./month_dropdown_options";
@@ -36,7 +38,11 @@ export default class MonthDropdown extends Component<
3638
renderSelectOptions = (monthNames: string[]): React.ReactElement[] =>
3739
monthNames.map<React.ReactElement>(
3840
(m: string, i: number): React.ReactElement => (
39-
<option key={m} value={i}>
41+
<option
42+
key={m}
43+
value={i}
44+
disabled={isMonthDisabled(setMonth(this.props.date, i), this.props)}
45+
>
4046
{m}
4147
</option>
4248
),
@@ -91,7 +97,10 @@ export default class MonthDropdown extends Component<
9197

9298
onChange = (month: number): void => {
9399
this.toggleDropdown();
94-
if (month !== this.props.month) {
100+
if (
101+
month !== this.props.month &&
102+
!isMonthDisabled(setMonth(this.props.date, month), this.props)
103+
) {
95104
this.props.onChange(month);
96105
}
97106
};

src/month_dropdown_options.tsx

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,32 @@
1+
import { clsx } from "clsx";
12
import React, { Component } from "react";
23

34
import { ClickOutsideWrapper } from "./click_outside_wrapper";
5+
import {
6+
isMonthDisabled,
7+
setMonth,
8+
type DateFilterOptions,
9+
} from "./date_utils";
410

5-
interface MonthDropdownOptionsProps {
11+
interface MonthDropdownOptionsProps extends Pick<
12+
DateFilterOptions,
13+
"minDate" | "maxDate" | "excludeDates" | "includeDates" | "filterDate"
14+
> {
615
onCancel: VoidFunction;
716
onChange: (month: number) => void;
817
month: number;
918
monthNames: string[];
19+
date: Date;
1020
}
1121

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

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

27+
isDisabledMonth = (i: number): boolean =>
28+
isMonthDisabled(setMonth(this.props.date, i), this.props);
29+
1730
handleOptionKeyDown = (i: number, e: React.KeyboardEvent): void => {
1831
switch (e.key) {
1932
case "Enter":
@@ -41,38 +54,49 @@ export default class MonthDropdownOptions extends Component<MonthDropdownOptions
4154
this.monthOptionButtonsRef = {};
4255

4356
return this.props.monthNames.map<React.ReactElement>(
44-
(month: string, i: number): React.ReactElement => (
45-
<div
46-
ref={(el) => {
47-
this.monthOptionButtonsRef[i] = el;
48-
if (this.isSelectedMonth(i)) {
49-
el?.focus();
50-
}
51-
}}
52-
role="button"
53-
tabIndex={0}
54-
className={
55-
this.isSelectedMonth(i)
56-
? "react-datepicker__month-option react-datepicker__month-option--selected_month"
57-
: "react-datepicker__month-option"
58-
}
59-
key={month}
60-
onClick={this.onChange.bind(this, i)}
61-
onKeyDown={this.handleOptionKeyDown.bind(this, i)}
62-
aria-selected={this.isSelectedMonth(i) ? "true" : undefined}
63-
>
64-
{this.isSelectedMonth(i) ? (
65-
<span className="react-datepicker__month-option--selected"></span>
66-
) : (
67-
""
68-
)}
69-
{month}
70-
</div>
71-
),
57+
(month: string, i: number): React.ReactElement => {
58+
const isDisabled = this.isDisabledMonth(i);
59+
return (
60+
<div
61+
ref={(el) => {
62+
this.monthOptionButtonsRef[i] = el;
63+
if (this.isSelectedMonth(i)) {
64+
el?.focus();
65+
}
66+
}}
67+
role="button"
68+
tabIndex={0}
69+
className={clsx("react-datepicker__month-option", {
70+
"react-datepicker__month-option--selected_month":
71+
this.isSelectedMonth(i),
72+
"react-datepicker__month-option--disabled": isDisabled,
73+
})}
74+
key={month}
75+
onClick={this.onChange.bind(this, i)}
76+
onKeyDown={this.handleOptionKeyDown.bind(this, i)}
77+
aria-selected={this.isSelectedMonth(i) ? "true" : undefined}
78+
aria-disabled={isDisabled ? "true" : undefined}
79+
>
80+
{this.isSelectedMonth(i) ? (
81+
<span className="react-datepicker__month-option--selected">
82+
83+
</span>
84+
) : (
85+
""
86+
)}
87+
{month}
88+
</div>
89+
);
90+
},
7291
);
7392
};
7493

75-
onChange = (month: number): void => this.props.onChange(month);
94+
onChange = (month: number): void => {
95+
if (this.isDisabledMonth(month)) {
96+
return;
97+
}
98+
this.props.onChange(month);
99+
};
76100

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

src/stylesheets/datepicker.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,16 @@ h2.react-datepicker__current-month {
723723
}
724724
}
725725

726+
.react-datepicker__month-option--disabled {
727+
cursor: default;
728+
color: $datepicker__muted-color;
729+
730+
&:hover {
731+
cursor: default;
732+
background-color: transparent;
733+
}
734+
}
735+
726736
.react-datepicker__close-icon {
727737
cursor: pointer;
728738
background-color: transparent;

src/test/month_dropdown_test.test.tsx

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ describe("MonthDropdown", () => {
2121

2222
function getMonthDropdown(
2323
overrideProps?: Partial<
24-
Pick<MonthDropdownProps, "dropdownMode" | "month" | "onChange">
24+
Pick<MonthDropdownProps, "dropdownMode" | "month" | "onChange" | "date">
2525
> &
26-
Omit<MonthDropdownProps, "dropdownMode" | "month" | "onChange">,
26+
Omit<MonthDropdownProps, "dropdownMode" | "month" | "onChange" | "date">,
2727
) {
2828
return render(
2929
<MonthDropdown
3030
dropdownMode="scroll"
3131
month={11}
32+
date={new Date(2025, 11, 1)}
3233
onChange={mockHandleChange}
3334
{...overrideProps}
3435
/>,
@@ -138,6 +139,7 @@ describe("MonthDropdown", () => {
138139
onChange={onCancelSpy}
139140
month={11}
140141
monthNames={monthNames}
142+
date={new Date(2025, 11, 1)}
141143
/>,
142144
);
143145
fireEvent.mouseDown(document.body);
@@ -291,6 +293,58 @@ describe("MonthDropdown", () => {
291293
const firstMonthOption = monthOptions[0];
292294
expect(document.activeElement).toEqual(firstMonthOption);
293295
});
296+
297+
it("does not call onChange when clicking a month outside of minDate/maxDate range", () => {
298+
monthDropdown = getMonthDropdown({
299+
month: 3,
300+
date: new Date(2025, 3, 1),
301+
minDate: new Date(2025, 0, 1),
302+
maxDate: new Date(2025, 5, 30),
303+
});
304+
const monthReadView = safeQuerySelector(
305+
monthDropdown,
306+
".react-datepicker__month-read-view",
307+
);
308+
fireEvent.click(monthReadView);
309+
310+
const monthOptions = safeQuerySelectorAll(
311+
monthDropdown,
312+
".react-datepicker__month-option",
313+
);
314+
315+
// August (index 7) is outside the Jan-Jun 2025 range
316+
const disabledMonthOption = monthOptions[7]!;
317+
expect(disabledMonthOption.getAttribute("aria-disabled")).toEqual("true");
318+
319+
fireEvent.click(disabledMonthOption);
320+
expect(handleChangeResult).toBeNull();
321+
});
322+
323+
it("calls onChange when clicking a month inside of minDate/maxDate range", () => {
324+
monthDropdown = getMonthDropdown({
325+
month: 3,
326+
date: new Date(2025, 3, 1),
327+
minDate: new Date(2025, 0, 1),
328+
maxDate: new Date(2025, 5, 30),
329+
});
330+
const monthReadView = safeQuerySelector(
331+
monthDropdown,
332+
".react-datepicker__month-read-view",
333+
);
334+
fireEvent.click(monthReadView);
335+
336+
const monthOptions = safeQuerySelectorAll(
337+
monthDropdown,
338+
".react-datepicker__month-option",
339+
);
340+
341+
// May (index 4) is inside the Jan-Jun 2025 range
342+
const enabledMonthOption = monthOptions[4]!;
343+
expect(enabledMonthOption.getAttribute("aria-disabled")).toBeNull();
344+
345+
fireEvent.click(enabledMonthOption);
346+
expect(handleChangeResult).toEqual(4);
347+
});
294348
});
295349

296350
describe("select mode", () => {
@@ -381,5 +435,32 @@ describe("MonthDropdown", () => {
381435
});
382436
expect(handleChangeResult).toEqual(9);
383437
});
438+
439+
it("disables options for months outside of minDate/maxDate range", () => {
440+
monthDropdown = getMonthDropdown({
441+
dropdownMode: "select",
442+
month: 3,
443+
date: new Date(2025, 3, 1),
444+
minDate: new Date(2025, 0, 1),
445+
maxDate: new Date(2025, 5, 30),
446+
});
447+
const options = Array.from(
448+
monthDropdown.querySelectorAll<HTMLOptionElement>("option"),
449+
);
450+
expect(options.map((o) => o.disabled)).toEqual([
451+
false, // Jan
452+
false, // Feb
453+
false, // Mar
454+
false, // Apr
455+
false, // May
456+
false, // Jun
457+
true, // Jul
458+
true, // Aug
459+
true, // Sep
460+
true, // Oct
461+
true, // Nov
462+
true, // Dec
463+
]);
464+
});
384465
});
385466
});

0 commit comments

Comments
 (0)