Repository: gpbl/react-day-picker
Pull request #2672 (2025-01-25 17:05:57)

fix: display calendar in a valid month when month prop is invalid
## What's Changed

Passing a `month` prop that is outside the `startMonth` and `endMonth` range breaks the calendar.
The PR fixes this edge case by always displaying a valid month as suggested in https://github.com/gpbl/react-day-picker/issues/2671#issuecomment-2613980971.

## Type of Change

- [x] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update



Patch:
diff --git a/examples/InvalidMonth.tsx b/examples/InvalidMonth.tsx
new file mode 100644
index 000000000..84f102ec6
--- /dev/null
+++ b/examples/InvalidMonth.tsx
@@ -0,0 +1,16 @@
+import React, { useState } from "react";
+
+import { DayPicker } from "react-day-picker";
+
+export function InvalidMonth() {
+  const [month, setMonth] = useState(new Date(2024, 5));
+  return (
+    <DayPicker
+      month={month}
+      onMonthChange={setMonth}
+      captionLayout="dropdown"
+      startMonth={new Date(2024, 6)}
+      endMonth={new Date(2024, 9)}
+    />
+  );
+}
diff --git a/examples/index.ts b/examples/index.ts
index 0e1910563..f6f570ee2 100644
--- a/examples/index.ts
+++ b/examples/index.ts
@@ -28,6 +28,7 @@ export * from "./HideNavigation";
 export * from "./Input";
 export * from "./InputRange";
 export * from "./InputTime";
+export * from "./InvalidMonth";
 export * from "./ItalianLabels";
 export * from "./ItalianLabels";
 export * from "./Keyboard";
diff --git a/src/helpers/getInitialMonth.ts b/src/helpers/getInitialMonth.ts
index a36584d0a..d89bfe624 100644
--- a/src/helpers/getInitialMonth.ts
+++ b/src/helpers/getInitialMonth.ts
@@ -28,12 +28,12 @@ export function getInitialMonth(
   let initialMonth = month || defaultMonth || today;
   const { differenceInCalendarMonths, addMonths, startOfMonth } = dateLib;
 
-  // Fix the initialMonth if is after the to-date
+  // Fix the initialMonth if is after the endMonth
   if (endMonth && differenceInCalendarMonths(endMonth, initialMonth) < 0) {
     const offset = -1 * (numberOfMonths - 1);
     initialMonth = addMonths(endMonth, offset);
   }
-  // Fix the initialMonth if is before the from-date
+  // Fix the initialMonth if is before the startMonth
   if (startMonth && differenceInCalendarMonths(initialMonth, startMonth) < 0) {
     initialMonth = startMonth;
   }
diff --git a/src/useCalendar.ts b/src/useCalendar.ts
index 4f5c2442b..639b4c813 100644
--- a/src/useCalendar.ts
+++ b/src/useCalendar.ts
@@ -96,7 +96,8 @@ export function useCalendar(
   const initialMonth = getInitialMonth(props, dateLib);
   const [firstMonth, setFirstMonth] = useControlledValue(
     initialMonth,
-    props.month ? startOfMonth(props.month) : undefined
+    // initialMonth is always computed from props.month if provided
+    props.month ? initialMonth : undefined
   );
 
   useEffect(() => {


Test patch:
diff --git a/examples/InvalidMonth.test.tsx b/examples/InvalidMonth.test.tsx
new file mode 100644
index 000000000..20384260b
--- /dev/null
+++ b/examples/InvalidMonth.test.tsx
@@ -0,0 +1,11 @@
+import React from "react";
+
+import { grid } from "@/test/elements";
+import { render } from "@/test/render";
+
+import { InvalidMonth } from "./InvalidMonth";
+
+test("should display calendar in a valid month when month prop is invalid", () => {
+  render(<InvalidMonth />);
+  expect(grid()).toHaveAccessibleName(`July 2024`);
+});
diff --git a/src/helpers/getInitialMonth.test.ts b/src/helpers/getInitialMonth.test.ts
index fd5dec7d3..e0415c0b6 100644
--- a/src/helpers/getInitialMonth.test.ts
+++ b/src/helpers/getInitialMonth.test.ts
@@ -1,50 +1,78 @@
-import { addMonths, isSameMonth } from "date-fns";
+import { addMonths, isSameDay, isSameMonth, startOfMonth } from "date-fns";
 
 import { defaultDateLib } from "../classes/DateLib";
 
 import { getInitialMonth } from "./getInitialMonth";
 
-describe("when no endMonth is given", () => {
+it("return start of month", () => {
+  const month = new Date(2010, 11, 12);
+  const initialMonth = getInitialMonth({ month }, defaultDateLib);
+  expect(isSameDay(initialMonth, startOfMonth(month))).toBe(true);
+});
+
+describe("when no startMonth or endMonth is given", () => {
+  const month = new Date(2010, 11, 12);
+  const defaultMonth = new Date(2011, 11, 12);
+  const today = new Date(2012, 11, 12);
   describe("when month is in context", () => {
-    const month = new Date(2010, 11, 12);
     it("return that month", () => {
-      const startMonth = getInitialMonth({ month }, defaultDateLib);
-      expect(isSameMonth(startMonth, month)).toBe(true);
+      const initialMonth = getInitialMonth(
+        { month, defaultMonth, today },
+        defaultDateLib
+      );
+      expect(isSameMonth(initialMonth, month)).toBe(true);
     });
   });
-  describe("when defaultMonth is in context", () => {
-    const defaultMonth = new Date(2010, 11, 12);
+  describe("when defaultMonth is in context and no month is given", () => {
     it("return that month", () => {
-      const startMonth = getInitialMonth({ defaultMonth }, defaultDateLib);
-      expect(isSameMonth(startMonth, defaultMonth)).toBe(true);
+      const initialMonth = getInitialMonth(
+        { defaultMonth, today },
+        defaultDateLib
+      );
+      expect(isSameMonth(initialMonth, defaultMonth)).toBe(true);
     });
   });
   describe("when no month or defaultMonth", () => {
-    const today = new Date(2010, 11, 12);
     it("return the today month", () => {
-      const startMonth = getInitialMonth({ today }, defaultDateLib);
-      expect(isSameMonth(startMonth, today)).toBe(true);
+      const initialMonth = getInitialMonth({ today }, defaultDateLib);
+      expect(isSameMonth(initialMonth, today)).toBe(true);
     });
   });
 });
+
+describe("when startMonth is given and is after the default initial month", () => {
+  it("return the startMonth", () => {
+    const month = new Date(2010, 11, 12);
+    const startMonth = addMonths(month, 1);
+    const initialMonth = getInitialMonth(
+      { month, numberOfMonths: 3, startMonth },
+      defaultDateLib
+    );
+    expect(isSameMonth(initialMonth, startMonth)).toBe(true);
+  });
+});
+
 describe("when endMonth is given", () => {
-  describe("when endMonth is before the default initial date", () => {
+  describe("when endMonth is before the default initial month", () => {
     const month = new Date(2010, 11, 12);
     const endMonth = addMonths(month, -2);
     describe("when the number of month is 1", () => {
       it("return the endMonth", () => {
-        const startMonth = getInitialMonth({ month, endMonth }, defaultDateLib);
-        expect(isSameMonth(startMonth, endMonth)).toBe(true);
+        const initialMonth = getInitialMonth(
+          { month, endMonth },
+          defaultDateLib
+        );
+        expect(isSameMonth(initialMonth, endMonth)).toBe(true);
       });
     });
     describe("when the number of month is 3", () => {
       it("return the endMonth plus the number of months", () => {
-        const startMonth = getInitialMonth(
+        const initialMonth = getInitialMonth(
           { month, numberOfMonths: 3, endMonth },
           defaultDateLib
         );
         const expectedMonth = addMonths(endMonth, -1 * (3 - 1));
-        expect(isSameMonth(startMonth, expectedMonth)).toBe(true);
+        expect(isSameMonth(initialMonth, expectedMonth)).toBe(true);
       });
     });
   });