From 5340beda7af205733b2fc3ae5ddd4ee6ab4907a2 Mon Sep 17 00:00:00 2001 From: Martijn Russchen Date: Wed, 26 Nov 2025 14:42:52 +0100 Subject: [PATCH 1/2] fix: reset monthSelectedIn when changeMonth is called in custom header Fixes #3829 When using monthsShown >= 2 with a custom header, calling changeMonth() would produce inconsistent results depending on which calendar panel the user last selected a date in. This was because the monthSelectedIn state (which tracks the offset for multi-month display) was not being reset when changeMonth was explicitly called. This fix: - Adds onMonthSelectedInChange callback prop to Calendar component - Resets monthSelectedIn to 0 when changeMonth is called, ensuring the target month always appears in the leftmost position - Adds test to verify the fix --- src/calendar.tsx | 8 ++++++- src/index.tsx | 5 ++++ src/test/calendar_test.test.tsx | 41 +++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/calendar.tsx b/src/calendar.tsx index 5c5d391359..3122ceb98a 100644 --- a/src/calendar.tsx +++ b/src/calendar.tsx @@ -180,6 +180,7 @@ type CalendarProps = React.PropsWithChildren< showPreviousMonths?: boolean; monthsShown?: number; monthSelectedIn?: number; + onMonthSelectedInChange?: (monthSelectedIn: number) => void; onSelect: ( day: Date, event?: @@ -455,7 +456,12 @@ export default class Calendar extends Component { ({ date }) => ({ date: setMonth(date, Number(month)), }), - () => this.handleMonthChange(this.state.date), + () => { + this.handleMonthChange(this.state.date); + // Reset monthSelectedIn to 0 so the target month appears in the leftmost position + // This ensures consistent behavior when using changeMonth in custom headers + this.props.onMonthSelectedInChange?.(0); + }, ); }; diff --git a/src/index.tsx b/src/index.tsx index 5ea00fd14c..212cf1ca1b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1461,6 +1461,10 @@ export class DatePicker extends Component { } }; + handleMonthSelectedInChange = (monthSelectedIn: number): void => { + this.setState({ monthSelectedIn }); + }; + renderCalendar = () => { if (!this.props.inline && !this.isCalendarOpen()) { return null; @@ -1492,6 +1496,7 @@ export class DatePicker extends Component { dropdownMode={ this.props.dropdownMode ?? DatePicker.defaultProps.dropdownMode } + onMonthSelectedInChange={this.handleMonthSelectedInChange} > {this.props.children} diff --git a/src/test/calendar_test.test.tsx b/src/test/calendar_test.test.tsx index 0af4670162..ba1c0782a6 100644 --- a/src/test/calendar_test.test.tsx +++ b/src/test/calendar_test.test.tsx @@ -984,6 +984,47 @@ describe("Calendar", () => { expect(header).toHaveLength(1); expect(time).toHaveLength(1); }); + + it("should display the target month in the leftmost position when changeMonth is called with monthsShown >= 2", () => { + // This test verifies the fix for issue #3829 + // When using changeMonth in a custom header with monthsShown >= 2, + // the target month should always appear in the leftmost position + // regardless of which calendar panel the user last selected a date in + const onMonthSelectedInChangeSpy = jest.fn(); + + const renderCustomHeaderWithMonthSelect = ({ + changeMonth, + }: { + changeMonth: (month: number) => void; + }) => ( +
+ +
+ ); + + const { calendar } = getCalendar({ + renderCustomHeader: renderCustomHeaderWithMonthSelect, + monthsShown: 2, + onMonthSelectedInChange: onMonthSelectedInChangeSpy, + }); + + // Select June (month index 5) from the month dropdown + const monthSelect = safeQuerySelector(calendar, ".month-select"); + fireEvent.change(monthSelect, { target: { value: 5 } }); + + // Verify that onMonthSelectedInChange was called with 0 + // This ensures the target month appears in the leftmost position + expect(onMonthSelectedInChangeSpy).toHaveBeenCalledWith(0); + }); }); describe("when showDisabledMonthNavigation is enabled", () => { From 63fcf4ef5b8573ace0b1ed79e3c01f4d52e695ef Mon Sep 17 00:00:00 2001 From: Martijn Russchen Date: Wed, 26 Nov 2025 15:03:47 +0100 Subject: [PATCH 2/2] test: add coverage for handleMonthSelectedInChange in DatePicker Add test to verify that monthSelectedIn is reset to 0 when changeMonth is called from a custom header. This ensures the fix for issue #3829 has complete test coverage. --- src/test/datepicker_test.test.tsx | 53 +++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/test/datepicker_test.test.tsx b/src/test/datepicker_test.test.tsx index 94629138d3..2e42e88755 100644 --- a/src/test/datepicker_test.test.tsx +++ b/src/test/datepicker_test.test.tsx @@ -2897,6 +2897,59 @@ describe("DatePicker", () => { expect(instance!.state.monthSelectedIn).toEqual(undefined); }); + it("should reset monthSelectedIn to 0 when changeMonth is called from custom header", () => { + let instance: DatePicker | null = null; + let changeMonthFn: ((month: number) => void) | null = null; + + const { container } = render( + { + instance = node; + }} + inline + monthsShown={2} + selected={newDate("2024-06-15")} + renderCustomHeader={({ changeMonth }) => { + changeMonthFn = changeMonth; + return ( +
+ +
+ ); + }} + />, + ); + + expect(instance).toBeTruthy(); + + // First, select a day in the second month panel to set monthSelectedIn to 1 + const dayButtonsInSecondMonth = container + .querySelectorAll(".react-datepicker__month-container")[1] + ?.querySelectorAll( + ".react-datepicker__day:not(.react-datepicker__day--outside-month)", + ); + expect(dayButtonsInSecondMonth).toBeTruthy(); + expect(dayButtonsInSecondMonth!.length).toBeGreaterThan(0); + + // Click a day in the second month to set monthSelectedIn to 1 + fireEvent.click(dayButtonsInSecondMonth![10]!); + expect(instance!.state.monthSelectedIn).toEqual(1); + + // Now call changeMonth from the custom header + expect(changeMonthFn).toBeTruthy(); + act(() => { + changeMonthFn!(0); // Change to January + }); + + // monthSelectedIn should be reset to 0 + expect(instance!.state.monthSelectedIn).toEqual(0); + }); + it("should show the popper arrow when showPopperArrow is true", () => { const { container } = render(); const input = safeQuerySelector(container, "input");