Repository: gpbl/react-day-picker
Pull request #2177 (2024-06-01 13:01:17)

fix: `captionLayout` prop not working as expected


Patch:
diff --git a/examples/Dropdown.tsx b/examples/Dropdown.tsx
index 45299745f..d916cb519 100644
--- a/examples/Dropdown.tsx
+++ b/examples/Dropdown.tsx
@@ -3,5 +3,5 @@ import React from "react";
 import { DayPicker } from "react-day-picker";
 
 export function Dropdown() {
-  return <DayPicker dropdownNavigation fromYear={2015} toYear={2025} />;
+  return <DayPicker captionLayout="dropdown" fromYear={2010} />;
 }
diff --git a/examples/DropdownMultipleMonths.tsx b/examples/DropdownMultipleMonths.tsx
index b9e50b46c..4c7458f09 100644
--- a/examples/DropdownMultipleMonths.tsx
+++ b/examples/DropdownMultipleMonths.tsx
@@ -6,7 +6,7 @@ export function DropdownMultipleMonths() {
   return (
     <DayPicker
       numberOfMonths={5}
-      dropdownNavigation
+      captionLayout="dropdown"
       fromYear={2015}
       toYear={2025}
     />
diff --git a/src/components/MonthCaption.tsx b/src/components/MonthCaption.tsx
index 2cf594d62..b49f43c8f 100644
--- a/src/components/MonthCaption.tsx
+++ b/src/components/MonthCaption.tsx
@@ -24,7 +24,7 @@ export function MonthCaption(props: {
 }) {
   const {
     classNames,
-    dropdownNavigation,
+    captionLayout,
     formatters: { formatCaption },
     locale,
     styles
@@ -36,22 +36,15 @@ export function MonthCaption(props: {
       className={classNames[UI.MonthCaption]}
       style={styles?.[UI.MonthCaption]}
     >
-      {dropdownNavigation ? (
-        <DropdownNav
-          month={props.month}
-          index={props.index}
-          showMonths
-          showYears
-        />
-      ) : dropdownNavigation ? (
+      {captionLayout?.startsWith("dropdown") ? (
         <DropdownNav
           month={props.month}
           index={props.index}
           showMonths={
-            dropdownNavigation === true || dropdownNavigation === "month"
+            captionLayout === "dropdown" || captionLayout === "dropdown-months"
           }
           showYears={
-            dropdownNavigation === true || dropdownNavigation === "year"
+            captionLayout === "dropdown" || captionLayout === "dropdown-years"
           }
         />
       ) : (
diff --git a/src/components/MonthsDropdown.tsx b/src/components/MonthsDropdown.tsx
index cf657d3b2..87c367480 100644
--- a/src/components/MonthsDropdown.tsx
+++ b/src/components/MonthsDropdown.tsx
@@ -26,6 +26,7 @@ export function MonthsDropdown(props: {
   const {
     classNames,
     components,
+    disableNavigation,
     labels: { labelMonthDropdown }
   } = useProps();
 
@@ -43,6 +44,7 @@ export function MonthsDropdown(props: {
     <Dropdown
       name="month"
       aria-label={labelMonthDropdown()}
+      aria-disabled={disableNavigation ? "true" : undefined}
       rootClassName={classNames[UI.MonthsDropdown]}
       options={dropdown.months}
       value={props.month.date.getMonth()}
diff --git a/src/components/YearsDropdown.tsx b/src/components/YearsDropdown.tsx
index 2818e045d..23dc29966 100644
--- a/src/components/YearsDropdown.tsx
+++ b/src/components/YearsDropdown.tsx
@@ -26,6 +26,7 @@ export function YearsDropdown(props: {
   const {
     classNames,
     components,
+    disableNavigation,
     labels: { labelYearDropdown }
   } = useProps();
 
@@ -42,8 +43,8 @@ export function YearsDropdown(props: {
   const Dropdown = components?.Dropdown ?? DefaultDropdown;
   return (
     <Dropdown
-      name="year"
       aria-label={labelYearDropdown()}
+      aria-disabled={disableNavigation ? "true" : undefined}
       rootClassName={classNames[UI.YearsDropdown]}
       options={dropdown.years}
       value={props.month.date.getFullYear()}
diff --git a/src/helpers/getFromToDate.ts b/src/helpers/getFromToDate.ts
index 1f3949eb2..5dde7181a 100644
--- a/src/helpers/getFromToDate.ts
+++ b/src/helpers/getFromToDate.ts
@@ -21,24 +21,24 @@ export function getFromToDate(
     | "fromMonth"
     | "toMonth"
     | "today"
-    | "dropdownNavigation"
+    | "captionLayout"
   >
 ): Pick<DayPickerProps<Mode>, "fromDate" | "toDate"> {
   const { fromYear, toYear, fromMonth, toMonth } = props;
   let { fromDate, toDate } = props;
-
+  const hasDropdowns = props.captionLayout?.startsWith("dropdown");
   if (fromMonth) {
     fromDate = startOfMonth(fromMonth);
   } else if (fromYear) {
     fromDate = new Date(fromYear, 0, 1);
-  } else if (!fromDate && props.dropdownNavigation !== undefined) {
+  } else if (!fromDate && hasDropdowns) {
     fromDate = startOfYear(addYears(props.today ?? new Date(), -100));
   }
   if (toMonth) {
     toDate = endOfMonth(toMonth);
   } else if (toYear) {
     toDate = new Date(toYear, 11, 31);
-  } else if (!toDate && props.dropdownNavigation) {
+  } else if (!toDate && hasDropdowns) {
     toDate = endOfYear(props.today ?? new Date());
   }
   return {
diff --git a/src/types.ts b/src/types.ts
index 93c7488bc..fa999869b 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -143,15 +143,17 @@ export interface PropsBase {
   /**
    * Show dropdowns to navigate between months or years.
    *
-   * - `true`: display the dropdowns for both month and year.
-   * - `month`: display the dropdown for the month.
-   * - `year`: display the dropdown for the year.
-   * - `false`: hide the dropdowns.
+   * - `true`: display the dropdowns for both month and year
+   * - `label`: display the month and the year as a label. Change the label with
+   *   the `formatCaption` formatter.
+   * - `month`: display only the dropdown for the months
+   * - `year`: display only the dropdown for the years
    *
-   * **Note:** showing the dropdown will default {@link fromYear} to the 100
-   * years ago, and {@link toYear} to the current year.
+   * **Note:** showing the dropdown will set the start/end months
+   * {@link fromYear} to the 100 years ago, and {@link toYear} to the current
+   * year.
    */
-  dropdownNavigation?: boolean | "month" | "year";
+  captionLayout?: "label" | "dropdown" | "dropdown-months" | "dropdown-years";
   /**
    * Display always 6 weeks per each month, regardless the month’s number of
    * weeks. Weeks will be filled with the days from the next month.
diff --git a/website/docs/using-daypicker/customization.mdx b/website/docs/using-daypicker/customization.mdx
index bf28306fd..7edd5e4ca 100644
--- a/website/docs/using-daypicker/customization.mdx
+++ b/website/docs/using-daypicker/customization.mdx
@@ -8,37 +8,39 @@ Use the customization props to customize the appearance of the calendar.
 
 ### Customization Props
 
-| Prop Name         | Type                                                     | Description                                                      |
-| ----------------- | -------------------------------------------------------- | ---------------------------------------------------------------- |
-| `captionLayout`   | `"dropdown"` \| `"buttons"` \|<br/> `"dropdown-buttons"` | Change the layout of the caption. <br /> Default is `"buttons"`. |
-| `showOutsideDays` | `boolean`                                                | Display the days falling into the other months.                  |
-| `fixedWeeks`      | `boolean`                                                | Display 6 weeks per months.                                      |
-| `hideWeekdayRow`  | `boolean`                                                | Hide the row displaying the weekday names.                       |
-| `footer`          | `ReactNode`                                              | Displays a footer below the calendar.                            |
+| Prop Name         | Type                                                                                  | Description                                                 |
+| ----------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
+| `captionLayout`   | \| `label`<br/> \| `"dropdown"`<br/> \| `"dropdown-months"`<br/> \|`"dropdown-years"` | Choose the layout of the month caption. Default is `label`. |
+| `showOutsideDays` | `boolean`                                                                             | Display the days falling into the other months.             |
+| `fixedWeeks`      | `boolean`                                                                             | Display 6 weeks per months.                                 |
+| `hideWeekdayRow`  | `boolean`                                                                             | Hide the row displaying the weekday names.                  |
+| `footer`          | `ReactNode`                                                                           | Displays a footer below the calendar.                       |
 
 ## Caption Layout
 
-Use the `captionLayout` prop to the layout of the caption used to navigate the calendar.
-
-As default, DayPicker will show navigation buttons on the top corner.
+Use the `captionLayout` prop to choose the layout of the month caption:
 
 ```tsx
-<DayPicker captionLayout="buttons" /> // Default value
+<DayPicker captionLayout="label" /> // Default value: shows the month and year.
 ```
 
-### Dropdown Navigation
-
-Use `dropdown` or `dropdown-buttons` to display a navigation dropdown.
+| Caption Layout      | Description                                           |
+| ------------------- | ----------------------------------------------------- |
+| `"label"`           | Display the month and year as a label. Default value. |
+| `"dropdown"`        | Display a dropdown with the months and years.         |
+| `"dropdown-months"` | Display a dropdown with the months only.              |
+| `"dropdown-years"`  | Display a dropdown with the years only.               |
 
-:::warning
+### Caption Dropdown
 
-To use this prop, you need to set both `fromYear` and `toYear`. (This requirement will be removed in a next version.)
-
-:::
+Set `captionLayout="dropdown"` to display a navigation dropdown. This will allow navigation within the last 100 years. Use the `from*` and `to*` props to [set the start and end dates](./navigation.mdx#start-and-end-dates).
 
 ```tsx
-<DayPicker captionLayout="dropdown" fromYear={2010} toYear={2024} />
-<DayPicker captionLayout="dropdown-buttons" fromYear={2010} toYear={2024} />
+<DayPicker captionLayout="dropdown" />
+<DayPicker captionLayout="dropdown-months"  /> // Only months
+<DayPicker captionLayout="dropdown-years"  /> // Only years
+
+<DayPicker captionLayout="dropdown" fromYear={2015} toYear={2025} /> // Set the years range
 ```
 
 <BrowserWindow>
@@ -59,17 +61,10 @@ By default, DayPicker hides the days falling into the other months. Use `showOut
 
 ### Fixed Weeks
 
-In a year, months can have between 4 an 6 weeks. Use `fixedWeeks` with `showOutsideDays` to always display 6 weeks per month. This will prevent the grid changing its height while navigating the calendar.
-
-:::note
-
-In the current version, this prop requires `showOutsideDays` set to work. This
-requirement will be removed in the next major version.
-
-:::
+In a year, months can have between 4 an 6 weeks. Use `fixedWeeks` to always display 6 weeks per month. This will prevent the grid changing its height while navigating the calendar.
 
 ```tsx
-<DayPicker showOutsideDays fixedWeeks />
+<DayPicker fixedWeeks showWeekNumber />
 ```
 
 <BrowserWindow>
@@ -122,3 +117,21 @@ To display the column with the week numbers, use the `showWeekNumber` prop. Use
 <BrowserWindow>
   <Examples.Weeknumber />
 </BrowserWindow>
+
+## Formatters and Labels
+
+Use the [formatters](./localization.mdx#formatters) and [labels](./localization.mdx#labels) to customize the format of the dates and the ARIA labels.
+
+| Prop Name    | Type                                              | Description                                    |
+| ------------ | ------------------------------------------------- | ---------------------------------------------- |
+| `formatters` | [`Formatters`](../api/type-aliases/Formatters.md) | Format of the dates displayed in the calendar. |
+| `labels`     | [`Labels`](../api/type-aliases/Labels.md)         | Set the ARIA labels used in the calendar.      |
+
+## Custom Components
+
+In DayPicker, you can replace the components used internally to render the calendar. See the [Custom Components](../advanced-guides/custom-components.mdx) guide for more information.
+
+| Prop Name    | Type                                                          | Description                                                 |
+| ------------ | ------------------------------------------------------------- | ----------------------------------------------------------- |
+| `components` | [`CustomComponents`](../api/type-aliases/CustomComponents.md) | Change the internal components used to render the calendar. |
+| `classNames` | [`ClassNames`](../api/type-aliases/ClassNames.md)             | Use custom class names instead of the default ones.         |


Test patch:
diff --git a/src/helpers/getFromToDate.test.ts b/src/helpers/getFromToDate.test.ts
index 514127078..fab7b0433 100644
--- a/src/helpers/getFromToDate.test.ts
+++ b/src/helpers/getFromToDate.test.ts
@@ -50,10 +50,10 @@ describe('when "toYear" is passed in', () => {
   });
 });
 
-describe('when "showCaption" is passed in', () => {
+describe('when "captionLayout" is dropdown', () => {
   const today = new Date(2024, 4, 3);
   const { fromDate, toDate } = getFromToDate({
-    dropdownNavigation: true,
+    captionLayout: "dropdown",
     today
   });
   test('"fromDate" should be 100 years ago', () => {
@@ -62,35 +62,34 @@ describe('when "showCaption" is passed in', () => {
   test('"toDate" should be the end of this year', () => {
     expect(toDate).toEqual(new Date(2024, 11, 31));
   });
-});
-
-describe('when "fromYear" and "showCaption" are passed in', () => {
-  const today = new Date(2024, 4, 3);
-  const fromYear = 2022;
-  const { fromDate, toDate } = getFromToDate({
-    dropdownNavigation: true,
-    fromYear,
-    today
-  });
-  test('"fromDate" should be equal to the "fromYear"', () => {
-    expect(fromDate).toEqual(new Date(2022, 0, 1));
-  });
-  test('"toDate" should be the end of this year', () => {
-    expect(toDate).toEqual(new Date(2024, 11, 31));
-  });
-});
-describe('when "toYear" and "showCaption" are passed in', () => {
-  const today = new Date(2021, 4, 3);
-  const toYear = 2022;
-  const { toDate, fromDate } = getFromToDate({
-    dropdownNavigation: true,
-    toYear,
-    today
-  });
-  test('"fromDate" should be 100 years ago', () => {
-    expect(fromDate).toEqual(new Date(1921, 0, 1));
+  describe('when "fromYear" is set', () => {
+    const today = new Date(2024, 4, 3);
+    const fromYear = 2022;
+    const { fromDate, toDate } = getFromToDate({
+      captionLayout: "dropdown",
+      fromYear,
+      today
+    });
+    test('"fromDate" should be equal to the "fromYear"', () => {
+      expect(fromDate).toEqual(new Date(2022, 0, 1));
+    });
+    test('"toDate" should be the end of this year', () => {
+      expect(toDate).toEqual(new Date(2024, 11, 31));
+    });
   });
-  test('"toDate" should be equal to "toYear"', () => {
-    expect(toDate).toEqual(new Date(2022, 11, 31));
+  describe('when "toYear" is set', () => {
+    const today = new Date(2021, 4, 3);
+    const toYear = 2022;
+    const { toDate, fromDate } = getFromToDate({
+      captionLayout: "dropdown",
+      toYear,
+      today
+    });
+    test('"fromDate" should be 100 years ago', () => {
+      expect(fromDate).toEqual(new Date(1921, 0, 1));
+    });
+    test('"toDate" should be equal to "toYear"', () => {
+      expect(toDate).toEqual(new Date(2022, 11, 31));
+    });
   });
 });