diff --git a/examples/CustomCaption.tsx b/examples/CustomCaption.tsx
index fe0d9c27ea..1df5b6d716 100644
--- a/examples/CustomCaption.tsx
+++ b/examples/CustomCaption.tsx
@@ -1,10 +1,14 @@
import React from "react";
import { format } from "date-fns";
-import { MonthCaptionProps, DayPicker, useCalendar } from "react-day-picker";
+import {
+ MonthCaptionProps,
+ DayPicker,
+ useCalendarContext
+} from "react-day-picker";
function CustomMonthCaption(props: MonthCaptionProps) {
- const { goToMonth, nextMonth, previousMonth } = useCalendar();
+ const { goToMonth, nextMonth, previousMonth } = useCalendarContext();
return (
{format(props.month.date, "MMM yyy")}
diff --git a/examples/CustomMultiple.tsx b/examples/CustomMultiple.tsx
index 46083001e3..9568c26b61 100644
--- a/examples/CustomMultiple.tsx
+++ b/examples/CustomMultiple.tsx
@@ -1,12 +1,15 @@
import React, { useState } from "react";
import { isSameDay } from "date-fns";
-import { DayMouseEventHandler, DayPicker } from "react-day-picker";
+import { DayEventHandler, DayPicker } from "react-day-picker";
export function CustomMultiple() {
const [value, setValue] = useState([]);
- const handleDayClick: DayMouseEventHandler = (day, modifiers) => {
+ const handleDayClick: DayEventHandler = (
+ day,
+ modifiers
+ ) => {
const newValue = [...value];
if (modifiers.selected) {
const index = value.findIndex((d) => isSameDay(day, d));
diff --git a/examples/CustomSingle.tsx b/examples/CustomSingle.tsx
index e31a6dbde6..a13504d747 100644
--- a/examples/CustomSingle.tsx
+++ b/examples/CustomSingle.tsx
@@ -4,7 +4,7 @@ import { DayPicker, DayPickerProps, type Mode } from "react-day-picker";
export function CustomSingle() {
const [selectedDate, setSelectedDate] = useState();
- const modifiers: DayPickerProps["modifiers"] = {};
+ const modifiers: DayPickerProps["modifiers"] = {};
if (selectedDate) {
modifiers.selected = selectedDate;
}
diff --git a/examples/CustomWeek.test.tsx b/examples/CustomWeek.test.tsx
index fc83068dbb..9607aea0b2 100644
--- a/examples/CustomWeek.test.tsx
+++ b/examples/CustomWeek.test.tsx
@@ -15,7 +15,7 @@ beforeEach(() => {
describe("when a day is clicked", () => {
beforeEach(async () => {
- await act(() => user.click(gridcell(today)));
+ await user.click(gridcell(today));
});
test("the whole week should appear selected", () => {
const week = [
@@ -33,7 +33,7 @@ describe("when a day is clicked", () => {
});
describe("when clicking the day again", () => {
beforeEach(async () => {
- await act(() => user.click(gridcell(today)));
+ await user.click(gridcell(today));
});
test("the whole week should not be selected", () => {
const week = [
diff --git a/examples/Dialog.tsx b/examples/Dialog.tsx
index a6004e56a8..a535e965ec 100644
--- a/examples/Dialog.tsx
+++ b/examples/Dialog.tsx
@@ -1,7 +1,7 @@
import React, { useEffect, useId, useRef, useState } from "react";
import { format, isValid, parse } from "date-fns";
-import { DayPicker, type SelectHandler } from "react-day-picker";
+import { DayPicker } from "react-day-picker";
export function Dialog() {
const dialogRef = useRef(null);
@@ -45,7 +45,7 @@ export function Dialog() {
* Function to handle the DayPicker select event: update the input value and
* the selected date, and set the month.
*/
- const handleDayPickerSelect: SelectHandler<"single"> = (date) => {
+ const handleDayPickerSelect = (date: Date | undefined) => {
if (!date) {
setInputValue("");
setSelectedDate(undefined);
diff --git a/examples/Input.test.tsx b/examples/Input.test.tsx
index 15ad1ee3e0..c199e765bb 100644
--- a/examples/Input.test.tsx
+++ b/examples/Input.test.tsx
@@ -3,7 +3,7 @@ import React from "react";
import { format } from "date-fns";
-import { act, render, screen } from "@/test/render";
+import { render, screen } from "@/test/render";
import { user } from "@/test/user";
import { Input } from "./Input";
diff --git a/examples/Keyboard.tsx b/examples/Keyboard.tsx
index d57887215b..36ae98ab7d 100644
--- a/examples/Keyboard.tsx
+++ b/examples/Keyboard.tsx
@@ -1,8 +1,12 @@
import React, { useState } from "react";
-import { DayPicker, type DayPickerProps } from "react-day-picker";
+import {
+ DayPicker,
+ type DayPickerProps,
+ type PropsSingle
+} from "react-day-picker";
-export function Keyboard(props: DayPickerProps<"single">) {
+export function Keyboard(props: DayPickerProps & PropsSingle) {
const [selected, setSelected] = useState(undefined);
return (
- );
+ const selected = [new Date(), addDays(new Date(), 1)];
+ return ;
}
diff --git a/examples/None.tsx b/examples/None.tsx
index c515580c08..ec60f8c7b6 100644
--- a/examples/None.tsx
+++ b/examples/None.tsx
@@ -3,5 +3,5 @@ import React from "react";
import { DayPicker } from "react-day-picker";
export function None() {
- return ;
+ return ;
}
diff --git a/examples/Range.test.tsx b/examples/Range.test.tsx
index 410d88b0a6..feb12d07eb 100644
--- a/examples/Range.test.tsx
+++ b/examples/Range.test.tsx
@@ -49,7 +49,7 @@ describe("when a day in the range is clicked", () => {
describe("when a day in the range is clicked again", () => {
const day = days[2];
beforeEach(async () => act(() => user.click(gridcell(day))));
- test("no day should be selected (??)", () => {
+ test("no day should be selected", () => {
expect(getAllSelectedDays()).toHaveLength(0);
});
test("should match the snapshot", () => {
diff --git a/examples/RangeShiftKey.tsx b/examples/RangeShiftKey.tsx
index dcec46679e..7c381a3570 100644
--- a/examples/RangeShiftKey.tsx
+++ b/examples/RangeShiftKey.tsx
@@ -4,12 +4,12 @@ import { isSameDay } from "date-fns";
import {
DateRange,
DayPicker,
- type DayProps,
- useSelection
+ useRangeContext,
+ type DayProps
} from "react-day-picker";
function DayWithShiftKey(props: DayProps) {
- const { selected } = useSelection<"range">();
+ const { selected } = useRangeContext();
const onClick = props.rootProps?.onClick;
const handleClick: MouseEventHandler = (e) => {
diff --git a/examples/Single.test.tsx b/examples/Single.test.tsx
index be15f1f9dc..bcf8b16d26 100644
--- a/examples/Single.test.tsx
+++ b/examples/Single.test.tsx
@@ -21,13 +21,16 @@ describe("when a day is clicked", () => {
});
test("should appear as selected", () => {
expect(gridcell(day)).toHaveAttribute("aria-selected", "true");
+ expect(gridcell(day)).toHaveFocus();
+ expect(gridcell(day)).toHaveClass("rdp-selected");
});
describe("when the day is clicked again", () => {
beforeEach(async () => {
await user.click(gridcell(day));
});
- test("should appear as not selected", () => {
+ test("should not appear as selected", () => {
expect(gridcell(day)).not.toHaveAttribute("aria-selected");
+ expect(gridcell(day)).not.toHaveClass("rdp-selected");
});
});
});
diff --git a/examples/TestCase2047.tsx b/examples/TestCase2047.tsx
index 8272302bf4..24b1d94d0b 100644
--- a/examples/TestCase2047.tsx
+++ b/examples/TestCase2047.tsx
@@ -10,7 +10,7 @@ export function TestCase2047() {
const [selected, setSelected] = React.useState(defaultSelected);
return (
= (
- range: DateRange | undefined
- ) => {
+ const handleChange = (range: DateRange | undefined) => {
range && setSelected(range);
};
return (
diff --git a/examples/__snapshots__/Range.test.tsx.snap b/examples/__snapshots__/Range.test.tsx.snap
index f156b06fe9..7e90ec07f0 100644
--- a/examples/__snapshots__/Range.test.tsx.snap
+++ b/examples/__snapshots__/Range.test.tsx.snap
@@ -342,7 +342,7 @@ exports[`should match the snapshot 1`] = `
aria-colindex="1"
aria-label=""
aria-selected="true"
- class="rdp-day rdp-focusable rdp-range_start rdp-selected"
+ class="rdp-day rdp-range_start rdp-selected rdp-focusable"
role="gridcell"
tabindex="0"
>
@@ -356,7 +356,7 @@ exports[`should match the snapshot 1`] = `
aria-colindex="2"
aria-label=""
aria-selected="true"
- class="rdp-day rdp-focusable rdp-range_middle rdp-selected"
+ class="rdp-day rdp-range_middle rdp-selected rdp-focusable"
role="gridcell"
tabindex="-1"
>
@@ -370,7 +370,7 @@ exports[`should match the snapshot 1`] = `
aria-colindex="3"
aria-label=""
aria-selected="true"
- class="rdp-day rdp-focusable rdp-range_middle rdp-selected"
+ class="rdp-day rdp-range_middle rdp-selected rdp-focusable"
role="gridcell"
tabindex="-1"
>
@@ -384,7 +384,7 @@ exports[`should match the snapshot 1`] = `
aria-colindex="4"
aria-label=""
aria-selected="true"
- class="rdp-day rdp-focusable rdp-range_middle rdp-selected"
+ class="rdp-day rdp-range_middle rdp-selected rdp-focusable"
role="gridcell"
tabindex="-1"
>
@@ -398,7 +398,7 @@ exports[`should match the snapshot 1`] = `
aria-colindex="5"
aria-label=""
aria-selected="true"
- class="rdp-day rdp-focusable rdp-range_end rdp-selected"
+ class="rdp-day rdp-range_end rdp-selected rdp-focusable"
role="gridcell"
tabindex="-1"
>
diff --git a/react-day-picker.code-workspace b/react-day-picker.code-workspace
index c5cc442ef6..647f6e3f52 100644
--- a/react-day-picker.code-workspace
+++ b/react-day-picker.code-workspace
@@ -34,6 +34,16 @@
"**/node_modules": true,
"**/versioned_docs": true
},
- "jest.runMode": "on-save"
+ "jest.runMode": "on-demand",
+ "git.showProgress": true,
+ "scm.diffDecorationsGutterVisibility": "hover",
+ "scm.diffDecorationsGutterPattern": {
+ "modified": false
+ },
+ "scm.diffDecorationsGutterWidth": 1,
+ "scm.diffDecorations": "none",
+ "[typescriptreact]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ }
}
}
diff --git a/src/DayPicker.tsx b/src/DayPicker.tsx
index 2d3dcd12de..0119a6d05c 100644
--- a/src/DayPicker.tsx
+++ b/src/DayPicker.tsx
@@ -1,22 +1,17 @@
import React from "react";
import { Calendar } from "./components/Calendar";
-import { ContextProviders } from "./contexts/root";
-import type { DayPickerProps, Mode } from "./types";
+import { ContextProviders } from "./contexts/providers";
+import type { DayPickerProps } from "./types";
/**
* DayPicker is a React component to create date pickers, calendars, and date
* inputs for web applications.
*
- * @template T - The {@link Mode | selection mode}. Defaults to `"default"`.
- * @template R - Whether the selection is required. Defaults to `false`.
* @group Components
* @see http://daypicker.dev
*/
-export function DayPicker<
- T extends Mode = "default",
- R extends boolean = false
->(props: DayPickerProps) {
+export function DayPicker(props: DayPickerProps) {
return (
diff --git a/src/UI.ts b/src/UI.ts
index c46f37113c..f8705cf4c7 100644
--- a/src/UI.ts
+++ b/src/UI.ts
@@ -4,6 +4,8 @@ import type { CustomComponents, ClassNames, Styles } from "./types";
* The UI elements composing DayPicker. These elements are mapped to
* {@link CustomComponents}, the {@link ClassNames} and the {@link Styles} used by
* DayPicker.
+ *
+ * Some of these elements are extended by flags and modifiers.
*/
export enum UI {
/** The previous button in the navigation. */
@@ -17,7 +19,10 @@ export enum UI {
Calendar = "calendar",
/** The Chevron SVG element used by navigation buttons and dropdowns. */
Chevron = "chevron",
- /** The grid cell with the day's date. Extended by {@link DayModifier}. */
+ /**
+ * The grid cell with the day's date. Extended by {@link DayFlag} and
+ * {@link SelectionFlag}.
+ */
Day = "day",
/** The element containing the formatted day's date, inside the grid cell. */
DayDate = "day_date",
@@ -60,14 +65,27 @@ export enum UI {
YearsDropdown = "years_dropdown"
}
-/** The modifiers for the {@link UI.Day}. */
-export enum DayModifier {
+/** The flags for the {@link UI.Day}. */
+export enum DayFlag {
/** The day is disabled */
disabled = "disabled",
/** The day is hidden */
hidden = "hidden",
/** The day is outside the current month */
outside = "outside",
+ /** The day is focusable. */
+ focusable = "focusable",
+ /** The day is focused. */
+ focused = "focused",
+ /** The day is today. */
+ today = "today"
+}
+
+/**
+ * The state that can be applied to the {@link UI.Day} element when in selection
+ * mode.
+ */
+export enum SelectionState {
/** The day is at the end of a selected range. */
range_end = "range_end",
/** The day is at the middle of a selected range. */
@@ -75,23 +93,17 @@ export enum DayModifier {
/** The day is at the start of a selected range. */
range_start = "range_start",
/** The day is selected. */
- selected = "selected",
- /** The day is focusable. */
- focusable = "focusable",
- /** The day is focused. */
- focused = "focused",
- /** The day is today. */
- today = "today"
+ selected = "selected"
}
/** Flags that can be applied to the {@link UI.Calendar} element. */
export enum CalendarFlag {
/** Assigned when the week numbers are show. */
- hasWeekNumbers = "has_week_numbers",
+ has_week_numbers = "has_week_numbers",
/** Assigned when the weekdays are hidden. */
- noWeekdays = "no_weekdays",
+ no_weekdays = "no_weekdays",
/** Assigned when the calendar has multiple months. */
- hasMultipleMonths = "has_multiple_months"
+ has_multiple_months = "has_multiple_months"
}
/** Flags that can be applied to the {@link UI.Chevron} element. */
@@ -106,5 +118,5 @@ export enum WeekNumberFlag {
* Assigned when the week number is interactive, i.e. has an
* `onWeekNumberClick` event attached to it.
*/
- isInteractive = "week_number_interactive"
+ week_number_interactive = "week_number_interactive"
}
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index 22bb29032b..2e4805c1a7 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -1,7 +1,4 @@
-import React, { ButtonHTMLAttributes } from "react";
-
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-import type { PropsBase } from "../types";
+import React, { type ButtonHTMLAttributes } from "react";
/**
* Render the button elements in the calendar.
diff --git a/src/components/Calendar.tsx b/src/components/Calendar.tsx
index 9ae9626098..48b25562de 100644
--- a/src/components/Calendar.tsx
+++ b/src/components/Calendar.tsx
@@ -1,8 +1,8 @@
import React from "react";
import { UI, CalendarFlag } from "../UI";
-import { useCalendar } from "../contexts/calendar";
-import { useProps } from "../contexts/props";
+import { useCalendarContext } from "../contexts/calendar";
+import { usePropsContext } from "../contexts/props";
import { Footer as DefaultFooter } from "./Footer";
import { Month as DefaultMonth } from "./Month";
@@ -35,9 +35,9 @@ export function Calendar() {
style,
styles,
title
- } = useProps();
+ } = usePropsContext();
- const calendar = useCalendar();
+ const calendar = useCalendarContext();
// Apply classnames according to props
const cssClassNames = [classNames[UI.Calendar]];
@@ -45,13 +45,13 @@ export function Calendar() {
cssClassNames.push(className);
}
if (numberOfMonths > 1) {
- cssClassNames.push(classNames[CalendarFlag.hasMultipleMonths]);
+ cssClassNames.push(classNames[CalendarFlag.has_multiple_months]);
}
if (showWeekNumber) {
- cssClassNames.push(classNames[CalendarFlag.hasWeekNumbers]);
+ cssClassNames.push(classNames[CalendarFlag.has_week_numbers]);
}
if (hideWeekdayRow) {
- cssClassNames.push(classNames[CalendarFlag.noWeekdays]);
+ cssClassNames.push(classNames[CalendarFlag.no_weekdays]);
}
const Nav = components?.Nav ?? DefaultNav;
diff --git a/src/components/Chevron.tsx b/src/components/Chevron.tsx
index 3728821241..0abd469a25 100644
--- a/src/components/Chevron.tsx
+++ b/src/components/Chevron.tsx
@@ -1,7 +1,7 @@
import React from "react";
import { ChevronFlag, UI } from "../UI";
-import { useProps } from "../contexts/props";
+import { usePropsContext } from "../contexts/props";
/**
* Render the chevron icon used in the navigation buttons and dropdowns.
@@ -16,7 +16,7 @@ export function Chevron(props: {
orientation?: "up" | "down" | "left" | "right";
}) {
const { size = 24, orientation = "left" } = props;
- const { classNames, disableNavigation } = useProps();
+ const { classNames, disableNavigation } = usePropsContext();
const svgClassName = [
classNames[UI.Chevron],
diff --git a/src/components/Day.tsx b/src/components/Day.tsx
index a99d2eef2f..cba36e61d3 100644
--- a/src/components/Day.tsx
+++ b/src/components/Day.tsx
@@ -1,8 +1,8 @@
import React from "react";
-import type { HTMLAttributes, ReactNode } from "react";
+import type { ReactNode } from "react";
import type { CalendarDay } from "../classes";
-import type { DayModifiers } from "../types";
+import type { Modifiers } from "../types";
/**
* Render the gridcell of a day in the calendar and handle the interaction and
@@ -17,7 +17,7 @@ import type { DayModifiers } from "../types";
*/
export function Day(props: {
day: CalendarDay;
- modifiers: DayModifiers;
+ modifiers: Modifiers;
children?: ReactNode;
rootProps: Pick<
JSX.IntrinsicElements["div"],
diff --git a/src/components/DayDate.tsx b/src/components/DayDate.tsx
index 28c0b1605a..e7275b3f29 100644
--- a/src/components/DayDate.tsx
+++ b/src/components/DayDate.tsx
@@ -1,7 +1,7 @@
import React from "react";
import type { CalendarDay } from "../classes";
-import type { DayModifiers } from "../types";
+import type { Modifiers } from "../types";
/**
* Render the date as string inside the day grid cell.
@@ -16,7 +16,7 @@ export function DayDate(props: {
/** The date to display. */
formattedDate: string;
/** The modifiers for the day. */
- modifiers: DayModifiers;
+ modifiers: Modifiers;
/** The HTML attributes for the root element. */
rootProps: {
className: string;
@@ -27,3 +27,14 @@ export function DayDate(props: {
}
export type DayDateProps = Parameters[0];
+
+/**
+ * @deprecated The component has been renamed. Use `DayDate` instead.
+ * @protected
+ */
+export const DayContent = DayDate;
+/**
+ * @deprecated The type has been renamed. Use `DayDateProps` instead.
+ * @protected
+ */
+export type DayContentProps = DayDateProps;
diff --git a/src/components/DayWrapper.tsx b/src/components/DayWrapper.tsx
index 222601c164..d6c571e6b1 100644
--- a/src/components/DayWrapper.tsx
+++ b/src/components/DayWrapper.tsx
@@ -1,24 +1,17 @@
import React from "react";
-import {
- type FocusEventHandler,
- type KeyboardEventHandler,
- type MouseEventHandler,
- type PointerEventHandler,
- type TouchEventHandler,
- useEffect,
- useRef
-} from "react";
-import { UI, DayModifier } from "../UI";
+import { UI, DayFlag } from "../UI";
import { CalendarDay } from "../classes/CalendarDay";
-import { useCalendar } from "../contexts/calendar";
-import { useFocus } from "../contexts/focus";
-import { useModifiers } from "../contexts/modifiers";
-import { useProps } from "../contexts/props";
-import { useSelection } from "../contexts/selection";
+import { useCalendarContext } from "../contexts/calendar";
+import { useFocusContext } from "../contexts/focus";
+import { useModifiersContext } from "../contexts/modifiers";
+import { usePropsContext } from "../contexts/props";
import { debounce } from "../helpers/debounce";
import { getClassNamesForModifiers } from "../helpers/getClassNamesForModifiers";
import { getStyleForModifiers } from "../helpers/getStyleForModifiers";
+import { useMultiContext } from "../selection/multi";
+import { useRangeContext } from "../selection/range";
+import { useSingleContext } from "../selection/single";
import { DayProps, Day as DefaultDay } from "./Day";
import { DayDateProps, DayDate as DefaultDayDate } from "./DayDate";
@@ -34,7 +27,7 @@ export function DayWrapper(props: {
/** The day to be rendered in the gridcell. */
day: CalendarDay;
}) {
- const cellRef = useRef(null);
+ const cellRef = React.useRef(null);
const {
classNames,
@@ -43,6 +36,7 @@ export function DayWrapper(props: {
formatters: { formatDay },
labels: { labelDay },
locale,
+ mode,
modifiersClassNames = {},
modifiersStyles = {},
onDayFocus,
@@ -60,15 +54,18 @@ export function DayWrapper(props: {
onDayTouchMove,
onDayTouchStart,
styles = {}
- } = useProps();
+ } = usePropsContext();
- const { isInteractive } = useCalendar();
- const { setSelected } = useSelection();
- const { getModifiers } = useModifiers();
+ const { isInteractive } = useCalendarContext();
+ const { getModifiers } = useModifiersContext();
+
+ const single = useSingleContext();
+ const multi = useMultiContext();
+ const range = useRangeContext();
const {
autoFocusTarget,
- focusedDay,
+ focused,
focus,
blur,
focusDayBefore,
@@ -81,23 +78,37 @@ export function DayWrapper(props: {
focusYearAfter,
focusStartOfWeek,
focusEndOfWeek
- } = useFocus();
+ } = useFocusContext();
+
const modifiers = getModifiers(props.day);
- const onClick: MouseEventHandler = (e) => {
+ const onClick: React.MouseEventHandler = (e) => {
if (modifiers.disabled) {
e.preventDefault();
e.stopPropagation();
return;
}
- setSelected(props.day.date, modifiers, e);
+
+ switch (mode) {
+ case "single":
+ single.setSelected(props.day.date, modifiers, e);
+ break;
+ case "multiple":
+ multi.setSelected(props.day.date, modifiers, e);
+ break;
+ case "range":
+ range.setSelected(props.day.date, modifiers, e);
+ break;
+ }
+
if (modifiers.focusable) {
focus(props.day);
}
+
onDayClick?.(props.day.date, modifiers, e);
};
- const onFocus: FocusEventHandler = (e) => {
+ const onFocus: React.FocusEventHandler = (e) => {
if (modifiers.disabled) {
e.preventDefault();
e.stopPropagation();
@@ -107,45 +118,45 @@ export function DayWrapper(props: {
onDayFocus?.(props.day.date, modifiers, e);
};
- const onBlur: FocusEventHandler = (e) => {
+ const onBlur: React.FocusEventHandler = (e) => {
blur();
onDayBlur?.(props.day.date, modifiers, e);
};
- const onMouseEnter: MouseEventHandler = (e) => {
+ const onMouseEnter: React.MouseEventHandler = (e) => {
onDayMouseEnter?.(props.day.date, modifiers, e);
};
- const onMouseLeave: MouseEventHandler = (e) => {
+ const onMouseLeave: React.MouseEventHandler = (e) => {
onDayMouseLeave?.(props.day.date, modifiers, e);
};
- const onPointerEnter: PointerEventHandler = (e) => {
+ const onPointerEnter: React.PointerEventHandler = (e) => {
onDayPointerEnter?.(props.day.date, modifiers, e);
};
- const onPointerLeave: PointerEventHandler = (e) => {
+ const onPointerLeave: React.PointerEventHandler = (e) => {
onDayPointerLeave?.(props.day.date, modifiers, e);
};
- const onTouchCancel: TouchEventHandler = (e) => {
+ const onTouchCancel: React.TouchEventHandler = (e) => {
onDayTouchCancel?.(props.day.date, modifiers, e);
};
- const onTouchEnd: TouchEventHandler = (e) => {
+ const onTouchEnd: React.TouchEventHandler = (e) => {
onDayTouchEnd?.(props.day.date, modifiers, e);
};
- const onTouchMove: TouchEventHandler = (e) => {
+ const onTouchMove: React.TouchEventHandler = (e) => {
onDayTouchMove?.(props.day.date, modifiers, e);
};
- const onTouchStart: TouchEventHandler = (e) => {
+ const onTouchStart: React.TouchEventHandler = (e) => {
onDayTouchStart?.(props.day.date, modifiers, e);
};
- const onKeyUp: KeyboardEventHandler = (e) => {
+ const onKeyUp: React.KeyboardEventHandler = (e) => {
onDayKeyUp?.(props.day.date, modifiers, e);
};
- const onKeyPress: KeyboardEventHandler = (e) => {
+ const onKeyPress: React.KeyboardEventHandler = (e) => {
onDayKeyPress?.(props.day.date, modifiers, e);
};
- const onKeyDown: KeyboardEventHandler = (e) => {
+ const onKeyDown: React.KeyboardEventHandler = (e) => {
switch (e.key) {
case "ArrowLeft":
e.preventDefault();
@@ -171,7 +182,15 @@ export function DayWrapper(props: {
case "Enter":
e.preventDefault();
e.stopPropagation();
- setSelected(props.day.date, modifiers, e);
+ if (mode === "single" && !modifiers.disabled) {
+ single.setSelected(props.day.date, modifiers, e);
+ }
+ if (mode === "multiple" && !modifiers.disabled) {
+ multi.setSelected(props.day.date, modifiers, e);
+ }
+ if (mode === "range" && !modifiers.disabled) {
+ range.setSelected(props.day.date, modifiers, e);
+ }
break;
case "PageUp":
e.preventDefault();
@@ -198,7 +217,7 @@ export function DayWrapper(props: {
};
const isAutoFocusTarget = Boolean(autoFocusTarget?.isEqualTo(props.day));
- const isFocused = Boolean(focusedDay?.isEqualTo(props.day));
+ const isFocused = Boolean(focused?.isEqualTo(props.day));
const style = getStyleForModifiers(modifiers, modifiersStyles, styles);
@@ -211,7 +230,7 @@ export function DayWrapper(props: {
const className = [classNames[UI.Day], ...classNameForModifiers];
if (isFocused) {
- className.push(classNames[DayModifier.focused]);
+ className.push(classNames[DayFlag.focused]);
}
const dayRootProps: DayProps["rootProps"] = {
@@ -242,14 +261,14 @@ export function DayWrapper(props: {
ref: cellRef
};
- useEffect(() => {
+ React.useEffect(() => {
if (!cellRef.current) return; // no element to focus
- if (!focusedDay) return; // no day to focus
- if (!props.day.isEqualTo(focusedDay)) return; // not this day`
+ if (!focused) return; // no day to focus
+ if (!props.day.isEqualTo(focused)) return; // not this day`
if (modifiers.disabled || modifiers.hidden) return; // cannot focus
cellRef.current.focus();
- }, [focusedDay, modifiers.disabled, modifiers.hidden, props.day]);
+ }, [focused, modifiers.disabled, modifiers.hidden, props.day]);
const Day = components?.Day ?? DefaultDay;
diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx
index 52aaa2d4de..71688b9331 100644
--- a/src/components/Dropdown.tsx
+++ b/src/components/Dropdown.tsx
@@ -1,9 +1,9 @@
-import React, { SelectHTMLAttributes } from "react";
+import React, { type SelectHTMLAttributes } from "react";
import type { Month } from "date-fns";
import { UI } from "../UI";
-import { useProps } from "../contexts/props";
+import { usePropsContext } from "../contexts/props";
import { Chevron as DefaultChevron } from "./Chevron";
import { Option as DefaultOption } from "./Option";
@@ -37,7 +37,7 @@ export function Dropdown(
} & Omit, "children">
) {
const { options, rootClassName, className, ...selectProps } = props;
- const { classNames, components } = useProps();
+ const { classNames, components } = usePropsContext();
const cssClassRoot = [classNames[UI.DropdownRoot], rootClassName].join(" ");
const cssClassSelect = [classNames[UI.Dropdown], className].join(" ");
diff --git a/src/components/DropdownNav.tsx b/src/components/DropdownNav.tsx
index f998fd2c51..7731fdb734 100644
--- a/src/components/DropdownNav.tsx
+++ b/src/components/DropdownNav.tsx
@@ -2,7 +2,7 @@ import React from "react";
import { UI } from "../UI";
import type { CalendarMonth } from "../classes";
-import { useProps } from "../contexts/props";
+import { usePropsContext } from "../contexts/props";
import { MonthsDropdown } from "./MonthsDropdown";
import { YearsDropdown } from "./YearsDropdown";
@@ -25,7 +25,7 @@ export function DropdownNav(props: {
/** The index where this month is displayed. */
index: number;
}) {
- const { classNames, styles } = useProps();
+ const { classNames, styles } = usePropsContext();
return (
diff --git a/src/components/Nav.tsx b/src/components/Nav.tsx
index 98f9d869c5..ae084c4bd5 100644
--- a/src/components/Nav.tsx
+++ b/src/components/Nav.tsx
@@ -1,8 +1,8 @@
import React, { MouseEventHandler } from "react";
import { UI } from "../UI";
-import { useCalendar } from "../contexts/calendar";
-import { useProps } from "../contexts/props";
+import { useCalendarContext } from "../contexts/calendar";
+import { usePropsContext } from "../contexts/props";
import { Button as DefaultButton } from "./Button";
import { Chevron as DefaultChevron } from "./Chevron";
@@ -25,9 +25,9 @@ export function Nav() {
id,
onNextClick,
onPrevClick
- } = useProps();
+ } = usePropsContext();
- const calendar = useCalendar();
+ const calendar = useCalendarContext();
const handlePreviousClick = () => {
if (!calendar.previousMonth) return;
diff --git a/src/components/Week.tsx b/src/components/Week.tsx
index 682244eb19..cac07181e8 100644
--- a/src/components/Week.tsx
+++ b/src/components/Week.tsx
@@ -4,7 +4,7 @@ import { getUnixTime } from "date-fns/getUnixTime";
import { UI } from "../UI";
import type { CalendarDay, CalendarWeek } from "../classes";
-import { useProps } from "../contexts/props";
+import { usePropsContext } from "../contexts/props";
import { DayWrapper } from "./DayWrapper";
import { WeekNumber as DefaultWeekNumber } from "./WeekNumber";
@@ -18,7 +18,7 @@ import { WeekNumber as DefaultWeekNumber } from "./WeekNumber";
* @see https://react-day-picker.js.org/advanced-guides/custom-components
*/
export function Week(props: { ["aria-rowindex"]: number; week: CalendarWeek }) {
- const { styles, classNames, showWeekNumber, components } = useProps();
+ const { styles, classNames, showWeekNumber, components } = usePropsContext();
const WeekNumber = components?.WeekNumber ?? DefaultWeekNumber;
diff --git a/src/components/WeekNumber.tsx b/src/components/WeekNumber.tsx
index 1087ba8888..4f75b0d431 100644
--- a/src/components/WeekNumber.tsx
+++ b/src/components/WeekNumber.tsx
@@ -2,7 +2,7 @@ import React from "react";
import { UI, WeekNumberFlag } from "../UI";
import type { CalendarWeek } from "../classes";
-import { useProps } from "../contexts/props";
+import { usePropsContext } from "../contexts/props";
/**
* Render the cell with the number of the week.
@@ -20,7 +20,7 @@ export function WeekNumber(props: { week: CalendarWeek }) {
locale,
styles,
onWeekNumberClick
- } = useProps();
+ } = usePropsContext();
const isInteractive = Boolean(onWeekNumberClick);
@@ -31,7 +31,7 @@ export function WeekNumber(props: { week: CalendarWeek }) {
aria-label={labelWeekNumber(props.week.weekNumber, { locale })}
className={[
classNames[UI.WeekNumber],
- isInteractive ? classNames[WeekNumberFlag.isInteractive] : ""
+ isInteractive ? classNames[WeekNumberFlag.week_number_interactive] : ""
].join(" ")}
style={styles?.[UI.WeekNumber]}
tabIndex={isInteractive ? 0 : undefined}
diff --git a/src/components/Weekday.tsx b/src/components/Weekday.tsx
index 710aa21093..7121d6b6e0 100644
--- a/src/components/Weekday.tsx
+++ b/src/components/Weekday.tsx
@@ -1,7 +1,7 @@
import React from "react";
import { UI } from "../UI";
-import { useProps } from "../contexts/props";
+import { usePropsContext } from "../contexts/props";
/**
* Render the column header with the weekday name (e.g. "Mo", "Tu", etc.).
@@ -23,7 +23,7 @@ export function Weekday(props: {
locale,
hideWeekdayRow,
styles
- } = useProps();
+ } = usePropsContext();
return (
= (e) => {
const month = setYear(
@@ -46,7 +46,7 @@ export function YearsDropdown(props: {
aria-label={labelYearDropdown()}
disabled={Boolean(disableNavigation)}
rootClassName={classNames[UI.YearsDropdown]}
- options={dropdown.years}
+ options={dropdownOptions.years}
value={props.month.date.getFullYear()}
onChange={handleChange}
/>
diff --git a/src/contexts/calendar.test.tsx b/src/contexts/calendar.test.ts
similarity index 77%
rename from src/contexts/calendar.test.tsx
rename to src/contexts/calendar.test.ts
index cb5b5231d1..4e74c748be 100644
--- a/src/contexts/calendar.test.tsx
+++ b/src/contexts/calendar.test.ts
@@ -1,18 +1,16 @@
-import React from "react";
-
import { renderHook } from "@/test/renderHook";
-import { useCalendar } from "./calendar";
+import { useCalendarContext } from "./calendar";
it("should return the next month", () => {
- const { result } = renderHook(useCalendar, {
+ const { result } = renderHook(useCalendarContext, {
month: new Date(2020, 0, 1)
});
expect(result.current.nextMonth).toEqual(new Date(2020, 1, 1));
});
it("should return the previous month", () => {
- const { result } = renderHook(useCalendar, {
+ const { result } = renderHook(useCalendarContext, {
month: new Date(2020, 0, 1)
});
expect(result.current.previousMonth).toEqual(new Date(2019, 11, 1));
@@ -20,14 +18,14 @@ it("should return the previous month", () => {
describe("dropdown", () => {
it("should return undefined if no fromMonth is provided", () => {
- const { result } = renderHook(useCalendar, {
+ const { result } = renderHook(useCalendarContext, {
fromMonth: undefined
});
expect(result.current.dropdownOptions.months).toBeUndefined();
});
it("should return undefined if no toMonth is provided", () => {
- const { result } = renderHook(useCalendar, { toMonth: undefined });
+ const { result } = renderHook(useCalendarContext, { toMonth: undefined });
expect(result.current.dropdownOptions.months).toBeUndefined();
});
@@ -36,7 +34,7 @@ describe("dropdown", () => {
fromMonth: new Date(2020, 1, 1),
toMonth: new Date(2023, 2, 1)
};
- const { result } = renderHook(useCalendar, dayPicker);
+ const { result } = renderHook(useCalendarContext, dayPicker);
const months = result.current.dropdownOptions.months;
expect(months).toHaveLength(12);
expect(months?.[0]).toEqual({
diff --git a/src/contexts/calendar.tsx b/src/contexts/calendar.tsx
index e668a0a8ac..aeaf6142b9 100644
--- a/src/contexts/calendar.tsx
+++ b/src/contexts/calendar.tsx
@@ -1,32 +1,35 @@
-import React from "react";
-import { createContext, type ReactNode, useContext } from "react";
+import React, { type ReactElement, createContext, useContext } from "react";
import { startOfMonth } from "date-fns/startOfMonth";
-import type { CalendarWeek, CalendarMonth, CalendarDay } from "../classes";
-import { DropdownOption } from "../components/Dropdown";
+import type { CalendarWeek, CalendarDay, CalendarMonth } from "../classes";
+import type { DropdownOption } from "../components/Dropdown";
import { getDates } from "../helpers/getDates";
import { getDays } from "../helpers/getDays";
import { getDisplayMonths } from "../helpers/getDisplayMonths";
import { getDropdownMonths } from "../helpers/getDropdownMonths";
import { getDropdownYears } from "../helpers/getDropdownYears";
+import { getInitialMonth } from "../helpers/getInitialMonth";
import { getMonths } from "../helpers/getMonths";
import { getNextMonth } from "../helpers/getNextMonth";
import { getPreviousMonth } from "../helpers/getPreviousMonth";
-import { getStartMonth } from "../helpers/getStartMonth";
import { getWeeks } from "../helpers/getWeeks";
import { useControlledValue } from "../helpers/useControlledValue";
-import { useProps } from "./props";
+import { usePropsContext } from "./props";
+
+/** @private */
+export const CalendarContext = createContext(
+ undefined
+);
/**
- * Share the calendar state and navigation methods across the components.
- *
- * Access the calendar context using the {@link useCalendar} hook.
+ * Share the calendar state and navigation methods across the components.\
*
- * @group Contexts
+ * Access the calendar context using the {@link useCalendarContext} hook.
*/
-export interface CalendarContext {
+export type CalendarContextValue = {
+ today: Date;
/** All the unique dates displayed to the calendar. */
dates: Date[];
/**
@@ -40,16 +43,11 @@ export interface CalendarContext {
/** The months displayed in the calendar. */
months: CalendarMonth[];
/**
- * The month displayed as first the calendar. When
- * {@link PropsBase.numberOfMonths} is greater than `1`, it is the first of the
- * displayed months.
+ * The month displayed as first the calendar. When `numberOfMonths` is greater
+ * than `1`, it is the first of the displayed months.
*/
firstMonth: Date;
- /**
- * The month displayed as last the calendar. When
- * {@link PropsBase.numberOfMonths} is greater than `1`, it is the last of the
- * displayed months.
- */
+ /** The month displayed as last the calendar. */
lastMonth: Date;
/** The next month to display. */
nextMonth: Date | undefined;
@@ -62,10 +60,16 @@ export interface CalendarContext {
/** The options to use in the years dropdown. */
years: DropdownOption[] | undefined;
};
+
+ /** Set the first month displayed in the calendar. */
+ setFirstMonth: (date: Date) => void;
+
/**
- * Navigate to the specified month. Will fire the
- * {@link PropsBase.onMonthChange} callback.
+ * Whether the calendar is interactive, i.e. DayPicker has a selection `mode`
+ * set or the `onDayClick` prop is set.
*/
+ isInteractive: boolean;
+ /** Navigate to the specified month. Will fire the `onMonthChange` callback. */
goToMonth: (month: Date) => void;
/** Navigate to the next month. */
goToNextMonth: () => void;
@@ -83,30 +87,21 @@ export interface CalendarContext {
goToDay: (day: CalendarDay) => void;
/** Whether the given date is included in the displayed months. */
isDayDisplayed: (day: CalendarDay) => boolean;
+};
- /**
- * Whether the calendar is interactive, i.e. DayPicker has a selection `mode`
- * set or the `onDayClick` prop is set.
- */
- isInteractive: boolean;
-}
-
-const calendarContext = createContext(undefined);
+function useCalendar(): CalendarContextValue {
+ const props = usePropsContext();
-/** @private */
-export function CalendarProvider(providerProps: { children?: ReactNode }) {
- const props = useProps();
-
- const startMonth = getStartMonth(props);
+ const initialDisplayMonth = getInitialMonth(props);
// The first month displayed in the calendar
- const [firstMonth, setFirstMonth] = useControlledValue(
- startMonth,
+ const [firstDisplayedMonth, setFirstMonth] = useControlledValue(
+ initialDisplayMonth,
props.month ? startOfMonth(props.month) : undefined
);
/** An array of the months displayed in the calendar. */
- const displayMonths = getDisplayMonths(firstMonth, props);
+ const displayMonths = getDisplayMonths(firstDisplayedMonth, props);
/** The last month displayed in the calendar. */
const lastMonth = displayMonths[displayMonths.length - 1];
@@ -123,8 +118,13 @@ export function CalendarProvider(providerProps: { children?: ReactNode }) {
/** An array of the Days displayed in the calendar. */
const days = getDays(months);
- const nextMonth = getNextMonth(firstMonth, props);
- const previousMonth = getPreviousMonth(firstMonth, props);
+ const previousMonth = getPreviousMonth(firstDisplayedMonth, props);
+ const nextMonth = getNextMonth(firstDisplayedMonth, props);
+
+ const isInteractive =
+ props.mode !== undefined || props.onDayClick !== undefined;
+
+ const { disableNavigation, onMonthChange, startMonth, endMonth } = props;
function isDayDisplayed(day: CalendarDay) {
return weeks.some((week: CalendarWeek) => {
@@ -135,20 +135,20 @@ export function CalendarProvider(providerProps: { children?: ReactNode }) {
}
function goToMonth(date: Date) {
- if (props.disableNavigation) {
+ if (disableNavigation) {
return;
}
let newMonth = startOfMonth(date);
// if month is before startMonth, use the first month instead
- if (props.startMonth && newMonth < startOfMonth(props.startMonth)) {
- newMonth = startOfMonth(props.startMonth);
+ if (startMonth && newMonth < startOfMonth(startMonth)) {
+ newMonth = startOfMonth(startMonth);
}
// if month is after endMonth, use the last month instead
- if (props.endMonth && newMonth > startOfMonth(props.endMonth)) {
- newMonth = startOfMonth(props.endMonth);
+ if (endMonth && newMonth > startOfMonth(endMonth)) {
+ newMonth = startOfMonth(endMonth);
}
setFirstMonth(newMonth);
- props.onMonthChange?.(newMonth);
+ onMonthChange?.(newMonth);
}
function goToDay(day: CalendarDay) {
@@ -175,53 +175,61 @@ export function CalendarProvider(providerProps: { children?: ReactNode }) {
return previousMonth ? goToMonth(previousMonth) : undefined;
}
- const isInteractive =
- props.mode !== "default" || props.onDayClick !== undefined;
-
- const calendar: CalendarContext = {
+ const calendarContextValue: CalendarContextValue = {
dates,
months,
weeks,
days,
- firstMonth,
+ today: props.today,
+
+ firstMonth: firstDisplayedMonth,
lastMonth,
previousMonth,
nextMonth,
- goToMonth,
- goToNextMonth,
- goToPreviousMonth,
- goToDay,
+ setFirstMonth,
isInteractive,
- isDayDisplayed,
dropdownOptions: {
- months: getDropdownMonths(props, firstMonth.getFullYear()),
- years: getDropdownYears(props, lastMonth.getMonth())
- }
+ months: getDropdownMonths(firstDisplayedMonth, props),
+ years: getDropdownYears(firstDisplayedMonth, props)
+ },
+ isDayDisplayed,
+ goToMonth,
+ goToDay,
+ goToNextMonth,
+ goToPreviousMonth
};
+ return calendarContextValue;
+}
+
+/** @private */
+export function CalendarContextProvider(props: { children: ReactElement }) {
+ const calendarContextValue = useCalendar();
return (
-
- {providerProps.children}
-
+
+ {props.children}
+
);
}
/**
- * Return the calendar state and navigation methods to navigate the calendar.
+ * Access to the props passed to `DayPicker`, with some meaningful defaults.
*
* Use this hook from the custom components passed via the `components` prop.
*
- * @group Hooks
+ * @group Contexts
* @see https://react-day-picker.js.org/advanced-guides/custom-components
*/
-export function useCalendar(): CalendarContext {
- const context = useContext(calendarContext);
- if (!context)
- throw new Error(`useCalendar must be used within a CalendarProvider.`);
-
- return context;
+export function useCalendarContext() {
+ const calendarContext = useContext(CalendarContext);
+ if (!calendarContext) {
+ throw new Error(
+ "useCalendarContext() must be used within a CalendarContextProvider"
+ );
+ }
+ return calendarContext;
}
diff --git a/src/contexts/focus.test.tsx b/src/contexts/focus.test.tsx
index dcf9084868..27b4771736 100644
--- a/src/contexts/focus.test.tsx
+++ b/src/contexts/focus.test.tsx
@@ -1,8 +1,6 @@
-import { gridcell } from "@/test/elements";
import { renderHook } from "@/test/renderHook";
-import { user } from "@/test/user";
-import { useFocus } from "./focus";
+import { useFocusContext } from "./focus";
const month = new Date(2020, 0, 1);
const today = new Date(2020, 0, 14);
@@ -10,13 +8,13 @@ const today = new Date(2020, 0, 14);
describe("autoFocusTarget", () => {
describe("when not in interactive", () => {
test("the auto focus target is undefined", () => {
- const { result } = renderHook(useFocus, { month, today });
+ const { result } = renderHook(useFocusContext, { month, today });
expect(result.current.autoFocusTarget).toBeUndefined();
});
});
describe("when in selection mode", () => {
test("the autofocus target should be today", () => {
- const { result } = renderHook(useFocus, {
+ const { result } = renderHook(useFocusContext, {
month,
today,
mode: "single"
@@ -27,10 +25,10 @@ describe("autoFocusTarget", () => {
});
describe("if today is disabled", () => {
test("the autofocus target should be the first focusable day (the 1st of month)", () => {
- const { result } = renderHook(useFocus, {
+ const { result } = renderHook(useFocusContext, {
month,
today,
- mode: "multiple",
+ mode: "single",
disabled: [today]
});
expect(result.current.autoFocusTarget?.date).toEqual(month);
diff --git a/src/contexts/focus.tsx b/src/contexts/focus.tsx
index f81a8d478d..75812dd0b5 100644
--- a/src/contexts/focus.tsx
+++ b/src/contexts/focus.tsx
@@ -1,39 +1,25 @@
import React, {
- createContext,
ReactNode,
+ createContext,
useContext,
useEffect,
useState
} from "react";
-import { DayModifier } from "../UI";
+import { DayFlag } from "../UI";
import type { CalendarDay } from "../classes";
import { getNextFocus } from "../helpers/getNextFocus";
+import type { MoveFocusBy, MoveFocusDir, Mode } from "../types";
-import { useCalendar } from "./calendar";
-import { useModifiers } from "./modifiers";
-import { useProps } from "./props";
-
-export type MoveFocusBy =
- | "day"
- | "week"
- | "startOfWeek"
- | "endOfWeek"
- | "month"
- | "year";
+import { useCalendarContext } from "./calendar";
+import { useModifiersContext } from "./modifiers";
+import { usePropsContext } from "./props";
-export type MoveFocusDir = "after" | "before";
+const FocusContext = createContext(undefined);
-/**
- * Share the focused day and the methods to move the focus.
- *
- * Access this context from the {@link useFocus} hook.
- *
- * @group Contexts
- */
-export interface FocusContext {
+export type FocusContextValue = {
/** The date that is currently focused. */
- focusedDay: CalendarDay | undefined;
+ focused: CalendarDay | undefined;
/**
* The date that is target of the focus when tabbing into the month grid. The
* focus target is the selected date first, then the today date, then the
@@ -41,8 +27,11 @@ export interface FocusContext {
*/
autoFocusTarget: CalendarDay | undefined;
initiallyFocused: boolean;
- /** Focus a date. */
+ /** Focus the given day. */
focus: (day: CalendarDay | undefined) => void;
+ /** Set the last focused day. */
+ setLastFocused: (day: CalendarDay | undefined) => void;
+
/** Blur the focused day. */
blur: () => void;
/** Focus the day after the focused day. */
@@ -65,31 +54,38 @@ export interface FocusContext {
focusStartOfWeek: () => void;
/* Focus the day at the end of the week of focused day. */
focusEndOfWeek: () => void;
-}
+};
-const focusContext = createContext(undefined);
+/**
+ * Share the focused day and the methods to move the focus.
+ *
+ * Use this hook from the custom components passed via the `components` prop.
+ *
+ * @group Contexts
+ * @see https://react-day-picker.js.org/advanced-guides/custom-components
+ */
-/** @private */
-export function FocusProvider({
- children
-}: {
- children: ReactNode;
-}): JSX.Element {
- const { goToDay, isDayDisplayed, days, isInteractive } = useCalendar();
+function useFocus(): FocusContextValue {
+ const { goToDay, isDayDisplayed, isInteractive } = useCalendarContext();
- const { autoFocus = false, ...props } = useProps();
- const { calendarModifiers, getModifiers } = useModifiers();
+ const props = usePropsContext();
+ const { autoFocus } = props;
+ const {
+ dayFlags: internal,
+ selectionStates: selection,
+ getModifiers
+ } = useModifiersContext();
- const [focused, setFocused] = useState();
+ const [focused, focus] = useState();
const [lastFocused, setLastFocused] = useState();
const [initiallyFocused, setInitiallyFocused] = useState(false);
- const today = calendarModifiers.today[0];
+ const today = internal.today[0];
let autoFocusTarget: CalendarDay | undefined;
const isValidFocusTarget = (day: CalendarDay) => {
- return isDayDisplayed(day) && !getModifiers(day)[DayModifier.disabled];
+ return isDayDisplayed(day) && !getModifiers(day)[DayFlag.disabled];
};
if (isInteractive) {
@@ -98,14 +94,14 @@ export function FocusProvider({
} else if (lastFocused) {
autoFocusTarget = lastFocused;
} else if (
- calendarModifiers.selected[0] &&
- isValidFocusTarget(calendarModifiers.selected[0])
+ selection.selected[0] &&
+ isValidFocusTarget(selection.selected[0])
) {
- autoFocusTarget = calendarModifiers.selected[0];
+ autoFocusTarget = selection.selected[0];
} else if (today && isValidFocusTarget(today)) {
autoFocusTarget = today;
- } else if (calendarModifiers.focusable[0]) {
- autoFocusTarget = calendarModifiers.focusable[0];
+ } else if (internal.focusable[0]) {
+ autoFocusTarget = internal.focusable[0];
}
}
@@ -114,14 +110,15 @@ export function FocusProvider({
if (!autoFocus) return;
if (!autoFocusTarget) return;
if (!initiallyFocused) return;
+ // TODO: bug here?
setInitiallyFocused(true);
- setFocused(autoFocusTarget);
+ focus(autoFocusTarget);
}, [autoFocus, autoFocusTarget, focused, initiallyFocused]);
- function blur() {
+ const blur = () => {
setLastFocused(focused);
- setFocused(undefined);
- }
+ focus(undefined);
+ };
function moveFocus(moveBy: MoveFocusBy, moveDir: MoveFocusDir) {
if (!focused) return;
@@ -129,15 +126,16 @@ export function FocusProvider({
if (!nextFocus) return;
goToDay(nextFocus);
- setFocused(nextFocus);
+ focus(nextFocus);
}
- const value: FocusContext = {
+ const contextValue: FocusContextValue = {
autoFocusTarget,
initiallyFocused,
- focusedDay: focused,
+ focus,
+ focused,
+ setLastFocused,
blur,
- focus: setFocused,
focusDayAfter: () => moveFocus("day", "after"),
focusDayBefore: () => moveFocus("day", "before"),
focusWeekAfter: () => moveFocus("week", "after"),
@@ -150,23 +148,30 @@ export function FocusProvider({
focusEndOfWeek: () => moveFocus("endOfWeek", "after")
};
+ return contextValue;
+}
+
+/** @private */
+export function FocusContextProvider<
+ ModeType extends Mode | undefined = undefined,
+ IsRequired extends boolean = false
+>({ children }: { children: ReactNode }) {
+ const focusContextValue = useFocus();
+
return (
- {children}
+
+ {children}
+
);
}
-/**
- * Share the focused day and the methods to move the focus.
- *
- * Use this hook from the custom components passed via the `components` prop.
- *
- * @group Hooks
- * @see https://react-day-picker.js.org/advanced-guides/custom-components
- */
-export function useFocus(): FocusContext {
- const context = useContext(focusContext);
- if (!context) {
- throw new Error("useFocus must be used within a FocusProvider");
+/** @group Contexts */
+export function useFocusContext() {
+ const propsContext = useContext(FocusContext);
+ if (!propsContext) {
+ throw new Error(
+ "useFocusContext() must be used within a FocusContextProvider"
+ );
}
- return context;
+ return propsContext;
}
diff --git a/src/contexts/modifiers.tsx b/src/contexts/modifiers.tsx
index 4c9b8ae790..9451f6b51f 100644
--- a/src/contexts/modifiers.tsx
+++ b/src/contexts/modifiers.tsx
@@ -1,40 +1,43 @@
-import React, { createContext, useContext } from "react";
-import type { ReactNode } from "react";
+import React, { type ReactElement, createContext, useContext } from "react";
import { isSameDay } from "date-fns/isSameDay";
import { isSameMonth } from "date-fns/isSameMonth";
-import type { CalendarDay } from "../classes";
+import { DayFlag, SelectionState } from "../UI";
+import { CalendarDay } from "../classes";
+import { useMultiContext } from "../selection/multi";
+import { useRangeContext } from "../selection/range";
+import { useSingleContext } from "../selection/single";
import type {
- DayModifiers,
- InternalModifier,
- CalendarModifiers
+ CustomModifiers,
+ DayFlags,
+ Modifiers,
+ SelectionStates
} from "../types";
+import { isDateInRange } from "../utils";
import { dateMatchModifiers } from "../utils/dateMatchModifiers";
-import { useCalendar } from "./calendar";
-import { useProps } from "./props";
-import { useSelection } from "./selection";
-
-/**
- * Holds all the modifiers used in the the calendar.
- *
- * Use the Modifiers context in custom component by calling the
- * {@link useModifiers} hook.
- *
- * @group Contexts
- */
-export interface ModifiersContext {
- /** Return the modifiers of the specified day. */
- getModifiers: (day: CalendarDay) => DayModifiers;
- /** A map of all the modifiers used by the calendar. */
- calendarModifiers: CalendarModifiers;
-}
-
-const modifiersContext = createContext(undefined);
+import { useCalendarContext } from "./calendar";
+import { usePropsContext } from "./props";
/** @private */
-export function ModifiersProvider({ children }: { children: ReactNode }) {
+export const ModifiersContext = createContext<
+ ModifiersContextValue | undefined
+>(undefined);
+
+/** Maps of all the modifiers with the calendar days. */
+export type ModifiersContextValue = {
+ /** List the days with custom modifiers passed via the `modifiers` prop. */
+ customModifiers: Record;
+ /** List the days with the internal modifiers. */
+ dayFlags: Record;
+ /** List the days with selection modifiers. */
+ selectionStates: Record;
+ /** Get the modifiers for a given day. */
+ getModifiers: (day: CalendarDay) => Modifiers;
+};
+
+function useModifiers(): ModifiersContextValue {
const {
disabled,
hidden,
@@ -43,27 +46,31 @@ export function ModifiersProvider({ children }: { children: ReactNode }) {
onDayClick,
showOutsideDays,
today
- } = useProps();
- const calendar = useCalendar();
- const selection = useSelection();
-
- /** Modifiers that are set internally. */
- const internal: Record = {
- focused: [],
- outside: [],
- disabled: [],
- hidden: [],
- today: [],
- focusable: [],
- selected: [],
- range_start: [],
- range_middle: [],
- range_end: []
+ } = usePropsContext();
+
+ const calendar = useCalendarContext();
+ const single = useSingleContext();
+ const multi = useMultiContext();
+ const range = useRangeContext();
+
+ const internal: Record = {
+ [DayFlag.focused]: [],
+ [DayFlag.outside]: [],
+ [DayFlag.disabled]: [],
+ [DayFlag.hidden]: [],
+ [DayFlag.today]: [],
+ [DayFlag.focusable]: []
};
- /** Custom modifiers that are coming from the `modifiers` props */
const custom: Record = {};
+ const selection: Record = {
+ [SelectionState.range_end]: [],
+ [SelectionState.range_middle]: [],
+ [SelectionState.range_start]: [],
+ [SelectionState.selected]: []
+ };
+
for (const day of calendar.days) {
const { date, displayMonth } = day;
@@ -71,39 +78,52 @@ export function ModifiersProvider({ children }: { children: ReactNode }) {
const isDisabled = Boolean(disabled && dateMatchModifiers(date, disabled));
- const isSelected =
- !isDisabled &&
- (selection.isSelected(date) ||
- Boolean(
- modifiers?.selected && dateMatchModifiers(date, modifiers.selected)
- ));
-
const isHidden =
Boolean(hidden && dateMatchModifiers(date, hidden)) ||
(!showOutsideDays && isOutside);
- const isInteractive =
- mode !== "default" || (mode === "default" && onDayClick !== undefined);
+ const isElementInteractive = mode || onDayClick !== undefined;
- const isFocusable = isInteractive && !isDisabled && !isHidden;
+ const isFocusable = isElementInteractive && !isDisabled && !isHidden;
const isToday = isSameDay(date, today);
- const isStartOfRange = selection.isStartOfRange(date);
- const isEndOfRange = selection.isEndOfRange(date);
- const isMiddleOfRange = selection.isMiddleOfRange(date);
-
if (isOutside) internal.outside.push(day);
if (isDisabled) internal.disabled.push(day);
if (isHidden) internal.hidden.push(day);
if (isFocusable) internal.focusable.push(day);
- if (isSelected) internal.selected.push(day);
if (isToday) internal.today.push(day);
- if (isStartOfRange) internal.range_start.push(day);
- if (isEndOfRange) internal.range_end.push(day);
- if (isMiddleOfRange) internal.range_middle.push(day);
- // Now add custom modifiers
+ // Add the selection modifiers
+ if (mode === "single" && !isDisabled) {
+ if (single.isSelected(day.date)) {
+ selection[SelectionState.selected].push(day);
+ }
+ }
+ if (mode === "multiple" && !isDisabled) {
+ if (multi.isSelected(day.date)) {
+ selection[SelectionState.selected].push(day);
+ }
+ }
+
+ if (mode === "range" && !isDisabled) {
+ if (range.isSelected(day.date)) {
+ selection[SelectionState.selected].push(day);
+ if (range.selected?.from && isSameDay(day.date, range.selected.from)) {
+ if (range.selected?.to)
+ selection[SelectionState.range_start].push(day);
+ } else if (
+ range.selected?.to &&
+ isSameDay(day.date, range.selected.to)
+ ) {
+ selection[SelectionState.range_end].push(day);
+ } else if (range.selected && isDateInRange(day.date, range.selected)) {
+ selection[SelectionState.range_middle].push(day);
+ }
+ }
+ }
+
+ // Add custom modifiers
if (modifiers) {
Object.keys(modifiers).forEach((name) => {
const modifierValue = modifiers?.[name];
@@ -121,57 +141,79 @@ export function ModifiersProvider({ children }: { children: ReactNode }) {
}
const getModifiers = (day: CalendarDay) => {
- const modifiers: DayModifiers = {
- focused: false,
- disabled: false,
- focusable: false,
- hidden: false,
- outside: false,
- range_end: false,
- range_middle: false,
- range_start: false,
- selected: false,
- today: false
+ // Initialize all the modifiers to false
+ const dayFlags: DayFlags = {
+ [DayFlag.focused]: false,
+ [DayFlag.disabled]: false,
+ [DayFlag.focusable]: false,
+ [DayFlag.hidden]: false,
+ [DayFlag.outside]: false,
+ [DayFlag.today]: false
};
+ const selectionStates: SelectionStates = {
+ [SelectionState.range_end]: false,
+ [SelectionState.range_middle]: false,
+ [SelectionState.range_start]: false,
+ [SelectionState.selected]: false
+ };
+ const customModifiers: CustomModifiers = {};
+ // Find the modifiers for the given day
for (const name in internal) {
- const daysWithModifier = internal[name as InternalModifier];
- modifiers[name] = daysWithModifier.some((d) => d === day);
+ const days = internal[name as DayFlag];
+ dayFlags[name as DayFlag] = days.some((d) => d === day);
+ }
+ for (const name in selection) {
+ const days = selection[name as SelectionState];
+ selectionStates[name as SelectionState] = days.some((d) => d === day);
}
-
for (const name in custom) {
- // This will override the internal modifiers with the same name, as intended
- modifiers[name] = custom[name].some((d) => d === day);
+ customModifiers[name] = custom[name].some((d) => d === day);
}
- return modifiers;
- };
- const calendarModifiers: CalendarModifiers = { ...internal, ...custom };
+ return {
+ ...selectionStates,
+ ...dayFlags,
+ // custom modifiers should override all the previous ones
+ ...customModifiers
+ };
+ };
- const value: ModifiersContext = {
- calendarModifiers,
+ return {
+ dayFlags: internal,
+ customModifiers: custom,
+ selectionStates: selection,
getModifiers
};
-
- return (
-
- {children}
-
- );
}
/**
- * Access the modifiers used by the calendar.
- *
- * Use this hook from the custom components passed via the `components` prop.
+ * Provide the shared props to the DayPicker components. Must be used as root of
+ * the other providers.
*
- * @group Hooks
- * @see https://react-day-picker.js.org/advanced-guides/custom-components
+ * @private
*/
-export function useModifiers(): ModifiersContext {
- const context = useContext(modifiersContext);
- if (!context)
- throw new Error(`useProps must be used within a PropsProvider.`);
+export function ModifiersContextProvider({
+ children
+}: {
+ children: ReactElement;
+}) {
+ const modifiers = useModifiers();
+
+ return (
+
+ {children}
+
+ );
+}
- return context;
+/** @group Contexts */
+export function useModifiersContext() {
+ const modifiersContext = useContext(ModifiersContext);
+ if (!modifiersContext) {
+ throw new Error(
+ "useModifiersContext() must be used within a ModifiersContextProvider"
+ );
+ }
+ return modifiersContext;
}
diff --git a/src/contexts/props.tsx b/src/contexts/props.tsx
index 93563fa45e..9fe016cdb1 100644
--- a/src/contexts/props.tsx
+++ b/src/contexts/props.tsx
@@ -1,129 +1,125 @@
-import React, {
- createContext,
- PropsWithChildren,
- useContext,
- useId
-} from "react";
+import React from "react";
+import * as customComponents from "../components/custom-components";
import { getDataAttributes } from "../helpers/getDataAttributes";
import { getDefaultClassNames } from "../helpers/getDefaultClassNames";
import { getFormatters } from "../helpers/getFormatters";
import { getStartEndMonths } from "../helpers/getStartEndMonths";
import * as defaultLabels from "../labels";
-import { V9DeprecatedProps } from "../types";
import type {
ClassNames,
+ CustomComponents,
DataAttributes,
- DayPickerProps,
Formatters,
Labels,
Mode,
- PropsBase,
- Selected,
- SelectHandler
+ DayPickerProps
} from "../types";
+const PropsContext = React.createContext(
+ undefined
+);
+
/**
* Holds the props passed to the DayPicker component, with some optional props
* set to meaningful defaults.
*
- * Access this context using the {@link useProps} hook.
- *
- * @template T - The {@link Mode | selection mode}. Defaults to `"default"`.
- * @template R - Whether the selection is required. Defaults to `false`.
- * @group Contexts
+ * Access this context using the {@link usePropsContext} hook.
*/
-export interface PropsContext<
- T extends Mode = "default",
- R extends boolean = false
-> extends Omit {
+export type PropsContextValue = DayPickerProps & {
+ /** The mode of the selection. */
+ mode: Mode | undefined;
+ /** The class names to add to the UI. */
classNames: ClassNames;
- /** The `data-*` attributes passed to ``. */
+ /** The unique ID of the DayPicker instance. */
+ id: string;
+ /** The data attributes to add to the calendar. */
dataAttributes: DataAttributes;
- endMonth: Date | undefined;
+ /** The components used in the UI. */
+ components: CustomComponents;
+ /** The formatters used in the UI. */
formatters: Formatters;
- id: string;
+ /** The labels used in the UI. */
labels: Labels;
- max: number | undefined;
- min: number | undefined;
- mode: T;
+ /** The number of months displayed in the calendar. */
numberOfMonths: number;
- required: boolean;
- startMonth: Date | undefined;
+ /** The date of today. */
today: Date;
- /** The currently selected value. */
- selected: Selected | undefined;
- /** The default selected value. */
- defaultSelected: Selected | undefined;
- /** The function that handles the day selection. */
- onSelect: SelectHandler | undefined;
-}
+ /** The month where the navigation starts. */
+ startMonth: Date | undefined;
+ /** The month where the navigation ends. */
+ endMonth: Date | undefined;
+};
+
+function useProps(initialProps: DayPickerProps) {
+ const reactId = React.useId();
-const propsContext = createContext | null>(null);
+ const { startMonth, endMonth } = getStartEndMonths(initialProps);
+
+ const propsContext: PropsContextValue = {
+ ...initialProps,
+ startMonth,
+ endMonth,
+ classNames: {
+ ...getDefaultClassNames(),
+ ...initialProps.classNames
+ },
+ components: {
+ ...customComponents,
+ ...initialProps.components
+ },
+ dataAttributes: getDataAttributes(initialProps),
+ formatters: getFormatters(initialProps.formatters),
+ id: initialProps.id ?? reactId,
+ labels: {
+ ...defaultLabels,
+ ...initialProps.labels
+ },
+ numberOfMonths: initialProps.numberOfMonths ?? 1,
+ today: initialProps.today ?? new Date()
+ };
+
+ return propsContext;
+}
/**
- * Provide the props to the DayPicker components. Must be used as root of the
- * other providers.
+ * Provide the shared props to the DayPicker components. Must be used as root of
+ * the other providers.
*
* @private
*/
-export function PropsProvider(
- props: PropsWithChildren>
-) {
- const reactId = useId();
- const { startMonth, endMonth } = getStartEndMonths(props);
- const {
- children,
- // Remove deprecated props
- fromDate,
- fromMonth,
- fromYear,
- toDate,
- toMonth,
- toYear,
- ...restProps
- } = props;
-
- const context = {
- ...restProps,
- classNames: { ...getDefaultClassNames(), ...restProps.classNames },
- dataAttributes: getDataAttributes(props),
- endMonth,
- formatters: getFormatters(props.formatters),
- id: props.id ?? reactId,
- labels: { ...defaultLabels, ...restProps.labels },
- max: "max" in props ? props.max ?? undefined : undefined,
- min: "min" in props ? props.min ?? undefined : undefined,
- mode: props.mode ?? ("default" as Mode),
- numberOfMonths: props.numberOfMonths ?? 1,
- required: "required" in props ? props.required ?? false : false,
- startMonth,
- today: props.today ?? new Date(),
- selected: "selected" in props ? props.selected : undefined,
- defaultSelected:
- "defaultSelected" in props ? props.defaultSelected : undefined,
- onSelect: "onSelect" in props ? props.onSelect : undefined
- };
+export function PropsContextProvider<
+ ModeType extends Mode | undefined = undefined,
+ IsRequired extends boolean = false
+>({
+ initialProps,
+ children
+}: React.PropsWithChildren<{
+ initialProps: DayPickerProps;
+}>) {
+ const propsContextValue = useProps(initialProps);
return (
- {children}
+
+ {children}
+
);
}
/**
- * Access to the props passed to DayPicker.
+ * Access to the props passed to `DayPicker`, with some meaningful defaults.
*
* Use this hook from the custom components passed via the `components` prop.
*
- * @group Hooks
+ * @group Contexts
* @see https://react-day-picker.js.org/advanced-guides/custom-components
*/
-export function useProps() {
- const context = useContext(propsContext);
- if (!context) {
+export function usePropsContext() {
+ const propsContext = React.useContext(PropsContext);
+ if (!propsContext) {
throw new Error(
- "useDayPicker() must be used within a ``."
+ "usePropsContext() must be used within a PropsContextProvider"
);
}
- return context;
+ return propsContext;
}
diff --git a/src/contexts/providers.tsx b/src/contexts/providers.tsx
new file mode 100644
index 0000000000..d63e3cd6c8
--- /dev/null
+++ b/src/contexts/providers.tsx
@@ -0,0 +1,48 @@
+import React from "react";
+import type { PropsWithChildren } from "react";
+
+import { MultiProvider } from "../selection/multi";
+import { RangeProvider } from "../selection/range";
+import { SingleProvider } from "../selection/single";
+import type {
+ DayPickerProps,
+ PropsMulti,
+ PropsRange,
+ PropsSingle
+} from "../types";
+
+import { CalendarContextProvider } from "./calendar";
+import { FocusContextProvider } from "./focus";
+import { ModifiersContextProvider } from "./modifiers";
+import { PropsContextProvider, usePropsContext } from "./props";
+
+export function SelectionProviders({ children }: PropsWithChildren) {
+ const props = usePropsContext();
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+/**
+ * Provide the value for all the contexts used by DayPicker.
+ *
+ * @private
+ */
+export function ContextProviders(props: PropsWithChildren) {
+ const { children, ...initialProps } = props;
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/src/contexts/root.tsx b/src/contexts/root.tsx
deleted file mode 100644
index f33c4e7f8a..0000000000
--- a/src/contexts/root.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from "react";
-import type { FunctionComponent, PropsWithChildren } from "react";
-
-import type { DayPickerProps, Mode } from "../types";
-
-import { CalendarProvider } from "./calendar";
-import { FocusProvider } from "./focus";
-import { ModifiersProvider } from "./modifiers";
-import { PropsProvider } from "./props";
-import { SelectionProvider } from "./selection";
-
-/**
- * Provide the value for all the contexts used by DayPicker.
- *
- * @private
- */
-export const ContextProviders: FunctionComponent<
- PropsWithChildren>
-> = (props: PropsWithChildren>) => {
- const { children, ...dayPickerProps } = props;
-
- return (
-
-
-
-
- {children}
-
-
-
-
- );
-};
diff --git a/src/contexts/selection.tsx b/src/contexts/selection.tsx
deleted file mode 100644
index 122df81160..0000000000
--- a/src/contexts/selection.tsx
+++ /dev/null
@@ -1,264 +0,0 @@
-import React, { createContext, useContext } from "react";
-import type { KeyboardEvent, MouseEvent, PropsWithChildren } from "react";
-
-import { addDays } from "date-fns/addDays";
-import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays";
-import { isSameDay } from "date-fns/isSameDay";
-import { subDays } from "date-fns/subDays";
-
-import { useControlledValue } from "../helpers/useControlledValue";
-import type { Mode, DayModifiers, Selected } from "../types";
-import { addToRange } from "../utils/addToRange";
-import { isDateInRange } from "../utils/isDateInRange";
-import { isDateRange } from "../utils/typeguards";
-
-import { useProps } from "./props";
-
-/**
- * Provides access to the currently selected value, allows setting the selected
- * days, and provides methods to check if a day is selected.
- *
- * Access the selection context using the {@link useSelection} hook.
- *
- * @template T - The {@link Mode | selection mode}. Defaults to `"default"`.
- * @template R - Whether the selection is required. Defaults to `false`.
- * @group Contexts
- */
-export interface SelectionContext<
- T extends Mode = "default",
- R extends boolean = false
-> {
- /** The currently selected value. */
- selected: Selected;
- /** Set the selected days. */
- setSelected: (
- date: Date,
- modifiers: DayModifiers,
- e: MouseEvent | KeyboardEvent
- ) => void;
- /** Return `true` if the given day is selected. */
- isSelected: (date: Date) => boolean;
- /**
- * In range selection mode, return `true` if the given day is the start of the
- * range.
- */
- isStartOfRange: (date: Date) => boolean;
- /**
- * In range selection mode, return `true` if the given day is the end of the
- * range.
- */
- isEndOfRange: (date: Date) => boolean;
- /**
- * In range selection mode, return `true` if the given day is in the middle of
- * the range.
- */
- isMiddleOfRange: (date: Date) => boolean;
-}
-
-const contextValue: SelectionContext = {
- selected: undefined,
- setSelected: () => undefined,
- isSelected: () => false,
- isStartOfRange: () => false,
- isEndOfRange: () => false,
- isMiddleOfRange: () => false
-};
-
-const selectionContext =
- createContext>(contextValue);
-
-/** @private */
-export function SelectionProvider(providerProps: PropsWithChildren) {
- const { required, min, max, onSelect, selected, defaultSelected, mode } =
- useProps();
-
- const [value, setValue] = useControlledValue(
- defaultSelected ?? selected,
- selected
- );
-
- /** Set the selected days when in "single" mode. */
- function setSingle(
- date: Date,
- modifiers: DayModifiers,
- e: MouseEvent | KeyboardEvent
- ) {
- let selected: Date | undefined;
- if (modifiers.selected && !required) {
- selected = undefined;
- } else {
- selected = date;
- }
- setValue(selected);
- onSelect?.(selected, date, modifiers, e);
- return selected;
- }
-
- /** Return `true` if the given day is selected in "single" mode. */
- function isSingleSelected(date: Date) {
- return Boolean(value && isSameDay(value as Date, date));
- }
-
- /** Set the selected days when in "multiple" mode. */
- function setMulti(
- date: Date,
- modifiers: DayModifiers,
- e: MouseEvent | KeyboardEvent
- ) {
- if (value !== undefined && !Array.isArray(value)) {
- // Not a multi select
- return;
- }
- let selected: Date[] = [];
-
- if (modifiers.selected) {
- // Date is already selected
- if (min && value && value.length <= min) {
- // Min value reached, do nothing
- selected = value;
- } else {
- // Remove the date from the selection
- selected =
- value?.filter((day: Date | undefined) => {
- return Boolean(day && date && !isSameDay(day, date));
- }) || ([] as Date[]);
- }
- } else if (max !== undefined && value?.length === max) {
- // Max value reached, reset the selection to date
- selected = date ? [date] : [];
- } else {
- // Add the date to the selection
- selected = [...(value ?? []), date];
- }
- setValue(selected);
- onSelect?.(selected, date, modifiers, e);
- return selected;
- }
- function isMultiSelected(date: Date) {
- if (!Array.isArray(value)) return false;
-
- return Boolean(value?.some((day) => isSameDay(day, date)));
- }
-
- /** Set the selected days when in "range" mode. */
- function setRange(
- date: Date,
- modifiers: DayModifiers,
- e: MouseEvent | KeyboardEvent
- ) {
- if (value !== undefined && !isDateRange(value)) {
- return;
- }
- const selected = date ? addToRange(date, value) : undefined;
-
- if (min) {
- if (
- selected?.from &&
- selected.to &&
- differenceInCalendarDays(selected.to, selected.from) <= min
- ) {
- selected.from = date;
- selected.to = undefined;
- }
- }
-
- if (max) {
- if (
- selected?.from &&
- selected.to &&
- differenceInCalendarDays(selected.to, selected.from) + 1 > max
- ) {
- selected.from = date;
- selected.to = undefined;
- }
- }
-
- setValue(selected);
- onSelect?.(selected, date, modifiers, e); // Now TypeScript knows it's a MouseEvent
-
- return value;
- }
- function isRangeSelected(date: Date) {
- if (value !== undefined && !isDateRange(value)) {
- // Not a range select
- return false;
- }
- return Boolean(value && isDateInRange(date, value));
- }
-
- function setSelected(
- date: Date,
- modifiers: DayModifiers,
- e: MouseEvent | KeyboardEvent
- ) {
- if (mode === "single") setSingle(date, modifiers, e);
- if (mode === "multiple") setMulti(date, modifiers, e);
- if (mode === "range") setRange(date, modifiers, e);
- }
-
- function isSelected(date: Date) {
- if (mode === "single") return isSingleSelected(date);
- if (mode === "multiple") return isMultiSelected(date);
- if (mode === "range") return isRangeSelected(date);
- return false;
- }
-
- function isStartOfRange(date: Date) {
- if (!isDateRange(value)) return false;
- return Boolean(value.from && isSameDay(value.from, date) && value.to);
- }
-
- function isEndOfRange(date: Date) {
- if (!isDateRange(value)) return false;
- return Boolean(value.to && isSameDay(value.to, date));
- }
-
- function isMiddleOfRange(date: Date) {
- if (!isDateRange(value)) return false;
- if (!value.from || !value.to) return false;
- return (
- isDateInRange(date, {
- from: addDays(value.from, 1),
- to: subDays(value.to, 1)
- }) &&
- !isStartOfRange(date) &&
- !isEndOfRange(date)
- );
- }
-
- const contextValue = {
- selected: value,
- setSelected,
- isSelected,
- isStartOfRange,
- isEndOfRange,
- isMiddleOfRange
- };
-
- return (
-
- {providerProps.children}
-
- );
-}
-
-/**
- * Access and change the currently selected values.
- *
- * Use this hook from the custom components passed via the `components` prop.
- *
- * @template T - The {@link Mode | selection mode}. Defaults to `"default"`.
- * @template R - Whether the selection is required. Defaults to `false`.
- * @group Hooks
- * @see https://react-day-picker.js.org/advanced-guides/custom-components
- */
-export function useSelection<
- T extends Mode = "default",
- R extends boolean = false
->() {
- const context = useContext(selectionContext);
- if (!context) {
- throw new Error(`useSelection must be used within a SelectionProvider.`);
- }
- return context as SelectionContext;
-}
diff --git a/src/formatters/formatMonthDropdown.test.ts b/src/formatters/formatMonthDropdown.test.ts
index 19651e6804..58133b6179 100644
--- a/src/formatters/formatMonthDropdown.test.ts
+++ b/src/formatters/formatMonthDropdown.test.ts
@@ -1,4 +1,3 @@
-import { Month } from "date-fns";
import { es } from "date-fns/locale/es";
import { formatMonthDropdown } from "./formatMonthDropdown";
@@ -6,13 +5,11 @@ import { formatMonthDropdown } from "./formatMonthDropdown";
const date = new Date(2022, 10, 21);
test("should return the formatted month dropdown label", () => {
- expect(formatMonthDropdown(date.getMonth() as Month)).toEqual("November");
+ expect(formatMonthDropdown(date.getMonth())).toEqual("November");
});
describe("when a locale is passed in", () => {
test("should format using the locale", () => {
- expect(formatMonthDropdown(date.getMonth() as Month, es)).toEqual(
- "noviembre"
- );
+ expect(formatMonthDropdown(date.getMonth(), es)).toEqual("noviembre");
});
});
diff --git a/src/formatters/formatMonthDropdown.ts b/src/formatters/formatMonthDropdown.ts
index 013881eea7..abe64cad60 100644
--- a/src/formatters/formatMonthDropdown.ts
+++ b/src/formatters/formatMonthDropdown.ts
@@ -6,6 +6,9 @@ import { enUS } from "date-fns/locale/en-US";
*
* @group Formatters
*/
-export function formatMonthDropdown(monthNumber: Month, locale = enUS): string {
- return locale.localize?.month(monthNumber) ?? monthNumber.toString();
+export function formatMonthDropdown(
+ monthNumber: number,
+ locale = enUS
+): string {
+ return locale.localize?.month(monthNumber as Month) ?? monthNumber.toString();
}
diff --git a/src/helpers/calculateMonthWeeks.test.ts b/src/helpers/calculateMonthWeeks.test.ts
deleted file mode 100644
index fc3ca7618e..0000000000
--- a/src/helpers/calculateMonthWeeks.test.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { enUS } from "date-fns/locale";
-
-import { calculateMonthWeeks } from "./calculateMonthWeeks";
-
-test("calculate weekly ranges correctly without options", () => {
- const startDate = new Date(2023, 0, 1); // January 1, 2023
- const endDate = new Date(2023, 0, 15); // January 15, 2023
-
- const result = calculateMonthWeeks(startDate, endDate);
-
- expect(result).toBeInstanceOf(Array);
- expect(result).toHaveLength(3);
-
- expect(result[0].dates).toHaveLength(7);
- expect(result[0].dates[0]).toBeSunday();
-
- expect(result[1].dates).toHaveLength(7);
- expect(result[2].dates).toHaveLength(7);
- expect(result[2].dates[6]).toHaveDate(21);
- expect(result[2].dates[6]).toBeSaturday();
-});
-
-test("calculate weekly ranges correctly with ISOWeek option", () => {
- const startDate = new Date(2023, 0, 1);
- const endDate = new Date(2023, 0, 17); // January 16, 2023
- const options = { ISOWeek: true };
-
- const result = calculateMonthWeeks(startDate, endDate, options);
-
- expect(result[0].dates).toHaveLength(7);
- expect(result[0].dates[0]).toBeMonday();
- expect(result[2].dates[6]).toBeSunday();
- expect(result[2].dates[6]).toHaveDate(15);
- expect(result[3].dates[6]).toHaveDate(22);
-});
-
-test("respect locale and weekStartsOn options", () => {
- const startDate = new Date(2023, 0, 1);
- const endDate = new Date(2023, 0, 15);
- const options = { locale: enUS, weekStartsOn: 2 as 0 | 2 }; // start on Tuesday
-
- const result = calculateMonthWeeks(startDate, endDate, options);
-
- expect(result[0].dates[0]).toBeTuesday();
- expect(result[1].dates[0]).toBeTuesday();
- expect(result[2].dates[0]).toBeTuesday();
-});
diff --git a/src/helpers/calculateMonthWeeks.ts b/src/helpers/calculateMonthWeeks.ts
deleted file mode 100644
index d3756354b0..0000000000
--- a/src/helpers/calculateMonthWeeks.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import {
- addDays,
- differenceInCalendarDays,
- endOfISOWeek,
- endOfWeek,
- getISOWeek,
- getWeek,
- Locale,
- startOfISOWeek,
- startOfWeek
-} from "date-fns";
-
-import { MonthWeek } from "./getMonthWeeks";
-
-/** Return the weeks between two dates. */
-export function calculateMonthWeeks(
- startDate: Date,
- endDate: Date,
- options?: {
- ISOWeek?: boolean;
- locale?: Locale;
- weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
- firstWeekContainsDate?: 1 | 4;
- }
-): MonthWeek[] {
- const toWeek = options?.ISOWeek
- ? endOfISOWeek(endDate)
- : endOfWeek(endDate, options);
- const fromWeek = options?.ISOWeek
- ? startOfISOWeek(startDate)
- : startOfWeek(startDate, options);
-
- const nOfDays = differenceInCalendarDays(toWeek, fromWeek);
- const days: Date[] = [];
-
- for (let i = 0; i <= nOfDays; i++) {
- days.push(addDays(fromWeek, i));
- }
-
- const weeksInMonth = days.reduce((result: MonthWeek[], date) => {
- const weekNumber = options?.ISOWeek
- ? getISOWeek(date)
- : getWeek(date, options);
-
- const existingWeek = result.find(
- (value) => value.weekNumber === weekNumber
- );
- if (existingWeek) {
- existingWeek.dates.push(date);
- return result;
- }
- result.push({
- weekNumber,
- dates: [date]
- });
- return result;
- }, []);
-
- return weeksInMonth;
-}
diff --git a/src/helpers/getClassNamesForModifiers.ts b/src/helpers/getClassNamesForModifiers.ts
index 7a2d2badbf..c693ea3028 100644
--- a/src/helpers/getClassNamesForModifiers.ts
+++ b/src/helpers/getClassNamesForModifiers.ts
@@ -1,23 +1,20 @@
-import { DayModifier } from "../UI";
-import type {
- DayModifiers,
- ModifiersClassNames,
- ClassNames,
- InternalModifier
-} from "../types";
+import { DayFlag, SelectionState } from "../UI";
+import type { ModifiersClassNames, ClassNames } from "../types";
export function getClassNamesForModifiers(
- dayModifiers: DayModifiers,
+ modifiers: Record,
modifiersClassNames: ModifiersClassNames,
classNames: ClassNames
) {
- const modifierClassNames = Object.entries(dayModifiers)
+ const modifierClassNames = Object.entries(modifiers)
.filter(([, active]) => active === true)
.reduce((previousValue, [key]) => {
if (modifiersClassNames[key]) {
previousValue.push(modifiersClassNames[key as string]);
- } else if (classNames[DayModifier[key as InternalModifier]]) {
- previousValue.push(classNames[DayModifier[key as InternalModifier]]);
+ } else if (classNames[DayFlag[key as DayFlag]]) {
+ previousValue.push(classNames[DayFlag[key as DayFlag]]);
+ } else if (classNames[SelectionState[key as SelectionState]]) {
+ previousValue.push(classNames[SelectionState[key as SelectionState]]);
}
return previousValue;
}, [] as string[]);
diff --git a/src/helpers/getDataAttributes.tsx b/src/helpers/getDataAttributes.tsx
index 1fc3ee49ce..71617ce9f4 100644
--- a/src/helpers/getDataAttributes.tsx
+++ b/src/helpers/getDataAttributes.tsx
@@ -1,9 +1,9 @@
-import React from "react";
-
-import type { PropsBase } from "../types";
+import type { Mode, DayPickerProps } from "../types";
/** Return the `data-` attributes from the props. */
-export function getDataAttributes(props: PropsBase): Record {
+export function getDataAttributes(
+ props: DayPickerProps
+): Record {
const dataAttributes: Record = {};
Object.entries(props).forEach(([key, val]) => {
if (key.startsWith("data-")) {
diff --git a/src/helpers/getDates.ts b/src/helpers/getDates.ts
index f0d0c9263b..24ee631a27 100644
--- a/src/helpers/getDates.ts
+++ b/src/helpers/getDates.ts
@@ -8,7 +8,8 @@ import { isAfter } from "date-fns/isAfter";
import { startOfISOWeek } from "date-fns/startOfISOWeek";
import { startOfWeek } from "date-fns/startOfWeek";
-import type { DayPickerProps, Mode } from "../types";
+import { PropsContextValue } from "../contexts/props";
+import type { Mode, DayPickerProps } from "../types";
/** The number of days in a month when having 6 weeks. */
const NrOfDaysWithFixedWeeks = 42;
@@ -19,7 +20,7 @@ const NrOfDaysWithFixedWeeks = 42;
* @param firstMonth The first month of the calendar
* @param lastMonth The last month of the calendar
* @param maxDate The date to end the calendar at
- * @param options Options for the calendar
+ * @param props Options for the calendar
* @param options.ISOWeek Whether or not to use ISOWeek
* @param options.fixedWeeks Whether or not to use fixed weeks
* @param options.locale The locale to use
@@ -27,16 +28,16 @@ const NrOfDaysWithFixedWeeks = 42;
*/
export function getDates(
displayMonths: Date[],
- maxDate?: Date | undefined,
- options?: Pick<
- DayPickerProps,
+ maxDate: Date | undefined,
+ props: Pick<
+ PropsContextValue,
"ISOWeek" | "fixedWeeks" | "locale" | "weekStartsOn"
>
): Date[] {
const firstMonth = displayMonths[0];
const lastMonth = displayMonths[displayMonths.length - 1];
- const { ISOWeek, fixedWeeks, locale, weekStartsOn } = options ?? {};
+ const { ISOWeek, fixedWeeks, locale, weekStartsOn } = props ?? {};
const startWeekFirstDate = ISOWeek
? startOfISOWeek(firstMonth)
diff --git a/src/helpers/getDefaultClassNames.ts b/src/helpers/getDefaultClassNames.ts
index 61f3ae02c3..96291d1069 100644
--- a/src/helpers/getDefaultClassNames.ts
+++ b/src/helpers/getDefaultClassNames.ts
@@ -1,9 +1,10 @@
import {
UI,
- DayModifier,
+ DayFlag,
CalendarFlag,
ChevronFlag,
- WeekNumberFlag
+ WeekNumberFlag,
+ SelectionState
} from "../UI";
import type { ClassNames } from "../types";
@@ -35,9 +36,14 @@ export function getDefaultClassNames(): Required {
`rdp-${WeekNumberFlag[key as keyof typeof WeekNumberFlag]}`;
}
- for (const key in DayModifier) {
- classNames[DayModifier[key as keyof typeof DayModifier]] =
- `rdp-${DayModifier[key as keyof typeof DayModifier]}`;
+ for (const key in DayFlag) {
+ classNames[DayFlag[key as keyof typeof DayFlag]] =
+ `rdp-${DayFlag[key as keyof typeof DayFlag]}`;
+ }
+
+ for (const key in SelectionState) {
+ classNames[SelectionState[key as keyof typeof SelectionState]] =
+ `rdp-${SelectionState[key as keyof typeof SelectionState]}`;
}
return classNames as Required;
diff --git a/src/helpers/getDisplayMonths.test.ts b/src/helpers/getDisplayMonths.test.ts
index 752ba30878..5372ea2d62 100644
--- a/src/helpers/getDisplayMonths.test.ts
+++ b/src/helpers/getDisplayMonths.test.ts
@@ -1,34 +1,36 @@
import { getDisplayMonths } from "./getDisplayMonths";
-describe("when the number of months is 1", () => {
+describe("getDisplayMonths", () => {
it("should return the months to display in the calendar", () => {
const firstMonth = new Date(2020, 0);
const expectedResult = [new Date(2020, 0)];
- const result = getDisplayMonths(firstMonth, { numberOfMonths: 1 });
+ const result = getDisplayMonths(firstMonth, {
+ numberOfMonths: 1,
+ endMonth: undefined
+ });
expect(result).toEqual(expectedResult);
});
-});
-describe("when the number of months is greater than 1", () => {
- it("should return the months to display in the calendar", () => {
+ it("should return the months to display in the calendar when the number of months is greater than 1", () => {
const firstMonth = new Date(2020, 0);
const expectedResult = [
new Date(2020, 0),
new Date(2020, 1),
new Date(2020, 2)
];
- const result = getDisplayMonths(firstMonth, { numberOfMonths: 3 });
+ const result = getDisplayMonths(firstMonth, {
+ numberOfMonths: 3,
+ endMonth: undefined
+ });
expect(result).toEqual(expectedResult);
});
-});
-describe("when passing a max date", () => {
- it("should return the months to display in the calendar", () => {
+ it("should return the months to display in the calendar when passing a max date", () => {
const firstMonth = new Date(2020, 0);
const expectedResult = [new Date(2020, 0), new Date(2020, 1)];
const result = getDisplayMonths(firstMonth, {
numberOfMonths: 3,
- toDate: new Date(2020, 1, 10)
+ endMonth: new Date(2020, 1, 10)
});
expect(result).toEqual(expectedResult);
});
diff --git a/src/helpers/getDisplayMonths.ts b/src/helpers/getDisplayMonths.ts
index 9adb67054f..1e7bc78eb5 100644
--- a/src/helpers/getDisplayMonths.ts
+++ b/src/helpers/getDisplayMonths.ts
@@ -1,18 +1,15 @@
import { addMonths } from "date-fns/addMonths";
+import type { PropsContextValue } from "../contexts/props";
+
export function getDisplayMonths(
- firstMonth: Date,
- options: {
- numberOfMonths: number;
- toDate?: Date;
- reverseMonths?: boolean;
- }
+ firstDisplayedMonth: Date,
+ props: Pick
) {
- const { numberOfMonths, toDate } = options;
const months: Date[] = [];
- for (let i = 0; i < numberOfMonths; i++) {
- const month = addMonths(firstMonth, i);
- if (toDate && month > toDate) {
+ for (let i = 0; i < props.numberOfMonths; i++) {
+ const month = addMonths(firstDisplayedMonth, i);
+ if (props.endMonth && month > props.endMonth) {
break;
}
months.push(month);
diff --git a/src/helpers/getDropdownMonths.test.ts b/src/helpers/getDropdownMonths.test.ts
index aa0a1a1f1e..b670ddf0b0 100644
--- a/src/helpers/getDropdownMonths.test.ts
+++ b/src/helpers/getDropdownMonths.test.ts
@@ -1,69 +1,36 @@
-import * as formatters from "../formatters";
+import { type Locale, format } from "date-fns";
+import { enUS as locale } from "date-fns/locale";
import { getDropdownMonths } from "./getDropdownMonths";
-
-test("returns undefined if `startMonth` is not defined", () => {
- const result = getDropdownMonths({
- startMonth: undefined,
- endMonth: new Date(),
- formatters
+import { getFormatters } from "./getFormatters";
+
+test("return correct dropdown options", () => {
+ const displayMonth = new Date(2022, 0, 1); // January 2022
+ const startMonth = new Date(2022, 0, 1); // January 2022
+ const endMonth = new Date(2022, 11, 31); // December 2022
+ const formatters = getFormatters({
+ formatMonthDropdown: (month: number, locale?: Locale) =>
+ format(new Date(2022, month), "MMMM", { locale })
});
- expect(result).toBeUndefined();
-});
-
-test("returns undefined if `endMonth` is not defined", () => {
- const result = getDropdownMonths({
- startMonth: new Date(),
- endMonth: undefined,
- formatters
+ const result = getDropdownMonths(displayMonth, {
+ formatters,
+ locale,
+ startMonth,
+ endMonth
});
- expect(result).toBeUndefined();
-});
-
-test("returns sorted months between `startMonth` and `endMonth`", () => {
- const startMonth = new Date(2023, 0, 1);
- const endMonth = new Date(2023, 11, 31);
-
- const result = getDropdownMonths(
- { startMonth, endMonth, formatters },
- startMonth.getFullYear()
- );
- expect(result).toBeDefined();
- expect(result).toHaveLength(12);
- if (!result) throw new Error("Unexpected undefined result");
- for (let i = 0; i < result.length - 1; i++) {
- expect(result[i].value).toBeLessThan(result[i + 1].value);
- }
-});
-
-test("formats month labels correctly", () => {
- const startMonth = new Date(2023, 3, 1);
- const endMonth = new Date(2023, 11, 31);
- const result = getDropdownMonths(
- { startMonth, endMonth, formatters },
- startMonth.getFullYear()
- );
- if (!result) throw new Error("Unexpected undefined result");
- expect(result[0]).toEqual({ disabled: false, label: "April", value: 3 });
- expect(result[8]).toEqual({ disabled: false, label: "December", value: 11 });
-});
-
-describe("when using a custom formatter", () => {
- test("formats month labels correctly", () => {
- const startMonth = new Date(2023, 0, 1);
- const endMonth = new Date(2023, 11, 31);
- const result = getDropdownMonths(
- {
- startMonth,
- endMonth,
- formatters: {
- formatMonthDropdown: (month) => `Month ${month.toString()}`
- }
- },
- startMonth.getFullYear()
- );
- if (!result) throw new Error("Unexpected undefined result");
- expect(result[0]).toEqual({ disabled: false, label: "Month 0", value: 0 });
- });
+ expect(result).toEqual([
+ { value: 0, label: "January", disabled: false },
+ { value: 1, label: "February", disabled: false },
+ { value: 2, label: "March", disabled: false },
+ { value: 3, label: "April", disabled: false },
+ { value: 4, label: "May", disabled: false },
+ { value: 5, label: "June", disabled: false },
+ { value: 6, label: "July", disabled: false },
+ { value: 7, label: "August", disabled: false },
+ { value: 8, label: "September", disabled: false },
+ { value: 9, label: "October", disabled: false },
+ { value: 10, label: "November", disabled: false },
+ { value: 11, label: "December", disabled: false }
+ ]);
});
diff --git a/src/helpers/getDropdownMonths.ts b/src/helpers/getDropdownMonths.ts
index e2bb1e0ca5..5729300d23 100644
--- a/src/helpers/getDropdownMonths.ts
+++ b/src/helpers/getDropdownMonths.ts
@@ -1,26 +1,28 @@
-import type { Month } from "date-fns";
+import type { Locale, Month } from "date-fns";
import { addMonths } from "date-fns/addMonths";
import { isBefore } from "date-fns/isBefore";
import { startOfMonth } from "date-fns/startOfMonth";
import { DropdownOption } from "../components/Dropdown";
-import { PropsContext } from "../contexts/props";
-import { Formatters, Mode } from "../types";
+import { PropsContextValue } from "../contexts/props";
/** Return the months to show in the dropdown. */
export function getDropdownMonths(
+ displayMonth: Date,
props: Pick<
- PropsContext,
- "startMonth" | "endMonth" | "locale"
- > & {
- formatters: Pick;
- },
- year?: number | undefined
+ PropsContextValue,
+ "formatters" | "locale" | "startMonth" | "endMonth"
+ >
): DropdownOption[] | undefined {
- if (!props.startMonth) return undefined;
- if (!props.endMonth) return undefined;
- const navStartMonth = startOfMonth(props.startMonth);
- const navEndMonth = startOfMonth(props.endMonth);
+ const { startMonth, endMonth } = props;
+
+ if (!startMonth) return undefined;
+ if (!endMonth) return undefined;
+
+ const year = displayMonth.getFullYear();
+
+ const navStartMonth = startOfMonth(startMonth);
+ const navEndMonth = startOfMonth(endMonth);
const months: number[] = [];
let month = navStartMonth;
@@ -32,17 +34,10 @@ export function getDropdownMonths(
return a - b;
});
const options = sortedMonths.map((value) => {
- const label = props.formatters.formatMonthDropdown(
- value as Month,
- props.locale
- );
+ const label = props.formatters.formatMonthDropdown(value, props.locale);
const disabled =
- (year &&
- props.startMonth &&
- new Date(year, value) < startOfMonth(props.startMonth)) ||
- (year &&
- props.endMonth &&
- new Date(year, value) > startOfMonth(props.endMonth)) ||
+ (startMonth && new Date(year, value) < startOfMonth(startMonth)) ||
+ (endMonth && new Date(year, value) > startOfMonth(endMonth)) ||
false;
return { value, label, disabled };
});
diff --git a/src/helpers/getDropdownYears.test.ts b/src/helpers/getDropdownYears.test.ts
index 459d6a2ddd..0c863e8779 100644
--- a/src/helpers/getDropdownYears.test.ts
+++ b/src/helpers/getDropdownYears.test.ts
@@ -1,72 +1,48 @@
-import { formatYearDropdown } from "../formatters";
+import { enUS as locale } from "date-fns/locale";
import { getDropdownYears } from "./getDropdownYears";
+import { getFormatters } from "./getFormatters";
-it("returns undefined if startMonth is not defined", () => {
- const result = getDropdownYears({
- endMonth: new Date(),
+test("return undefined if startMonth or endMonth is not provided", () => {
+ const displayMonth = new Date(2022, 0, 1); // January 2022
+ const formatters = getFormatters({
+ formatYearDropdown: (year: number) => `${year}`
+ });
+ const result1 = getDropdownYears(displayMonth, {
+ formatters,
+ locale,
startMonth: undefined,
- formatters: { formatYearDropdown }
+ endMonth: new Date(2022, 11, 31)
});
- expect(result).toBeUndefined();
-});
-
-it("returns undefined if `endMonth` is not defined", () => {
- const result = getDropdownYears({
- startMonth: new Date(),
- endMonth: undefined,
- formatters: { formatYearDropdown }
+ const result2 = getDropdownYears(displayMonth, {
+ formatters,
+ locale,
+ startMonth: new Date(2022, 0, 1),
+ endMonth: undefined
});
- expect(result).toBeUndefined();
+
+ expect(result1).toBeUndefined();
+ expect(result2).toBeUndefined();
});
-it("returns an array of years between `startMonth` and `endMonth`", () => {
- const startMonth = new Date(2020, 0, 1);
- const endMonth = new Date(2022, 11, 31);
- const result = getDropdownYears({
- startMonth,
- endMonth,
- formatters: {
- formatYearDropdown: (year: number): string =>
- `Formatted ${year.toString()}`
- }
+test("return correct dropdown options", () => {
+ const displayMonth = new Date(2022, 0, 1); // January 2022
+ const startMonth = new Date(2022, 0, 1); // January 2022
+ const endMonth = new Date(2024, 11, 31); // December 2024
+ const formatters = getFormatters({
+ formatYearDropdown: (year: number) => `${year}`
});
- expect(result).toEqual([
- {
- disabled: false,
- label: "Formatted 2020",
- value: 2020
- },
- {
- disabled: false,
- label: "Formatted 2021",
- value: 2021
- },
- {
- disabled: false,
- label: "Formatted 2022",
- value: 2022
- }
- ]);
-});
-
-it("handles same year for startMonth and endMonth", () => {
- const year = new Date(2021, 5, 15);
- const result = getDropdownYears({
- startMonth: year,
- endMonth: year,
- formatters: {
- formatYearDropdown: (year: number): string =>
- `Formatted ${year.toString()}`
- }
+ const result = getDropdownYears(displayMonth, {
+ formatters,
+ locale,
+ startMonth,
+ endMonth
});
expect(result).toEqual([
- {
- disabled: false,
- label: "Formatted 2021",
- value: 2021
- }
+ { value: 2022, label: "2022", disabled: false },
+ { value: 2023, label: "2023", disabled: false },
+ { value: 2024, label: "2024", disabled: false }
]);
});
diff --git a/src/helpers/getDropdownYears.ts b/src/helpers/getDropdownYears.ts
index ddf086301a..eae0b39009 100644
--- a/src/helpers/getDropdownYears.ts
+++ b/src/helpers/getDropdownYears.ts
@@ -6,34 +6,35 @@ import { startOfMonth } from "date-fns/startOfMonth";
import { startOfYear } from "date-fns/startOfYear";
import { DropdownOption } from "../components/Dropdown";
-import type { PropsContext } from "../contexts/props";
-import type { Formatters, Mode } from "../types";
+import { PropsContextValue } from "../contexts/props";
/** Return the years to show in the dropdown. */
export function getDropdownYears(
- props: Pick, "startMonth" | "endMonth"> & {
- formatters: Pick;
- },
- month?: number | undefined
+ displayMonth: Date,
+ props: Pick<
+ PropsContextValue,
+ "formatters" | "locale" | "startMonth" | "endMonth"
+ >
): DropdownOption[] | undefined {
- if (!props.startMonth) return undefined;
- if (!props.endMonth) return undefined;
- const firstNavYear = startOfYear(props.startMonth);
- const lastNavYear = endOfYear(props.endMonth);
+ const { startMonth, endMonth } = props;
+ if (!startMonth) return undefined;
+ if (!endMonth) return undefined;
+
+ const month = displayMonth.getMonth();
+ const firstNavYear = startOfYear(startMonth);
+ const lastNavYear = endOfYear(endMonth);
const years: number[] = [];
+
let year = firstNavYear;
while (isBefore(year, lastNavYear) || isSameYear(year, lastNavYear)) {
years.push(year.getFullYear());
year = addYears(year, 1);
}
+
return years.map((value) => {
const disabled =
- (month &&
- props.startMonth &&
- new Date(value, month) < startOfMonth(props.startMonth)) ||
- (month &&
- props.endMonth &&
- new Date(value, month) > startOfMonth(props.endMonth)) ||
+ (startMonth && new Date(value, month) < startOfMonth(startMonth)) ||
+ (month && endMonth && new Date(value, month) > startOfMonth(endMonth)) ||
false;
const label = props.formatters.formatYearDropdown(value);
return {
diff --git a/src/helpers/getFormatters.test.ts b/src/helpers/getFormatters.test.ts
index 94d136e2c5..ceafc7e429 100644
--- a/src/helpers/getFormatters.test.ts
+++ b/src/helpers/getFormatters.test.ts
@@ -1,5 +1,4 @@
import * as defaultFormatters from "../formatters";
-import type { PropsBase } from "../types";
import { getFormatters } from "./getFormatters";
diff --git a/src/helpers/getFormatters.ts b/src/helpers/getFormatters.ts
index 55111f9630..eeb7a45311 100644
--- a/src/helpers/getFormatters.ts
+++ b/src/helpers/getFormatters.ts
@@ -1,8 +1,8 @@
import * as defaultFormatters from "../formatters";
-import type { PropsBase } from "../types";
+import type { DayPickerProps } from "../types";
/** Return the formatters from the props merged with the default formatters. */
-export function getFormatters(customFormatters: PropsBase["formatters"]) {
+export function getFormatters(customFormatters: DayPickerProps["formatters"]) {
if (customFormatters?.formatMonthCaption && !customFormatters.formatCaption) {
customFormatters.formatCaption = customFormatters.formatMonthCaption;
}
diff --git a/src/helpers/getStartMonth.test.ts b/src/helpers/getInitialMonth.test.ts
similarity index 83%
rename from src/helpers/getStartMonth.test.ts
rename to src/helpers/getInitialMonth.test.ts
index 8eb6a591a9..9f3a351da5 100644
--- a/src/helpers/getStartMonth.test.ts
+++ b/src/helpers/getInitialMonth.test.ts
@@ -1,26 +1,26 @@
import { addMonths, isSameMonth } from "date-fns";
-import { getStartMonth } from "./getStartMonth";
+import { getInitialMonth } from "./getInitialMonth";
describe("when no endMonth is given", () => {
describe("when month is in context", () => {
const month = new Date(2010, 11, 12);
it("return that month", () => {
- const startMonth = getStartMonth({ month });
+ const startMonth = getInitialMonth({ month });
expect(isSameMonth(startMonth, month)).toBe(true);
});
});
describe("when defaultMonth is in context", () => {
const defaultMonth = new Date(2010, 11, 12);
it("return that month", () => {
- const startMonth = getStartMonth({ defaultMonth });
+ const startMonth = getInitialMonth({ defaultMonth });
expect(isSameMonth(startMonth, defaultMonth)).toBe(true);
});
});
describe("when no month or defaultMonth are in context", () => {
const today = new Date(2010, 11, 12);
it("return the today month", () => {
- const startMonth = getStartMonth({ today });
+ const startMonth = getInitialMonth({ today });
expect(isSameMonth(startMonth, today)).toBe(true);
});
});
@@ -32,7 +32,7 @@ describe("when endMonth is given", () => {
describe("when the number of month is 1", () => {
const numberOfMonths = 1;
it("return the endMonth", () => {
- const startMonth = getStartMonth({
+ const startMonth = getInitialMonth({
month,
endMonth,
numberOfMonths
@@ -43,7 +43,7 @@ describe("when endMonth is given", () => {
describe("when the number of month is 3", () => {
const numberOfMonths = 3;
it("return the endMonth plus the number of months", () => {
- const startMonth = getStartMonth({
+ const startMonth = getInitialMonth({
month,
endMonth,
numberOfMonths
diff --git a/src/helpers/getStartMonth.ts b/src/helpers/getInitialMonth.ts
similarity index 90%
rename from src/helpers/getStartMonth.ts
rename to src/helpers/getInitialMonth.ts
index a2c14e9941..6bdd9463a2 100644
--- a/src/helpers/getStartMonth.ts
+++ b/src/helpers/getInitialMonth.ts
@@ -2,14 +2,13 @@ import { addMonths } from "date-fns/addMonths";
import { differenceInCalendarMonths } from "date-fns/differenceInCalendarMonths";
import { startOfMonth } from "date-fns/startOfMonth";
-import { PropsBase } from "../types";
+import type { PropsContextValue } from "../contexts/props";
/** Return the start month based on the props passed to DayPicker. */
-
-export function getStartMonth(
+export function getInitialMonth(
props: Partial<
Pick<
- PropsBase,
+ PropsContextValue,
| "fromYear"
| "toYear"
| "startMonth"
diff --git a/src/helpers/getMonthWeeks.test.ts b/src/helpers/getMonthWeeks.test.ts
deleted file mode 100644
index 6d9e6e3aa1..0000000000
--- a/src/helpers/getMonthWeeks.test.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import { enGB, enUS } from "date-fns/locale";
-
-import { getMonthWeeks } from "./getMonthWeeks";
-
-describe('when using the "enUS" locale', () => {
- const locale = enUS;
- describe("when using fixed weeks", () => {
- const useFixedWeeks = true;
- describe("when getting the weeks for December 2022", () => {
- const date = new Date(2022, 11);
- const weeks = getMonthWeeks(date, { useFixedWeeks, locale });
- test("should return 49 - 1 week numbers", () => {
- const weekNumbers = weeks.map((week) => week.weekNumber);
- const expectedResult = [49, 50, 51, 52, 53, 1];
- expect(weekNumbers).toEqual(expectedResult);
- });
- test("the last week should be the one in the next year", () => {
- const lastWeek = weeks[weeks.length - 1];
- const lastWeekDates = lastWeek.dates.map((date) => date.getDate());
- const expectedResult = [1, 2, 3, 4, 5, 6, 7];
- expect(lastWeekDates).toEqual(expectedResult);
- });
- });
- describe("when getting the weeks for December 2021", () => {
- const weeks = getMonthWeeks(new Date(2021, 11), {
- useFixedWeeks: false,
- locale: enUS
- });
- test("should return 49 - 1 week numbers", () => {
- const weekNumbers = weeks.map((week) => week.weekNumber);
- const expectedResult = [49, 50, 51, 52, 1];
- expect(weekNumbers).toEqual(expectedResult);
- });
- test("the last week should be the last in the year", () => {
- const lastWeek = weeks[weeks.length - 1];
- const lastWeekDates = lastWeek.dates.map((date) => date.getDate());
- const expectedResult = [26, 27, 28, 29, 30, 31, 1];
- expect(lastWeekDates).toEqual(expectedResult);
- });
- test("week 1 contains the first day of the new year", () => {
- expect(weeks[4].dates.map((date) => date.getDate())).toEqual([
- 26, 27, 28, 29, 30, 31, 1
- ]);
- });
- });
- });
-});
-
-describe('when using the "enGB" locale', () => {
- const locale = enGB;
- describe("when getting the weeks for January 2022", () => {
- const date = new Date(2022, 0);
- const weeks = getMonthWeeks(date, { locale });
- test("the first week should be the last of the previous year", () => {
- const weekNumbers = weeks.map((week) => week.weekNumber);
- expect(weekNumbers[0]).toEqual(52);
- });
- test("the first week should contain days from previous year", () => {
- expect(weeks[0].dates.map((date) => date.getDate())).toEqual([
- 27, 28, 29, 30, 31, 1, 2
- ]);
- });
- test("the last week should be the last of January", () => {
- const weekNumbers = weeks.map((week) => week.weekNumber);
- expect(weekNumbers[weekNumbers.length - 1]).toEqual(5);
- });
- });
- describe("when setting thursday as first day of year", () => {
- const date = new Date(2022, 0);
- const weeks = getMonthWeeks(date, { locale, firstWeekContainsDate: 4 });
- test("the number of week should have number 52", () => {
- const weekNumbers = weeks.map((week) => week.weekNumber);
- expect(weekNumbers[0]).toEqual(52);
- });
- });
-});
-
-describe("when using the ISOWeek numbers", () => {
- const locale = enUS;
- describe("when getting the weeks for September 2022", () => {
- const date = new Date(2022, 8);
- const weeks = getMonthWeeks(date, { locale, ISOWeek: true });
- test("the last week should have number 39", () => {
- const weekNumbers = weeks.map((week) => week.weekNumber);
- expect(weekNumbers[weekNumbers.length - 1]).toEqual(39);
- });
- });
-});
-
-describe("when not using the ISOWeek numbers", () => {
- const locale = enUS;
- describe("when getting the weeks for September 2022", () => {
- const date = new Date(2022, 8);
- const weeks = getMonthWeeks(date, { locale, ISOWeek: false });
- test("the last week should have number 40", () => {
- const weekNumbers = weeks.map((week) => week.weekNumber);
- expect(weekNumbers[weekNumbers.length - 1]).toEqual(40);
- });
- });
-});
diff --git a/src/helpers/getMonthWeeks.ts b/src/helpers/getMonthWeeks.ts
deleted file mode 100644
index c9312b88e1..0000000000
--- a/src/helpers/getMonthWeeks.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import {
- addWeeks,
- endOfMonth,
- getWeeksInMonth,
- Locale,
- startOfMonth
-} from "date-fns";
-
-import { calculateMonthWeeks } from "./calculateMonthWeeks";
-
-/** Represent a week in the month. */
-export type MonthWeek = {
- /** The week number from the start of the year. */
- weekNumber: number;
- /** The dates in the week. */
- dates: Date[];
-};
-
-/**
- * Return the weeks belonging to the given month, adding the "outside days" to
- * the first and last week.
- */
-export function getMonthWeeks(
- month: Date,
- options: {
- locale: Locale;
- useFixedWeeks?: boolean;
- weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
- firstWeekContainsDate?: 1 | 4;
- ISOWeek?: boolean;
- }
-): MonthWeek[] {
- const weeksInMonth: MonthWeek[] = calculateMonthWeeks(
- startOfMonth(month),
- endOfMonth(month),
- options
- );
-
- if (options?.useFixedWeeks) {
- // Add extra weeks to the month, up to 6 weeks
- const nrOfMonthWeeks = getWeeksInMonth(month, options);
- if (nrOfMonthWeeks < 6) {
- const lastWeek = weeksInMonth[weeksInMonth.length - 1];
- const lastDate = lastWeek.dates[lastWeek.dates.length - 1];
- const toDate = addWeeks(lastDate, 6 - nrOfMonthWeeks);
- const extraWeeks = calculateMonthWeeks(
- addWeeks(lastDate, 1),
- toDate,
- options
- );
- weeksInMonth.push(...extraWeeks);
- }
- }
- return weeksInMonth;
-}
diff --git a/src/helpers/getMonths.test.ts b/src/helpers/getMonths.test.ts
index 29b7c72b53..192919adf3 100644
--- a/src/helpers/getMonths.test.ts
+++ b/src/helpers/getMonths.test.ts
@@ -1,96 +1,90 @@
import { CalendarMonth } from "../classes";
+import type { PropsContextValue } from "../contexts/props";
-import { getDates } from "./getDates";
import { getMonths } from "./getMonths";
-describe("when first and last months are the same", () => {
- it("should return the months to display in the calendar", () => {
- const month = new Date(2024, 0, 1);
- const dates = getDates([month]);
- const months = getMonths([month], dates);
- expect(months).toHaveLength(1);
- expect(months[0]).toBeInstanceOf(CalendarMonth);
- expect(months[0].date).toBe(month);
- expect(months[0].weeks[0].days[1].date).toStrictEqual(new Date(2024, 0, 1));
- expect(months[months.length - 1]).toBeInstanceOf(CalendarMonth);
- expect(months[0].weeks[4].days[1].date).toStrictEqual(
- new Date(2024, 0, 29)
- );
- });
- describe("when week starts on Saturday", () => {
- it("should return the months to display in the calendar", () => {
- const month = new Date(2024, 0, 1);
- const dates = getDates([month], undefined, { weekStartsOn: 6 });
- const months = getMonths([month], dates, { weekStartsOn: 6 });
-
- expect(months[0].weeks[0].days[0].date).toStrictEqual(
- new Date(2023, 11, 30)
- );
- expect(months[months.length - 1]).toBeInstanceOf(CalendarMonth);
- expect(months[0].weeks[4].days[0].date).toStrictEqual(
- new Date(2024, 0, 27)
- );
- });
- });
- describe("when using ISO weeks", () => {
- it("should return the months to display in the calendar", () => {
- const month = new Date(2024, 0, 1);
- const dates = getDates([month], undefined, { ISOWeek: true });
- const months = getMonths([month], dates);
-
- expect(months[0].weeks[0].days[1].date).toStrictEqual(
- new Date(2024, 0, 2)
- );
- expect(months[months.length - 1]).toBeInstanceOf(CalendarMonth);
- expect(months[0].weeks[4].days[1].date).toStrictEqual(
- new Date(2024, 0, 29)
- );
- });
- });
- describe("when using fixed weeks", () => {
- it("should return the months to display in the calendar", () => {
- const month = new Date(2023, 4, 1); // month with 4 weeks
- const dates = getDates([month], undefined, { fixedWeeks: true });
- const months = getMonths([month], dates);
-
- expect(months[0].weeks[0].days[0].date).toStrictEqual(
- new Date(2023, 3, 30)
- );
- expect(months[months.length - 1]).toBeInstanceOf(CalendarMonth);
- expect(months[0].weeks[4].days[1].date).toStrictEqual(
- new Date(2023, 4, 29)
- );
- });
- });
+const mockDates = [
+ new Date(2023, 4, 27), // May 1, 2023
+ new Date(2023, 5, 1), // June 1, 2023
+ new Date(2023, 5, 8), // June 2, 2023
+ new Date(2023, 5, 15), // June 15, 2023
+ new Date(2023, 5, 22), // June 22, 2023
+ new Date(2023, 5, 30), // June 30, 2023
+ new Date(2023, 6, 6) // June 30, 2023
+];
+
+const mockProps: Pick<
+ PropsContextValue,
+ | "fixedWeeks"
+ | "ISOWeek"
+ | "locale"
+ | "weekStartsOn"
+ | "reverseMonths"
+ | "firstWeekContainsDate"
+> = {
+ fixedWeeks: false,
+ ISOWeek: false,
+ locale: undefined,
+ weekStartsOn: 0, // Sunday
+ reverseMonths: false,
+ firstWeekContainsDate: 1
+};
+
+it("should return the correct months without ISO weeks and reverse months", () => {
+ const displayMonths = [new Date(2023, 5, 1)]; // June 2023
+
+ const result = getMonths(displayMonths, mockDates, mockProps);
+
+ expect(result).toHaveLength(1);
+ expect(result[0]).toBeInstanceOf(CalendarMonth);
+ expect(result[0].weeks).toHaveLength(5); // June 2023 has 5 weeks
});
-describe("when first and last months are not the same", () => {
- it("should return the months to display in the calendar", () => {
- const firstMonth = new Date(2024, 0, 1);
- const lastMonth = new Date(2024, 2, 1);
- const dates = getDates([firstMonth, lastMonth]);
- const months = getMonths([firstMonth, lastMonth], dates);
- expect(months).toHaveLength(2);
- expect(months[0]).toBeInstanceOf(CalendarMonth);
- expect(months[0].date).toBe(firstMonth);
- expect(months[0].weeks[0].days[1].date).toStrictEqual(new Date(2024, 0, 1));
- expect(months[months.length - 1]).toBeInstanceOf(CalendarMonth);
- expect(months[0].weeks[4].days[1].date).toStrictEqual(
- new Date(2024, 0, 29)
- );
- });
+it("should handle ISO weeks", () => {
+ const displayMonths = [new Date(2023, 5, 1)]; // June 2023
+
+ const isoProps = { ...mockProps, ISOWeek: true };
+
+ const result = getMonths(displayMonths, mockDates, isoProps);
+
+ expect(result).toHaveLength(1);
+ expect(result[0]).toBeInstanceOf(CalendarMonth);
+ expect(result[0].weeks).toHaveLength(5); // June 2023 has 5 ISO weeks
});
-describe("when reversing the order of the months", () => {
- it("should return the months to display in the calendar", () => {
- const firstMonth = new Date(2024, 0, 1);
- const lastMonth = new Date(2024, 2, 1);
- const dates = getDates([firstMonth, lastMonth]);
- const months = getMonths([firstMonth, lastMonth], dates, {
- reverseMonths: true
- });
- expect(months).toHaveLength(2);
- expect(months[0].date).toBe(lastMonth);
- expect(months[months.length - 1].date).toBe(firstMonth);
- });
+it("should handle reverse months", () => {
+ const displayMonths = [
+ new Date(2023, 4, 1), // May 2023
+ new Date(2023, 5, 1) // June 2023
+ ];
+
+ const reverseProps = { ...mockProps, reverseMonths: true };
+
+ const result = getMonths(displayMonths, mockDates, reverseProps);
+
+ expect(result).toHaveLength(2);
+ expect(result[0].date).toEqual(new Date(2023, 5, 1)); // June 2023
+ expect(result[1].date).toEqual(new Date(2023, 4, 1)); // May 2023
+});
+
+it("should handle fixed weeks", () => {
+ const displayMonths = [new Date(2023, 5, 1)]; // June 2023
+
+ const fixedWeeksProps = { ...mockProps, fixedWeeks: true };
+
+ const result = getMonths(displayMonths, mockDates, fixedWeeksProps);
+
+ expect(result).toHaveLength(1);
+ expect(result[0]).toBeInstanceOf(CalendarMonth);
+ expect(result[0].weeks).toHaveLength(6); // Fixed weeks should ensure 6 weeks in the month view
+});
+
+it("should handle months with no dates", () => {
+ const displayMonths = [new Date(2023, 5, 1)]; // June 2023
+
+ const result = getMonths(displayMonths, [], mockProps);
+
+ expect(result).toHaveLength(1);
+ expect(result[0]).toBeInstanceOf(CalendarMonth);
+ expect(result[0].weeks).toHaveLength(0); // No dates should result in no weeks
});
diff --git a/src/helpers/getMonths.ts b/src/helpers/getMonths.ts
index 704d8c8e21..97972bfee0 100644
--- a/src/helpers/getMonths.ts
+++ b/src/helpers/getMonths.ts
@@ -8,7 +8,7 @@ import { startOfISOWeek } from "date-fns/startOfISOWeek";
import { startOfWeek } from "date-fns/startOfWeek";
import { CalendarWeek, CalendarDay, CalendarMonth } from "../classes";
-import type { DayPickerProps, Mode } from "../types";
+import type { PropsContextValue } from "../contexts/props";
/** Return the months to display in the calendar. */
export function getMonths(
@@ -16,30 +16,31 @@ export function getMonths(
displayMonths: Date[],
/** The dates to display in the calendar. */
dates: Date[],
- options: Pick<
- DayPickerProps,
+ /** Options from the props context. */
+ props: Pick<
+ PropsContextValue,
| "fixedWeeks"
| "ISOWeek"
| "locale"
| "weekStartsOn"
| "reverseMonths"
| "firstWeekContainsDate"
- > = {}
+ >
): CalendarMonth[] {
const dayPickerMonths = displayMonths.reduce(
(months, month) => {
- const firstDateOfFirstWeek = options.ISOWeek
+ const firstDateOfFirstWeek = props.ISOWeek
? startOfISOWeek(month)
: startOfWeek(month, {
- locale: options.locale,
- weekStartsOn: options.weekStartsOn
+ locale: props.locale,
+ weekStartsOn: props.weekStartsOn
});
- const lastDateOfLastWeek = options.ISOWeek
+ const lastDateOfLastWeek = props.ISOWeek
? endOfISOWeek(endOfMonth(month))
: endOfWeek(endOfMonth(month), {
- locale: options.locale,
- weekStartsOn: options.weekStartsOn
+ locale: props.locale,
+ weekStartsOn: props.weekStartsOn
});
/** The dates to display in the month. */
@@ -47,7 +48,7 @@ export function getMonths(
return date >= firstDateOfFirstWeek && date <= lastDateOfLastWeek;
});
- if (options.fixedWeeks && monthDates.length < 42) {
+ if (props.fixedWeeks && monthDates.length < 42) {
const extraDates = dates.filter((date) => {
return (
date > lastDateOfLastWeek && date <= addDays(lastDateOfLastWeek, 7)
@@ -58,12 +59,12 @@ export function getMonths(
const weeks: CalendarWeek[] = monthDates.reduce(
(weeks, date) => {
- const weekNumber = options.ISOWeek
+ const weekNumber = props.ISOWeek
? getISOWeek(date)
: getWeek(date, {
- locale: options.locale,
- weekStartsOn: options.weekStartsOn,
- firstWeekContainsDate: options.firstWeekContainsDate
+ locale: props.locale,
+ weekStartsOn: props.weekStartsOn,
+ firstWeekContainsDate: props.firstWeekContainsDate
});
const week = weeks.find((week) => week.weekNumber === weekNumber);
@@ -86,7 +87,7 @@ export function getMonths(
[]
);
- if (!options.reverseMonths) {
+ if (!props.reverseMonths) {
return dayPickerMonths;
} else {
return dayPickerMonths.reverse();
diff --git a/src/helpers/getNextFocus.test.tsx b/src/helpers/getNextFocus.test.tsx
index 4207d8d226..c0baac8502 100644
--- a/src/helpers/getNextFocus.test.tsx
+++ b/src/helpers/getNextFocus.test.tsx
@@ -1,14 +1,13 @@
import React from "react";
import { CalendarDay } from "../classes";
-import type { MoveFocusBy, MoveFocusDir } from "../contexts/focus";
-import type { PropsContext } from "../contexts/props";
-import type { Mode } from "../types";
+import type { PropsContextValue } from "../contexts/props";
+import type { MoveFocusBy, MoveFocusDir } from "../types";
import { getNextFocus } from "./getNextFocus";
const props: Pick<
- PropsContext,
+ PropsContextValue,
"disabled" | "hidden" | "startMonth" | "endMonth"
> = {
disabled: [],
diff --git a/src/helpers/getNextFocus.tsx b/src/helpers/getNextFocus.tsx
index ce98e72f2c..74f645ea00 100644
--- a/src/helpers/getNextFocus.tsx
+++ b/src/helpers/getNextFocus.tsx
@@ -1,15 +1,14 @@
import React from "react";
import { CalendarDay } from "../classes";
-import type { MoveFocusBy, MoveFocusDir } from "../contexts/focus";
-import type { PropsContext } from "../contexts/props";
-import type { Mode } from "../types";
+import type { PropsContextValue } from "../contexts/props";
+import type { MoveFocusBy, MoveFocusDir, Mode } from "../types";
import { dateMatchModifiers } from "../utils/dateMatchModifiers";
import { getPossibleFocusDate } from "./getPossibleFocusDate";
export type Options = Pick<
- PropsContext,
+ PropsContextValue,
| "modifiers"
| "locale"
| "ISOWeek"
@@ -24,7 +23,7 @@ export function getNextFocus(
/** The date that is currently focused. */
focused: CalendarDay,
options: Pick<
- PropsContext,
+ PropsContextValue,
| "disabled"
| "hidden"
| "modifiers"
diff --git a/src/helpers/getNextMonth.test.ts b/src/helpers/getNextMonth.test.ts
index dc18c9d76a..ac4e8b3cea 100644
--- a/src/helpers/getNextMonth.test.ts
+++ b/src/helpers/getNextMonth.test.ts
@@ -9,6 +9,7 @@ describe("when number of months is 1", () => {
const disableNavigation = true;
it("the next month is undefined", () => {
const result = getNextMonth(startingMonth, {
+ numberOfMonths: 1,
disableNavigation,
endMonth: undefined,
startMonth: undefined
@@ -20,6 +21,7 @@ describe("when number of months is 1", () => {
const endMonth = addMonths(startingMonth, 3);
it("the next month is not undefined", () => {
const result = getNextMonth(startingMonth, {
+ numberOfMonths: 1,
endMonth,
startMonth: undefined
});
@@ -31,6 +33,7 @@ describe("when number of months is 1", () => {
const endMonth = startingMonth;
it("the next month is undefined", () => {
const result = getNextMonth(startingMonth, {
+ numberOfMonths: 1,
endMonth,
startMonth: undefined
});
diff --git a/src/helpers/getNextMonth.ts b/src/helpers/getNextMonth.ts
index 0dc0009962..038b0a591c 100644
--- a/src/helpers/getNextMonth.ts
+++ b/src/helpers/getNextMonth.ts
@@ -2,6 +2,8 @@ import { addMonths } from "date-fns/addMonths";
import { differenceInCalendarMonths } from "date-fns/differenceInCalendarMonths";
import { startOfMonth } from "date-fns/startOfMonth";
+import { PropsContextValue } from "../contexts/props";
+
/**
* Return the next month the user can navigate to according to the given
* options.
@@ -12,28 +14,31 @@ import { startOfMonth } from "date-fns/startOfMonth";
* - If the navigation is paged , is the number of months displayed ahead.
*/
export function getNextMonth(
- firstMonth: Date,
- options: {
- numberOfMonths?: number;
- startMonth: Date | undefined;
- endMonth: Date | undefined;
- pagedNavigation?: boolean;
- today?: Date;
- disableNavigation?: boolean;
- }
+ firstDisplayedMonth: Date,
+ props: Pick<
+ PropsContextValue,
+ | "startMonth"
+ | "endMonth"
+ | "numberOfMonths"
+ | "pagedNavigation"
+ | "disableNavigation"
+ >
): Date | undefined {
- if (options.disableNavigation) {
+ if (props.disableNavigation) {
return undefined;
}
- const { endMonth, pagedNavigation, numberOfMonths = 1 } = options;
+ const { pagedNavigation, numberOfMonths } = props;
const offset = pagedNavigation ? numberOfMonths : 1;
- const month = startOfMonth(firstMonth);
+ const month = startOfMonth(firstDisplayedMonth);
- if (!endMonth) {
+ if (!props.endMonth) {
return addMonths(month, offset);
}
- const monthsDiff = differenceInCalendarMonths(endMonth, firstMonth);
+ const monthsDiff = differenceInCalendarMonths(
+ props.endMonth,
+ firstDisplayedMonth
+ );
if (monthsDiff < numberOfMonths) {
return undefined;
diff --git a/src/helpers/getPossibleFocusDate.test.ts b/src/helpers/getPossibleFocusDate.test.ts
index 7e03f20607..78e8f67f5e 100644
--- a/src/helpers/getPossibleFocusDate.test.ts
+++ b/src/helpers/getPossibleFocusDate.test.ts
@@ -11,15 +11,14 @@ import {
endOfWeek
} from "date-fns";
-import type { MoveFocusBy, MoveFocusDir } from "../contexts/focus";
-import type { PropsContext } from "../contexts/props";
-import type { Mode } from "../types";
+import type { PropsContextValue } from "../contexts/props";
+import type { MoveFocusBy, MoveFocusDir, Mode } from "../types";
import { getPossibleFocusDate } from "./getPossibleFocusDate";
const baseDate = new Date(2023, 0, 1); // Jan 1, 2023
const options: Pick<
- PropsContext,
+ PropsContextValue,
"locale" | "ISOWeek" | "weekStartsOn" | "startMonth" | "endMonth"
> = {
locale: undefined,
diff --git a/src/helpers/getPossibleFocusDate.ts b/src/helpers/getPossibleFocusDate.ts
index f57e6d5ee5..3170b70024 100644
--- a/src/helpers/getPossibleFocusDate.ts
+++ b/src/helpers/getPossibleFocusDate.ts
@@ -9,9 +9,9 @@ import { min } from "date-fns/min";
import { startOfISOWeek } from "date-fns/startOfISOWeek";
import { startOfWeek } from "date-fns/startOfWeek";
-import type { MoveFocusBy, MoveFocusDir } from "../contexts/focus";
-import type { PropsContext } from "../contexts/props";
-import type { Mode } from "../types";
+import type { PropsContextValue } from "../contexts/props";
+import type { MoveFocusBy, MoveFocusDir } from "../types";
+import { Mode } from "../types";
/** Return the next date that should be focused. */
export function getPossibleFocusDate(
@@ -19,7 +19,7 @@ export function getPossibleFocusDate(
moveDir: MoveFocusDir,
focusedDate: Date,
options: Pick<
- PropsContext,
+ PropsContextValue,
"locale" | "ISOWeek" | "weekStartsOn" | "startMonth" | "endMonth"
>
): Date {
diff --git a/src/helpers/getPreviousMonth.test.ts b/src/helpers/getPreviousMonth.test.ts
index c8d568f037..ee00696b3e 100644
--- a/src/helpers/getPreviousMonth.test.ts
+++ b/src/helpers/getPreviousMonth.test.ts
@@ -1,69 +1,57 @@
-import { addMonths, isSameMonth } from "date-fns";
-
import { getPreviousMonth } from "./getPreviousMonth";
-const startingMonth = new Date(2020, 4, 31);
+it("should return undefined if navigation is disabled", () => {
+ const firstDisplayedMonth = new Date(2022, 0, 1); // January 2022
+ const props = {
+ disableNavigation: true,
+ pagedNavigation: false,
+ numberOfMonths: 1,
+ startMonth: new Date(2022, 0, 1)
+ };
+
+ const result = getPreviousMonth(firstDisplayedMonth, props);
-describe("when number of months is 1", () => {
- describe("when the navigation is disabled", () => {
- const disableNavigation = true;
- it("the previous month is undefined", () => {
- const result = getPreviousMonth(startingMonth, {
- disableNavigation,
- startMonth: undefined,
- endMonth: undefined
- });
- expect(result).toBe(undefined);
- });
- });
- describe("when in the navigable range", () => {
- const startMonth = addMonths(startingMonth, -3);
- it("the previous month is not undefined", () => {
- const result = getPreviousMonth(startingMonth, {
- startMonth,
- endMonth: undefined
- });
- const expectedPrevMonth = addMonths(startingMonth, -1);
- expect(result && isSameMonth(result, expectedPrevMonth)).toBeTruthy();
- });
- });
- describe("when not in the navigable range", () => {
- const startMonth = startingMonth;
- it("the previous month is undefined", () => {
- const result = getPreviousMonth(startingMonth, {
- startMonth,
- endMonth: undefined
- });
- expect(result).toBe(undefined);
- });
- });
+ expect(result).toBeUndefined();
});
-describe("when displaying 3 months", () => {
- const numberOfMonths = 3;
- describe("when the navigation is paged", () => {
- const pagedNavigation = true;
- it("the previous month is 3 months back", () => {
- const result = getPreviousMonth(startingMonth, {
- numberOfMonths,
- pagedNavigation,
- startMonth: undefined,
- endMonth: undefined
- });
- const expectedPrevMonth = addMonths(startingMonth, -numberOfMonths);
- expect(result && isSameMonth(result, expectedPrevMonth)).toBeTruthy();
- });
- });
- describe("when the navigation is not paged", () => {
- const pagedNavigation = false;
- it("the previous month is 1 months back", () => {
- const result = getPreviousMonth(startingMonth, {
- numberOfMonths,
- pagedNavigation,
- startMonth: undefined,
- endMonth: undefined
- });
- const expectedPrevMonth = addMonths(startingMonth, -1);
- expect(result && isSameMonth(result, expectedPrevMonth)).toBeTruthy();
- });
- });
+
+it("should return the previous month if startMonth is not provided", () => {
+ const firstDisplayedMonth = new Date(2022, 1, 1); // February 2022
+ const props = {
+ disableNavigation: false,
+ pagedNavigation: false,
+ numberOfMonths: 1,
+ startMonth: undefined
+ };
+
+ const result = getPreviousMonth(firstDisplayedMonth, props);
+
+ expect(result).toEqual(new Date(2022, 0, 1)); // January 2022
+});
+
+it("should return undefined if the previous month is before the startMonth", () => {
+ const firstDisplayedMonth = new Date(2022, 0, 1); // January 2022
+ const props = {
+ disableNavigation: false,
+ pagedNavigation: false,
+ numberOfMonths: 1,
+ startMonth: new Date(2022, 0, 1)
+ };
+
+ const result = getPreviousMonth(firstDisplayedMonth, props);
+
+ expect(result).toBeUndefined();
+});
+
+it("should return the correct previous month when pagedNavigation is true", () => {
+ const firstDisplayedMonth = new Date(2022, 2, 1); // March 2022
+ const props = {
+ disableNavigation: false,
+ pagedNavigation: true,
+ numberOfMonths: 2,
+ startMonth: new Date(2022, 0, 1)
+ };
+
+ const result = getPreviousMonth(firstDisplayedMonth, props);
+
+ expect(result).toEqual(new Date(2022, 0, 1)); // January 2022
});
diff --git a/src/helpers/getPreviousMonth.ts b/src/helpers/getPreviousMonth.ts
index 836b692753..268ba82099 100644
--- a/src/helpers/getPreviousMonth.ts
+++ b/src/helpers/getPreviousMonth.ts
@@ -2,6 +2,8 @@ import { addMonths } from "date-fns/addMonths";
import { differenceInCalendarMonths } from "date-fns/differenceInCalendarMonths";
import { startOfMonth } from "date-fns/startOfMonth";
+import type { PropsContextValue } from "../contexts/props";
+
/**
* Return the next previous the user can navigate to, according to the given
* options.
@@ -14,25 +16,21 @@ import { startOfMonth } from "date-fns/startOfMonth";
*/
export function getPreviousMonth(
firstDisplayedMonth: Date,
- options: {
- numberOfMonths?: number;
- startMonth: Date | undefined;
- endMonth: Date | undefined;
- pagedNavigation?: boolean;
- today?: Date;
- disableNavigation?: boolean;
- }
+ props: Pick<
+ PropsContextValue,
+ "startMonth" | "numberOfMonths" | "pagedNavigation" | "disableNavigation"
+ >
): Date | undefined {
- if (options.disableNavigation) {
+ if (props.disableNavigation) {
return undefined;
}
- const { startMonth, pagedNavigation, numberOfMonths = 1 } = options;
+ const { pagedNavigation, numberOfMonths } = props;
const offset = pagedNavigation ? numberOfMonths : 1;
const month = startOfMonth(firstDisplayedMonth);
- if (!startMonth) {
+ if (!props.startMonth) {
return addMonths(month, -offset);
}
- const monthsDiff = differenceInCalendarMonths(month, startMonth);
+ const monthsDiff = differenceInCalendarMonths(month, props.startMonth);
if (monthsDiff <= 0) {
return undefined;
diff --git a/src/helpers/getStartEndMonths.ts b/src/helpers/getStartEndMonths.ts
index 264ee133c3..184e219af3 100644
--- a/src/helpers/getStartEndMonths.ts
+++ b/src/helpers/getStartEndMonths.ts
@@ -5,7 +5,7 @@ import { startOfDay } from "date-fns/startOfDay";
import { startOfMonth } from "date-fns/startOfMonth";
import { startOfYear } from "date-fns/startOfYear";
-import type { DayPickerProps, Mode } from "../types";
+import type { Mode, DayPickerProps } from "../types";
/**
* Return the `fromMonth` and `toMonth` prop values values parsing the DayPicker
@@ -13,7 +13,7 @@ import type { DayPickerProps, Mode } from "../types";
*/
export function getStartEndMonths(
props: Pick<
- DayPickerProps,
+ DayPickerProps,
| "startMonth"
| "endMonth"
| "today"
@@ -24,7 +24,7 @@ export function getStartEndMonths(
| "fromMonth"
| "toMonth"
>
-): Pick, "startMonth" | "endMonth"> {
+): Pick {
let { startMonth, endMonth } = props;
// Handle deprecated code
diff --git a/src/helpers/getStyleForModifiers.test.ts b/src/helpers/getStyleForModifiers.test.ts
index 0a33bc2e04..a0fc7666dc 100644
--- a/src/helpers/getStyleForModifiers.test.ts
+++ b/src/helpers/getStyleForModifiers.test.ts
@@ -2,7 +2,7 @@ import type { CSSProperties } from "react";
// Update the path as needed
import { UI } from "../UI";
-import type { DayModifiers, ModifiersStyles, Styles } from "../types";
+import type { Modifiers, ModifiersStyles, Styles } from "../types";
import { getStyleForModifiers } from "./getStyleForModifiers";
@@ -11,7 +11,7 @@ const baseDayStyle: CSSProperties = {
color: "black"
};
const styles: Partial = { [UI.Day]: baseDayStyle };
-const defaultModifiers: DayModifiers = {
+const defaultModifiers: Modifiers = {
disabled: false,
hidden: false,
outside: false,
@@ -33,7 +33,7 @@ test("returns base style when no modifiers are provided", () => {
});
test("applies modifier styles to the base style", () => {
- const dayModifiers: DayModifiers = {
+ const dayModifiers: Modifiers = {
...defaultModifiers,
selected: true,
disabled: false
@@ -52,7 +52,7 @@ test("applies modifier styles to the base style", () => {
});
test("ignores modifiers that are not active", () => {
- const dayModifiers: DayModifiers = {
+ const dayModifiers: Modifiers = {
...defaultModifiers,
selected: false,
disabled: true
@@ -67,7 +67,7 @@ test("ignores modifiers that are not active", () => {
});
test("combines multiple active modifier styles", () => {
- const dayModifiers: DayModifiers = {
+ const dayModifiers: Modifiers = {
...defaultModifiers,
selected: true,
highlighted: true
@@ -88,7 +88,7 @@ test("combines multiple active modifier styles", () => {
});
test("applies the most recent modifier style when there are conflicts", () => {
- const dayModifiers: DayModifiers = {
+ const dayModifiers: Modifiers = {
...defaultModifiers,
selected: true,
highlighted: true
diff --git a/src/helpers/getStyleForModifiers.ts b/src/helpers/getStyleForModifiers.ts
index 497633d5dd..73bc6b435e 100644
--- a/src/helpers/getStyleForModifiers.ts
+++ b/src/helpers/getStyleForModifiers.ts
@@ -1,10 +1,10 @@
import type { CSSProperties } from "react";
import { UI } from "../UI";
-import type { DayModifiers, ModifiersStyles, Styles } from "../types";
+import type { Modifiers, ModifiersStyles, Styles } from "../types";
export function getStyleForModifiers(
- dayModifiers: DayModifiers,
+ dayModifiers: Modifiers,
modifiersStyles: Partial,
styles: Partial
): CSSProperties {
diff --git a/src/index.ts b/src/index.ts
index 6ea5bb36df..3a113307d6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,16 +1,19 @@
export * from "./DayPicker";
+export * from "./types";
export * from "./classes";
export * from "./components/custom-components";
+
export * from "./contexts/calendar";
export * from "./contexts/focus";
-export * from "./contexts/props";
-export * from "./contexts/selection";
export * from "./contexts/modifiers";
+export * from "./contexts/props";
+export * from "./selection/single";
+export * from "./selection/multi";
+export * from "./selection/range";
+
export * from "./formatters";
export * from "./helpers";
export * from "./labels";
-export * from "./types-deprecated";
-export * from "./types";
export * from "./utils";
export * from "./UI";
diff --git a/src/labels/labelDay.test.ts b/src/labels/labelDay.test.ts
index 2d84efffd7..f66ea3214a 100644
--- a/src/labels/labelDay.test.ts
+++ b/src/labels/labelDay.test.ts
@@ -1,11 +1,11 @@
import { es } from "date-fns/locale/es";
-import type { DayModifiers } from "../types";
+import type { Modifiers } from "../types";
import { labelDay } from "./labelDay";
const day = new Date(2022, 10, 21);
-const dayModifiers: DayModifiers = {
+const dayModifiers: Modifiers = {
disabled: false,
focusable: false,
focused: false,
diff --git a/src/labels/labelDay.ts b/src/labels/labelDay.ts
index 19a709ed5a..e5e6c4c47a 100644
--- a/src/labels/labelDay.ts
+++ b/src/labels/labelDay.ts
@@ -1,6 +1,6 @@
import { format } from "date-fns/format";
-import type { DayModifiers } from "../types";
+import type { Modifiers } from "../types";
/**
* Return an ARIA label for the day button. By default, it returns an empty
@@ -13,7 +13,7 @@ import type { DayModifiers } from "../types";
*/
export function labelDay(
date: Date,
- modifiers: DayModifiers,
+ modifiers: Modifiers,
options: Parameters[2]
) {
return "";
diff --git a/src/selection/multi.tsx b/src/selection/multi.tsx
new file mode 100644
index 0000000000..b52df65461
--- /dev/null
+++ b/src/selection/multi.tsx
@@ -0,0 +1,110 @@
+import React from "react";
+
+import { isSameDay } from "date-fns/isSameDay";
+
+import type { Modifiers, PropsMulti } from "../types";
+
+export type MultiContextValue = {
+ setSelected: (
+ triggerDate: Date,
+ modifiers: Modifiers,
+ e: React.MouseEvent | React.KeyboardEvent
+ ) => Date[] | undefined;
+ isSelected: (date: Date) => boolean;
+} & (T extends { required: true }
+ ? {
+ selected: Date[];
+ }
+ : {
+ selected: Date[] | undefined;
+ });
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const MultiContext = React.createContext | undefined>(
+ undefined
+);
+
+function useMulti({
+ required = false,
+ min = undefined,
+ max = undefined,
+ selected,
+ onSelect
+}: T): MultiContextValue {
+ const [dates, setDates] = React.useState(selected);
+
+ // Update the selected date if the required flag is set.
+ React.useEffect(() => {
+ if (required && dates === undefined) setDates([new Date()]);
+ }, [required, dates]);
+
+ // Update the selected date if the initialDates changes.
+ React.useEffect(() => {
+ if (selected) setDates(selected);
+ }, [selected]);
+
+ const isSelected = (date: Date) =>
+ dates?.some((d) => isSameDay(d, date)) ?? false;
+
+ const setSelected = (
+ triggerDate: Date,
+ modifiers: Modifiers,
+ e: React.MouseEvent | React.KeyboardEvent
+ ) => {
+ let newDates = [...(dates ?? [])];
+ if (isSelected(triggerDate)) {
+ if (dates?.length === min) {
+ // Min value reached, do nothing
+ return;
+ }
+ if (required && dates?.length === 1) {
+ // Required value already selected do nothing
+ return;
+ }
+ const newDates = dates?.filter((d) => !isSameDay(d, triggerDate));
+ setDates(newDates);
+ } else {
+ if (dates?.length === max) {
+ // Max value reached, reset the selection to date
+ newDates = [triggerDate];
+ } else {
+ // Add the date to the selection
+ newDates = [...newDates, triggerDate];
+ }
+ }
+ onSelect?.(newDates, triggerDate, modifiers, e);
+ setDates(newDates);
+ return newDates;
+ };
+
+ return {
+ selected: dates,
+ setSelected,
+ isSelected
+ } as MultiContextValue;
+}
+
+/** @private */
+export function MultiProvider(props: React.PropsWithChildren) {
+ const value = useMulti(props);
+ return (
+
+ {props.children}
+
+ );
+}
+
+/**
+ * Access to the multi context to get the selected dates or update them.
+ *
+ * @group Contexts
+ */
+export function useMultiContext() {
+ const context = React.useContext(MultiContext);
+ if (!context) {
+ throw new Error(
+ "useMultiContext must be used within a MultiContextProvider."
+ );
+ }
+ return context as MultiContextValue;
+}
diff --git a/src/selection/range.tsx b/src/selection/range.tsx
new file mode 100644
index 0000000000..416e02086f
--- /dev/null
+++ b/src/selection/range.tsx
@@ -0,0 +1,117 @@
+import React from "react";
+
+import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays";
+
+import { DateRange, Modifiers, PropsRange } from "../types";
+import { addToRange, isDateRange } from "../utils";
+import { isDateInRange } from "../utils/isDateInRange";
+
+export type RangeContextValue = {
+ setSelected: (
+ triggerDate: Date,
+ modifiers: Modifiers,
+ e: React.MouseEvent | React.KeyboardEvent
+ ) => DateRange | undefined;
+ isSelected: (date: Date) => boolean;
+} & (T extends { required: true }
+ ? {
+ selected: DateRange;
+ }
+ : {
+ selected: DateRange | undefined;
+ });
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const RangeContext = React.createContext | undefined>(
+ undefined
+);
+
+function useRange({
+ required,
+ min,
+ max,
+ selected,
+ onSelect
+}: T): RangeContextValue {
+ const [range, setRange] = React.useState(selected);
+
+ // Update the selected date if the required flag is set.
+ React.useEffect(() => {
+ if (required && range === undefined)
+ setRange({ from: undefined, to: undefined });
+ }, [required, range]);
+
+ // Update the selected date if the selected changes.
+ React.useEffect(() => {
+ if (selected) setRange(selected);
+ }, [selected]);
+
+ const isSelected = required
+ ? (date: Date) => isDateInRange(date, range as DateRange)
+ : (date: Date) => range && isDateInRange(date, range);
+
+ const setSelected = (
+ triggerDate: Date,
+ modifiers: Modifiers,
+ e: React.MouseEvent | React.KeyboardEvent
+ ) => {
+ const newRange = addToRange(triggerDate, range);
+
+ if (min) {
+ if (
+ newRange?.from &&
+ newRange.to &&
+ differenceInCalendarDays(newRange.to, newRange.from) <= min
+ ) {
+ newRange.from = triggerDate;
+ newRange.to = undefined;
+ }
+ }
+
+ if (max) {
+ if (
+ newRange?.from &&
+ newRange.to &&
+ differenceInCalendarDays(newRange.to, newRange.from) + 1 > max
+ ) {
+ newRange.from = triggerDate;
+ newRange.to = undefined;
+ }
+ }
+
+ setRange(newRange);
+ onSelect?.(newRange, triggerDate, modifiers, e);
+ return newRange;
+ };
+
+ return {
+ selected: range,
+ setSelected,
+ isSelected
+ } as RangeContextValue;
+}
+
+/** @private */
+export function RangeProvider(props: React.PropsWithChildren) {
+ const value = useRange(props);
+ return (
+
+ {props.children}
+
+ );
+}
+
+/**
+ * Access to the range context to get the selected range or update it.
+ *
+ * @group Contexts
+ */
+export function useRangeContext() {
+ const context = React.useContext(RangeContext);
+ if (!context) {
+ throw new Error(
+ "useRangeContext() must be used within a RangeContextProvider."
+ );
+ }
+ return context as RangeContextValue;
+}
diff --git a/src/selection/single.tsx b/src/selection/single.tsx
new file mode 100644
index 0000000000..cb862f7f7e
--- /dev/null
+++ b/src/selection/single.tsx
@@ -0,0 +1,84 @@
+import React from "react";
+
+import { isSameDay } from "date-fns/isSameDay";
+
+import { Modifiers, PropsSingle } from "../types";
+
+export type SingleContextValue = {
+ setSelected: (
+ triggerDate: Date,
+ modifiers: Modifiers,
+ e: React.MouseEvent | React.KeyboardEvent
+ ) => void;
+ isSelected: (date: Date) => boolean;
+} & (T extends { required: true }
+ ? {
+ selected: Date;
+ }
+ : {
+ selected: Date | undefined;
+ });
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const SingleContext = React.createContext | undefined>(
+ undefined
+);
+
+function useSingle({
+ required = false,
+ selected,
+ onSelect
+}: T): SingleContextValue {
+ const [date, setDate] = React.useState(selected);
+
+ // Update the selected date if the required flag is set.
+ React.useEffect(() => {
+ if (required && date === undefined) setDate(new Date());
+ }, [required, date]);
+
+ // Update the selected date if the selected changes.
+ React.useEffect(() => {
+ if (selected) setDate(selected);
+ }, [selected]);
+
+ const isSelected = (compareDate: Date) =>
+ date ? isSameDay(date, compareDate) : false;
+
+ const setSelected = (
+ triggerDate: Date,
+ modifiers: Modifiers,
+ e: React.MouseEvent | React.KeyboardEvent
+ ) => {
+ let newDate: Date | undefined = triggerDate;
+ if (!required && date && date && isSameDay(triggerDate, date)) {
+ // If the date is the same, clear the selection.
+ newDate = undefined;
+ }
+ setDate(newDate);
+ onSelect?.(newDate, triggerDate, modifiers, e);
+ return newDate;
+ };
+
+ return { selected: date, setSelected, isSelected } as SingleContextValue;
+}
+
+/** @private */
+export function SingleProvider(props: React.PropsWithChildren) {
+ const value = useSingle(props);
+ return (
+
+ {props.children}
+
+ );
+}
+
+/** @group Contexts */
+export function useSingleContext() {
+ const context = React.useContext(SingleContext);
+ if (!context) {
+ throw new Error(
+ "useSingleContext must be used within a SingleContextProvider"
+ );
+ }
+ return context;
+}
diff --git a/src/types.test.tsx b/src/types.test.tsx
deleted file mode 100644
index 0c8b35fc8a..0000000000
--- a/src/types.test.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import React from "react";
-
-import { DayPicker } from "./DayPicker";
-
-const Test = () => {
- return (
- <>
-
-
- {}}
- />
- {}}
- />
- {/** @ts-expect-error Wrong selected prop */}
-
- {}}
- />
-
- {/** @ts-expect-error Extra `selected` */}
-
-
- {}}
- />
- {}}
- />
- {/* */}
- {/* console.log(1)} /> */}
- >
- );
-};
-
-it("should type-check", () => {
- expect(true).toBeTruthy();
-});
diff --git a/src/types.ts b/src/types.ts
deleted file mode 100644
index b521bedcd5..0000000000
--- a/src/types.ts
+++ /dev/null
@@ -1,699 +0,0 @@
-import type {
- MouseEvent,
- FocusEvent,
- KeyboardEvent,
- TouchEvent,
- PointerEvent,
- CSSProperties,
- ReactNode
-} from "react";
-
-import type { Locale } from "date-fns";
-
-import {
- UI,
- DayModifier,
- CalendarFlag,
- ChevronFlag,
- WeekNumberFlag
-} from "./UI";
-import { CalendarDay } from "./classes";
-import * as components from "./components/custom-components";
-import {
- formatCaption,
- formatDay,
- formatMonthCaption,
- formatMonthDropdown,
- formatWeekdayName,
- formatWeekNumber,
- formatYearCaption,
- formatYearDropdown
-} from "./formatters";
-import {
- labelDay,
- labelCaption,
- labelMonthDropdown,
- labelNext,
- labelPrevious,
- labelWeekday,
- labelWeekNumber,
- labelWeekNumberHeader,
- labelYearDropdown
-} from "./labels";
-
-/**
- * The props for the {@link DayPicker} component.
- *
- * @template T - The selection mode. Defaults to `"default"`.
- * @template R - Whether the selection is required. Defaults to `false`.
- * @group Props
- */
-export type DayPickerProps<
- T extends Mode = "default",
- R extends boolean = false
-> = PropsBase & {
- /** The selection mode. */
- mode?: T | undefined;
- /** Makes the selection required. */
- required?: R | undefined;
-} & {
- /** Props for the single selection mode. */
- single: PropsSingle;
- /** Props for the multi selection mode. */
- multiple: PropsMulti;
- /** Props for the range selection mode. */
- range: PropsRange;
- default: object;
- }[T];
-
-/**
- * Selection modes supported by DayPicker.
- *
- * - `single`: use DayPicker to select single days.
- * - `multiple`: allow selecting multiple days.
- * - `range`: use DayPicker to select a range of days
- * - `default`: disable any built-in selection behavior. Customize what is
- * selected by using `onDayClick` and `modifiers`.
- *
- * @see https://react-day-picker.js.org/next/using-daypicker/selection-modes
- */
-export type Mode = "single" | "multiple" | "range" | "default";
-
-/**
- * Props to change the navigation, the styling and the behavior of the calendar.
- *
- * @group Props
- */
-export interface PropsBase {
- /** Class name to add to the root element */
- className?: string;
- /**
- * Change the class names used by DayPicker.
- *
- * Use this prop when you need to change the default class names — for example
- * when importing the style via CSS modules or when using a CSS framework.
- */
- classNames?: Partial;
- /** Change the class name for the day matching the `modifiers`. */
- modifiersClassNames?: ModifiersClassNames;
- /** Style to apply to the root element. */
- style?: CSSProperties;
- /** Change the inline styles of the HTML elements. */
- styles?: Partial;
- /** Change the class name for the day matching the {@link modifiers}. */
- modifiersStyles?: ModifiersStyles;
- /**
- * A unique id to replace the random generated ids – used by DayPicker for
- * accessibility.
- */
- id?: string;
- /**
- * The initial month to show in the calendar.
- *
- * Use this prop to let DayPicker control the current month. If you need to
- * set the month programmatically, use {@link month} and {@link onMonthChange}.
- *
- * @defaultValue The current month
- */
- defaultMonth?: Date;
- /**
- * The month displayed in the calendar.
- *
- * As opposed to `PropsBase.defaultMonth`, use this prop with
- * `PropsBase.onMonthChange} to change the month programmatically.
- */
- month?: Date;
- /**
- * The number of displayed months.
- *
- * @defaultValue 1
- */
- numberOfMonths?: number;
- /**
- * The earliest month to start the month navigation.
- *
- * @since 9.0.0
- */
- startMonth?: Date | undefined;
- /**
- * @private
- * @deprecated This prop has been removed. Use `hidden={{ before: date }}`
- * instead.
- */
- fromDate?: Date | undefined;
- /**
- * @private
- * @deprecated This prop has been renamed to `startMonth`.
- */
- fromMonth?: Date | undefined;
- /**
- * @private
- * @deprecated Use `startMonth` instead. E.g. `startMonth={new Date(year,
- * 0)}`.
- */
- fromYear?: number | undefined;
-
- /**
- * The latest month to end the month navigation.
- *
- * @since 9.0.0
- */
- endMonth?: Date;
- /**
- * @private
- * @deprecated This prop has been removed. Use `hidden={{ after: date }}`
- * instead.
- */
- toDate?: Date;
- /**
- * @private
- * @deprecated This prop has been renamed to `endMonth`.
- */
- toMonth?: Date;
- /**
- * @private
- * @deprecated Use `endMonth` instead. E.g. `endMonth={new Date(year, 0)}`.
- */
- toYear?: number;
-
- /** Paginate the month navigation displaying the `numberOfMonths` at time. */
- pagedNavigation?: boolean;
- /**
- * Render the months in reversed order (when {@link numberOfMonths} is set) to
- * display the most recent month first.
- */
- reverseMonths?: boolean;
- /**
- * Hide the navigation buttons. This prop won't disable the navigation: to
- * disable the navigation, use {@link disableNavigation}.
- *
- * @since 9.0.0
- */
- hideNavigation?: boolean;
- /**
- * Disable the navigation between months. This prop won't hide the navigation:
- * to hide the navigation, use {@link hideNavigation}.
- */
- disableNavigation?: boolean;
- /**
- * Show dropdowns to navigate between months or years.
- *
- * - `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 set the start/end months
- * {@link fromYear} to the 100 years ago, and {@link toYear} to the current
- * 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.
- */
- fixedWeeks?: boolean;
- /**
- * Hide the row displaying the weekday row header.
- *
- * @since 9.0.0
- */
- hideWeekdayRow?: boolean;
- /** Show the outside days (days falling in the next or the previous month). */
- showOutsideDays?: boolean;
- /**
- * Show the week numbers column. Weeks are numbered according to the local
- * week index.
- *
- * - To use ISO week numbering, use the `ISOWeek` prop.
- * - To change how the week numbers are displayed, use the `formatters` prop.
- */
- showWeekNumber?: boolean;
- /**
- * Use ISO week dates instead of the locale setting. Setting this prop will
- * ignore `weekStartsOn` and `firstWeekContainsDate`.
- *
- * @see https://en.wikipedia.org/wiki/ISO_week_date
- */
- ISOWeek?: boolean;
- /** Change the components used for rendering the calendar elements. */
- components?: CustomComponents;
- /** Content to add to the grid as footer element. */
- footer?: ReactNode;
- /**
- * When a selection mode is set, DayPicker will focus the first selected day
- * (if set) or the today's date (if not disabled).
- *
- * Use this prop when you need to focus DayPicker after a user actions, for
- * improved accessibility.
- */
- autoFocus?: boolean;
- /** Apply the `disabled` modifier to the matching days. */
- disabled?: Matcher | Matcher[] | undefined;
- /**
- * Apply the `hidden` modifier to the matching days. Will hide them from the
- * calendar.
- */
- hidden?: Matcher | Matcher[] | undefined;
- /**
- * The today’s date. Default is the current date. This date will get the
- * `today` modifier to style the day.
- */
- today?: Date;
- /** Add modifiers to the matching days. */
- modifiers?: Record | undefined;
- /**
- * Labels creators to override the defaults. Use this prop to customize the
- * aria-label attributes in DayPicker.
- */
- labels?: Partial;
- /**
- * Formatters used to format dates to strings. Use this prop to override the
- * default functions.
- */
- formatters?: Partial;
- /**
- * The text direction of the calendar. Use `ltr` for left-to-right (default)
- * or `rtl` for right-to-left.
- */
- dir?: HTMLDivElement["dir"];
- /**
- * A cryptographic nonce ("number used once") which can be used by Content
- * Security Policy for the inline `style` attributes.
- */
- nonce?: HTMLDivElement["nonce"];
- /** Add a `title` attribute to the container element. */
- title?: HTMLDivElement["title"];
- /** Add the language tag to the container element. */
- lang?: HTMLDivElement["lang"];
- /**
- * The date-fns locale object used to localize dates.
- *
- * @defaultValue en-US
- * @see https://date-fns.org/docs/Locale
- */
- locale?: Locale | undefined;
- /**
- * The index of the first day of the week (0 - Sunday). Overrides the locale's
- * one.
- */
- weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined;
- /**
- * The day of January, which is always in the first week of the year.
- *
- * @see https://date-fns.org/docs/getWeek
- * @see https://en.wikipedia.org/wiki/Week#Numbering
- */
- firstWeekContainsDate?: 1 | 4;
- /**
- * Enable `DD` and `DDDD` for week year tokens when formatting or parsing
- * dates.
- *
- * @see https://date-fns.org/docs/Unicode-Tokens
- */
- useAdditionalWeekYearTokens?: boolean | undefined;
- /**
- * Enable `YY` and `YYYY` for day of year tokens when formatting or parsing
- * dates.
- *
- * @see https://date-fns.org/docs/Unicode-Tokens
- */
- useAdditionalDayOfYearTokens?: boolean | undefined;
-
- /* EVENT HANDLERS */
- /** Event fired when the user navigates between months. */
- onMonthChange?: MonthChangeEventHandler;
- /** Event handler when a day is clicked. */
- onDayClick?: DayEventHandler;
- /** Event handler when a day is focused. */
- onDayFocus?: DayEventHandler;
- /** Event handler when a day is blurred. */
- onDayBlur?: DayEventHandler;
- /** Event handler when the mouse enters a day. */
- onDayMouseEnter?: DayEventHandler;
- /** Event handler when the mouse leaves a day. */
- onDayMouseLeave?: DayEventHandler;
- /** Event handler when a key is pressed on a day. */
- onDayKeyDown?: DayEventHandler;
- /** Event handler when a key is released on a day. */
- onDayKeyUp?: DayEventHandler;
- /** Event handler when a key is pressed and released on a day. */
- onDayKeyPress?: DayEventHandler;
- /** Event handler when a pointer enters a day. */
- onDayPointerEnter?: DayEventHandler;
- /** Event handler when a pointer leaves a day. */
- onDayPointerLeave?: DayEventHandler;
- /** Event handler when a touch is cancelled on a day. */
- onDayTouchCancel?: DayEventHandler;
- /** Event handler when a touch ends on a day. */
- onDayTouchEnd?: DayEventHandler;
- /** Event handler when a touch moves on a day. */
- onDayTouchMove?: DayEventHandler;
- /** Event handler when a touch starts on a day. */
- onDayTouchStart?: DayEventHandler;
- /** Event handler when the next month button is clicked. */
- onNextClick?: MonthChangeEventHandler;
- /** Event handler when the previous month button is clicked. */
- onPrevClick?: MonthChangeEventHandler;
- /**
- * Event handler when a week number is clicked. Requires {@link showWeekNumber}
- * to be set.
- */
- onWeekNumberClick?: WeekNumberMouseEventHandler;
-}
-
-/**
- * Props for the single selection mode, when `mode="single"`.
- *
- * @template R - Whether the selection is required. Defaults to `false`.
- * @group Props
- */
-export interface PropsSingle {
- /** Makes the selection required. */
- required?: R | undefined;
- /** The selected Date. */
- selected?: Selected<"single", R>;
- /** The initially selected value when not controlled. */
- defaultSelected?: Selected<"single", R>;
- /** The callback called when the user selects a day. */
- onSelect?: SelectHandler<"single", R> | undefined;
-}
-
-/**
- * Props for the multi selection mode, when `mode="multiple"`.
- *
- * @template R - Whether the selection is required. Defaults to `false`.
- * @group Props
- */
-export interface PropsMulti {
- /** The selected dates. */
- selected?: Selected<"multiple", R>;
- /** The initially selected values when not controlled. */
- defaultSelected?: Selected<"multiple", R>;
- /** The callback called when the user selects a day. */
- onSelect?: SelectHandler<"multiple", R>;
- /** Makes the selection required. */
- required?: R | undefined;
- /** The minimum number of days that can be selected. */
- min?: number | undefined;
- /** The maximum number of days that can be selected. */
- max?: number | undefined;
-}
-
-/**
- * Props for the range selection mode, when `mode="range"`.
- *
- * @template R - Whether the selection is required. Defaults to `false`.
- * @group Props
- */
-export interface PropsRange {
- /** The selected range. */
- selected?: Selected<"range", R>;
- /** The initially selected range when not controlled. */
- defaultSelected?: Selected<"range", R>;
- /** The callback called when the user selects a day. */
- onSelect?: SelectHandler<"range", R>;
- /** Makes the selection required. */
- required?: R;
- /** The minimum number of days that can be selected. */
- min?: number;
- /** The maximum number of days that can be selected. */
- max?: number;
-}
-/**
- * Props for the default selection mode, when `mode="default"`.
- *
- * @group Props
- */
-export interface PropsDefault extends PropsBase {
- mode?: undefined | "default";
-}
-
-/**
- * The selected value when in selection mode.
- *
- * @template T - The {@link Mode | selection mode}. Defaults to `"default"`.
- * @template R - Whether the selection is required. Defaults to `false`.
- */
-export type Selected<
- T extends Mode = "default",
- R extends boolean = false
-> = T extends "single"
- ? Date | (R extends true ? Date : undefined)
- : T extends "multiple"
- ? Date[] | (R extends true ? Date[] : undefined)
- : T extends "range"
- ? DateRange | (R extends true ? DateRange : undefined)
- : undefined;
-
-/**
- * The callback called when the user select a days from the calendar. *
- *
- * @template T - The {@link Mode | selection mode}. Defaults to `"default"`.
- * @template R - Whether the selection is required. Defaults to `false`.
- */
-export type SelectHandler<
- T extends Mode = "default",
- R extends boolean = false
-> = (
- /** The new selected value. */
- selected: Selected,
- /** The date that triggered the selection. */
- date: Date,
- /** The modifiers for the day that triggered the selection. */
- modifiers: DayModifiers,
- /** The event that made the selection. */
- e: MouseEvent | KeyboardEvent
-) => void;
-
-/**
- * The components that can be changed using the `components` prop.
- *
- * @see https://github.com/gpbl/react-day-picker/blob/main/src/components/custom-components.ts
- */
-export type CustomComponents = {
- [key in keyof typeof components]?: (typeof components)[key];
-};
-
-/** Represent a map of formatters used to render localized content. */
-export type Formatters = {
- /** Format the caption of a month grid. */
- formatCaption: typeof formatCaption;
- /** @deprecated Use {@link Formatters.formatCaption} instead. */
- formatMonthCaption: typeof formatMonthCaption;
- /** Format the label in the month dropdown. */
- formatMonthDropdown: typeof formatMonthDropdown;
- /** @deprecated Use {@link Formatters.formatYearDropdown} instead. */
- formatYearCaption: typeof formatYearCaption;
- /** Format the label in the year dropdown. */
- formatYearDropdown: typeof formatYearDropdown;
- /** Format the day in the day cell. */
- formatDay: typeof formatDay;
- /** Format the week number. */
- formatWeekNumber: typeof formatWeekNumber;
- /** Format the week day name in the header */
- formatWeekdayName: typeof formatWeekdayName;
-};
-
-/** Map of functions to translate ARIA labels for the relative elements. */
-export type Labels = {
- /** Return the label for the month dropdown. */
- labelCaption: typeof labelCaption;
- /** Return the label for the month dropdown. */
- labelMonthDropdown: typeof labelMonthDropdown;
- /** Return the label for the year dropdown. */
- labelYearDropdown: typeof labelYearDropdown;
- /** Return the label for the next month button. */
- labelNext: typeof labelNext;
- /** Return the label for the previous month button. */
- labelPrevious: typeof labelPrevious;
- /** Return the label for the day cell. */
- labelDay: typeof labelDay;
- /** Return the label for the weekday. */
- labelWeekday: typeof labelWeekday;
- /** Return the label for the week number. */
- labelWeekNumber: typeof labelWeekNumber;
- /**
- * Return the label for the column of the week number.
- *
- * @since 9.0.0
- */
- labelWeekNumberHeader: typeof labelWeekNumberHeader;
-};
-
-/**
- * A value or a function that matches a specific day.
- *
- * Matchers can be of different types:
- *
- * ```tsx
- * // will always match the day
- * const booleanMatcher: Matcher = true;
- *
- * // will match the today's date
- * const dateMatcher: Matcher = new Date();
- *
- * // will match the days in the array
- * const arrayMatcher: Matcher = [
- * new Date(2019, 1, 2),
- * new Date(2019, 1, 4)
- * ];
- *
- * // will match days after the 2nd of February 2019
- * const afterMatcher: DateAfter = { after: new Date(2019, 1, 2) };
- * // will match days before the 2nd of February 2019 }
- * const beforeMatcher: DateBefore = { before: new Date(2019, 1, 2) };
- *
- * // will match Sundays
- * const dayOfWeekMatcher: DayOfWeek = {
- * dayOfWeek: 0
- * };
- *
- * // will match the included days, except the two dates
- * const intervalMatcher: DateInterval = {
- * after: new Date(2019, 1, 2),
- * before: new Date(2019, 1, 5)
- * };
- *
- * // will match the included days, including the two dates
- * const rangeMatcher: DateRange = {
- * from: new Date(2019, 1, 2),
- * to: new Date(2019, 1, 5)
- * };
- *
- * // will match when the function return true
- * const functionMatcher: Matcher = (day: Date) => {
- * return day.getMonth() === 2; // match when month is March
- * };
- * ```
- */
-export type Matcher =
- | boolean
- | ((date: Date) => boolean)
- | Date
- | Date[]
- | DateRange
- | DateBefore
- | DateAfter
- | DateInterval
- | DayOfWeek;
-
-/**
- * A matcher to match a day falling after the specified date, with the date not
- * included.
- */
-export type DateAfter = { after: Date };
-
-/**
- * A matcher to match a day falling before the specified date, with the date not
- * included.
- */
-export type DateBefore = { before: Date };
-
-/**
- * A matcher to match a day falling before and/or after two dates, where the
- * dates are not included.
- */
-export type DateInterval = { before: Date; after: Date };
-
-/**
- * A matcher to match a range of dates. The range can be open. Differently from
- * {@link DateInterval}, the dates here are included.
- */
-export type DateRange = { from: Date | undefined; to?: Date | undefined };
-
-/**
- * A matcher to match a date being one of the specified days of the week (`0-6`,
- * where `0` is Sunday).
- */
-export type DayOfWeek = { dayOfWeek: number[] };
-
-/** A record with `data-*` attributes passed to ``. */
-export type DataAttributes = Record<`data-${string}`, unknown>;
-
-/**
- * The event handler triggered when interacting with a day.
- *
- * @template T - The event type that triggered the day event.
- */
-export type DayEventHandler = (
- /** The date that has triggered the event. */
- date: Date,
- /** The modifiers belonging to the date. */
- modifiers: DayModifiers,
- /** The DOM event that triggered this event. */
- e: T
-) => void;
-
-/** The event handler when a month is changed in the calendar. */
-export type MonthChangeEventHandler = (month: Date) => void;
-
-/** The event handler when the week number is clicked. */
-export type WeekNumberMouseEventHandler = (
- /** The week number that has been clicked. */
- weekNumber: number,
- /** The dates in the clicked week. */
- dates: Date[],
- /** The mouse event that triggered this event. */
- e: MouseEvent
-) => void;
-
-/** Maps {@link UI} elements to their CSS properties. */
-export type Styles = {
- [uiElement in UI]: CSSProperties | undefined;
-};
-
-/**
- * Maps {@link UI}, {@link DayModifier}, {@link CalendarFlag}, {@link ChevronFlag},
- * and {@link WeekNumberFlag} elements to their class names.
- */
-export type ClassNames = {
- [key in
- | UI
- | DayModifier
- | CalendarFlag
- | ChevronFlag
- | WeekNumberFlag]: string;
-};
-
-/** The modifiers that are internally used by DayPicker. */
-export type InternalModifier = keyof typeof DayModifier;
-
-/** A map of all the modifiers with the calendar days. */
-export type CalendarModifiers = Record &
- Record;
-
-/** The modifiers that are matching the day in the calendar. */
-export type DayModifiers = Record &
- Record;
-
-/** The style to apply to each day element matching a modifier. */
-export type ModifiersStyles = Record &
- Partial>;
-
-/** The classnames to assign to each day element matching a modifier. */
-export type ModifiersClassNames = Record &
- Partial>;
-
-export type test = DayPickerProps;
-
-/**
- * The props that have been deprecated since version 9.0.0.
- *
- * @since 9.0.0
- * @see https://react-day-picker.js.org/next/upgrading
- */
-export type V9DeprecatedProps =
- /** Use `hidden` prop instead. */
- | "fromDate"
- /** Use `hidden` prop instead. */
- | "toDate"
- /** Use `startMonth` instead. */
- | "fromMonth"
- /** Use `endMonth` instead. */
- | "toMonth"
- /** Use `startMonth` instead. */
- | "fromYear"
- /** Use `endMonth` instead. */
- | "toYear";
diff --git a/src/types-deprecated.ts b/src/types/deprecated.ts
similarity index 80%
rename from src/types-deprecated.ts
rename to src/types/deprecated.ts
index dad669c212..bcdb764efb 100644
--- a/src/types-deprecated.ts
+++ b/src/types/deprecated.ts
@@ -1,19 +1,16 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
-import { Calendar } from "./components/Calendar";
-import { MonthCaption, MonthCaptionProps } from "./components/MonthCaption";
-import { Week, type WeekProps } from "./components/Week";
-import { useCalendar } from "./contexts/calendar";
-import { PropsContext, useProps } from "./contexts/props";
-import { labelDay, labelNext, labelWeekday, labelWeekNumber } from "./labels";
-import type {
- Mode,
- PropsSingle,
- PropsDefault,
- PropsMulti,
- PropsRange,
- SelectHandler,
- DayEventHandler
-} from "./types";
+import { Calendar } from "../components/Calendar";
+import {
+ MonthCaption,
+ type MonthCaptionProps
+} from "../components/MonthCaption";
+import { Week, type WeekProps } from "../components/Week";
+import { useCalendarContext } from "../contexts/calendar";
+import { usePropsContext, type PropsContextValue } from "../contexts/props";
+import { labelDay, labelNext, labelWeekday, labelWeekNumber } from "../labels";
+
+import type { PropsMulti, PropsRange, PropsSingle } from "./props";
+import type { Mode, DayEventHandler } from "./shared";
/**
* @deprecated This type will be removed.
@@ -75,25 +72,25 @@ export type RowProps = WeekProps;
* @deprecated This type has been renamed. Use `PropsSingle` instead.
* @protected
*/
-export type DayPickerSingleProps = PropsSingle;
+export type DayPickerSingleProps = PropsSingle;
/**
* @deprecated This type has been renamed. Use `PropsMulti` instead.
* @protected
*/
-export type DayPickerMultipleProps = PropsMulti;
+export type DayPickerMultipleProps = PropsMulti;
/**
* @deprecated This type has been renamed. Use `PropsRange` instead.
* @protected
*/
-export type DayPickerRangeProps = PropsRange;
+export type DayPickerRangeProps = PropsRange;
/**
- * @deprecated This type will be removed.
+ * @deprecated This type will be removed. Use `NonNullable` instead
* @protected
*/
-export type DayPickerDefaultProps = PropsDefault;
+export type DayPickerDefaultProps = NonNullable;
/**
* @deprecated This type has been renamed. Use `Mode` instead.
@@ -111,20 +108,20 @@ export type Modifier = string;
* @deprecated This type will be removed. Use `SelectHandler<"single">` instead.
* @protected
*/
-export type SelectSingleEventHandler = SelectHandler<"single", false>;
+export type SelectSingleEventHandler = PropsSingle["onSelect"];
/**
* @deprecated This type will be removed. Use `SelectHandler<"multiple">`
* instead.
* @protected
*/
-export type SelectMultipleEventHandler = SelectHandler<"multiple", false>;
+export type SelectMultipleEventHandler = PropsMulti["onSelect"];
/**
* @deprecated This type will be removed. Use `SelectHandler<"range">` instead.
* @protected
*/
-export type SelectRangeEventHandler = SelectHandler<"range", false>;
+export type SelectRangeEventHandler = PropsRange["onSelect"];
/**
* @deprecated This type is not used anymore.
@@ -135,21 +132,21 @@ export type DayPickerProviderProps = any;
/**
* @deprecated This type has been renamed to `useProps`.
* @protected
- * @group Hooks
+ * @group Contexts
*/
-export const useDayPicker = useProps;
+export const useDayPicker = usePropsContext;
/**
* @deprecated This type has been renamed to `useProps`.
* @protected
- * @group Hooks
+ * @group Contexts
*/
-export const useNavigation = useCalendar;
+export const useNavigation = useCalendarContext;
/**
* @deprecated This hook has been removed. Use a custom `Day` component instead.
* @protected
- * @group Hooks
+ * @group Contexts
* @see https://react-day-picker.js.org/advanced-guides/custom-components
*/
export type useDayRender = any;
@@ -231,4 +228,4 @@ export type DayTouchEventHandler = DayEventHandler;
* @deprecated The type has been renamed. Use `PropsContext` instead.
* @protected
*/
-export type DayPickerContext = PropsContext;
+export type DayPickerContext = PropsContextValue;
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000000..cf18a0c265
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,3 @@
+export * from "./deprecated";
+export * from "./shared";
+export * from "./props";
diff --git a/src/types/props.test.tsx b/src/types/props.test.tsx
new file mode 100644
index 0000000000..7b6f7d0e91
--- /dev/null
+++ b/src/types/props.test.tsx
@@ -0,0 +1,64 @@
+import React from "react";
+
+import { DayPicker } from "../DayPicker";
+
+const Test = () => {
+ return (
+ <>
+
+
+ {}}
+ />
+ {}}
+ />
+ {/* @ts-expect-error Missing `selected` */}
+ {}}
+ />
+ {/* @ts-expect-error selected should not be undefined. */}
+
+ {}}
+ />
+ {}}
+ />
+ {/** @ts-expect-error Wrong selected prop */}
+
+ {}} />
+ {}}
+ />
+
+ {}} />
+ {}}
+ selected={new Date()}
+ onDayClick={() => {}}
+ />
+ >
+ );
+};
+
+it("should type-check", () => {
+ expect(true).toBeTruthy();
+});
diff --git a/src/types/props.ts b/src/types/props.ts
new file mode 100644
index 0000000000..8b352ca9f4
--- /dev/null
+++ b/src/types/props.ts
@@ -0,0 +1,416 @@
+/* eslint-disable @typescript-eslint/ban-types */
+import React from "react";
+
+import type { Locale } from "date-fns";
+
+import type {
+ ClassNames,
+ ModifiersClassNames,
+ Styles,
+ ModifiersStyles,
+ CustomComponents,
+ Matcher,
+ Labels,
+ Formatters,
+ MonthChangeEventHandler,
+ DayEventHandler,
+ WeekNumberMouseEventHandler,
+ Modifiers,
+ DateRange,
+ Mode
+} from "./shared";
+
+/**
+ * The props for the `` component.
+ *
+ * @group Props
+ */
+export type DayPickerProps = PropsBase &
+ (
+ | PropsSingle
+ | PropsSingleRequired
+ | PropsMulti
+ | PropsMultiRequired
+ | PropsRange
+ | PropsRangeRequired
+ | { mode?: undefined }
+ );
+
+/** @group Props */
+export interface PropsBase {
+ mode?: Mode | undefined;
+ /** Class name to add to the root element */
+ className?: string;
+ /**
+ * Change the class names used by DayPicker.
+ *
+ * Use this prop when you need to change the default class names — for example
+ * when importing the style via CSS modules or when using a CSS framework.
+ */
+ classNames?: Partial;
+ /** Change the class name for the day matching the `modifiers`. */
+ modifiersClassNames?: ModifiersClassNames;
+ /** Style to apply to the root element. */
+ style?: React.CSSProperties;
+ /** Change the inline styles of the HTML elements. */
+ styles?: Partial;
+ /** Change the class name for the day matching the {@link modifiers}. */
+ modifiersStyles?: ModifiersStyles;
+ /** A unique id to replace the React generated id. Used for ARIA labels. */
+ id?: string;
+ /**
+ * The initial month to show in the calendar.
+ *
+ * Use this prop to let DayPicker control the current month. If you need to
+ * set the month programmatically, use {@link month} and {@link onMonthChange}.
+ *
+ * @defaultValue The current month
+ */
+ defaultMonth?: Date;
+ /**
+ * The month displayed in the calendar.
+ *
+ * As opposed to `defaultMonth`, use this prop with `onMonthChange` to change
+ * the month programmatically.
+ */
+ month?: Date;
+ /**
+ * The number of displayed months.
+ *
+ * @defaultValue 1
+ */
+ numberOfMonths?: number;
+ /**
+ * The earliest month to start the month navigation.
+ *
+ * @since 9.0.0
+ */
+ startMonth?: Date | undefined;
+ /**
+ * @private
+ * @deprecated This prop has been removed. Use `hidden={{ before: date }}`
+ * instead.
+ */
+ fromDate?: Date | undefined;
+ /**
+ * @private
+ * @deprecated This prop has been renamed to `startMonth`.
+ */
+ fromMonth?: Date | undefined;
+ /**
+ * @private
+ * @deprecated Use `startMonth` instead. E.g. `startMonth={new Date(year,
+ * 0)}`.
+ */
+ fromYear?: number | undefined;
+
+ /**
+ * The latest month to end the month navigation.
+ *
+ * @since 9.0.0
+ */
+ endMonth?: Date;
+ /**
+ * @private
+ * @deprecated This prop has been removed. Use `hidden={{ after: date }}`
+ * instead.
+ */
+ toDate?: Date;
+ /**
+ * @private
+ * @deprecated This prop has been renamed to `endMonth`.
+ */
+ toMonth?: Date;
+ /**
+ * @private
+ * @deprecated Use `endMonth` instead. E.g. `endMonth={new Date(year, 0)}`.
+ */
+ toYear?: number;
+
+ /** Paginate the month navigation displaying the `numberOfMonths` at time. */
+ pagedNavigation?: boolean;
+ /**
+ * Render the months in reversed order (when {@link numberOfMonths} is set) to
+ * display the most recent month first.
+ */
+ reverseMonths?: boolean;
+ /**
+ * Hide the navigation buttons. This prop won't disable the navigation: to
+ * disable the navigation, use {@link disableNavigation}.
+ *
+ * @since 9.0.0
+ */
+ hideNavigation?: boolean;
+ /**
+ * Disable the navigation between months. This prop won't hide the navigation:
+ * to hide the navigation, use {@link hideNavigation}.
+ */
+ disableNavigation?: boolean;
+ /**
+ * Show dropdowns to navigate between months or years.
+ *
+ * - `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 set the start/end months
+ * {@link fromYear} to the 100 years ago, and {@link toYear} to the current
+ * 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.
+ */
+ fixedWeeks?: boolean;
+ /**
+ * Hide the row displaying the weekday row header.
+ *
+ * @since 9.0.0
+ */
+ hideWeekdayRow?: boolean;
+ /** Show the outside days (days falling in the next or the previous month). */
+ showOutsideDays?: boolean;
+ /**
+ * Show the week numbers column. Weeks are numbered according to the local
+ * week index.
+ *
+ * - To use ISO week numbering, use the `ISOWeek` prop.
+ * - To change how the week numbers are displayed, use the `formatters` prop.
+ */
+ showWeekNumber?: boolean;
+ /**
+ * Use ISO week dates instead of the locale setting. Setting this prop will
+ * ignore `weekStartsOn` and `firstWeekContainsDate`.
+ *
+ * @see https://en.wikipedia.org/wiki/ISO_week_date
+ */
+ ISOWeek?: boolean;
+ /** Change the components used for rendering the calendar elements. */
+ components?: CustomComponents;
+ /** Content to add to the grid as footer element. */
+ footer?: React.ReactNode;
+ /**
+ * When a selection mode is set, DayPicker will focus the first selected day
+ * (if set) or the today's date (if not disabled).
+ *
+ * Use this prop when you need to focus DayPicker after a user actions, for
+ * improved accessibility.
+ */
+ autoFocus?: boolean;
+ /** Apply the `disabled` modifier to the matching days. */
+ disabled?: Matcher | Matcher[] | undefined;
+ /**
+ * Apply the `hidden` modifier to the matching days. Will hide them from the
+ * calendar.
+ */
+ hidden?: Matcher | Matcher[] | undefined;
+ /**
+ * The today’s date. Default is the current date. This date will get the
+ * `today` modifier to style the day.
+ */
+ today?: Date;
+ /** Add modifiers to the matching days. */
+ modifiers?: Record | undefined;
+ /**
+ * Labels creators to override the defaults. Use this prop to customize the
+ * aria-label attributes in DayPicker.
+ */
+ labels?: Partial;
+ /**
+ * Formatters used to format dates to strings. Use this prop to override the
+ * default functions.
+ */
+ formatters?: Partial;
+ /**
+ * The text direction of the calendar. Use `ltr` for left-to-right (default)
+ * or `rtl` for right-to-left.
+ */
+ dir?: HTMLDivElement["dir"];
+ /**
+ * A cryptographic nonce ("number used once") which can be used by Content
+ * Security Policy for the inline `style` attributes.
+ */
+ nonce?: HTMLDivElement["nonce"];
+ /** Add a `title` attribute to the container element. */
+ title?: HTMLDivElement["title"];
+ /** Add the language tag to the container element. */
+ lang?: HTMLDivElement["lang"];
+ /**
+ * The date-fns locale object used to localize dates.
+ *
+ * @defaultValue en-US
+ * @see https://date-fns.org/docs/Locale
+ */
+ locale?: Locale | undefined;
+ /**
+ * The index of the first day of the week (0 - Sunday). Overrides the locale's
+ * one.
+ */
+ weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | undefined;
+ /**
+ * The day of January, which is always in the first week of the year.
+ *
+ * @see https://date-fns.org/docs/getWeek
+ * @see https://en.wikipedia.org/wiki/Week#Numbering
+ */
+ firstWeekContainsDate?: 1 | 4;
+ /**
+ * Enable `DD` and `DDDD` for week year tokens when formatting or parsing
+ * dates.
+ *
+ * @see https://date-fns.org/docs/Unicode-Tokens
+ */
+ useAdditionalWeekYearTokens?: boolean | undefined;
+ /**
+ * Enable `YY` and `YYYY` for day of year tokens when formatting or parsing
+ * dates.
+ *
+ * @see https://date-fns.org/docs/Unicode-Tokens
+ */
+ useAdditionalDayOfYearTokens?: boolean | undefined;
+
+ /* EVENT HANDLERS */
+ /** Event fired when the user navigates between months. */
+ onMonthChange?: MonthChangeEventHandler;
+ /** Event handler when a day is clicked. */
+ onDayClick?: DayEventHandler;
+ /** Event handler when a day is focused. */
+ onDayFocus?: DayEventHandler;
+ /** Event handler when a day is blurred. */
+ onDayBlur?: DayEventHandler;
+ /** Event handler when the mouse enters a day. */
+ onDayMouseEnter?: DayEventHandler;
+ /** Event handler when the mouse leaves a day. */
+ onDayMouseLeave?: DayEventHandler;
+ /** Event handler when a key is pressed on a day. */
+ onDayKeyDown?: DayEventHandler;
+ /** Event handler when a key is released on a day. */
+ onDayKeyUp?: DayEventHandler;
+ /** Event handler when a key is pressed and released on a day. */
+ onDayKeyPress?: DayEventHandler;
+ /** Event handler when a pointer enters a day. */
+ onDayPointerEnter?: DayEventHandler;
+ /** Event handler when a pointer leaves a day. */
+ onDayPointerLeave?: DayEventHandler;
+ /** Event handler when a touch is cancelled on a day. */
+ onDayTouchCancel?: DayEventHandler;
+ /** Event handler when a touch ends on a day. */
+ onDayTouchEnd?: DayEventHandler;
+ /** Event handler when a touch moves on a day. */
+ onDayTouchMove?: DayEventHandler;
+ /** Event handler when a touch starts on a day. */
+ onDayTouchStart?: DayEventHandler;
+ /** Event handler when the next month button is clicked. */
+ onNextClick?: MonthChangeEventHandler;
+ /** Event handler when the previous month button is clicked. */
+ onPrevClick?: MonthChangeEventHandler;
+ /** Event handler when a week number is clicked */
+ onWeekNumberClick?: WeekNumberMouseEventHandler;
+}
+/**
+ * The props when the single selection is required.
+ *
+ * @group Props
+ */
+export interface PropsSingleRequired {
+ mode: "single";
+ required: true;
+ selected: Date;
+ onSelect?: (
+ selected: Date,
+ triggerDate: Date,
+ modifiers: Modifiers,
+ e: React.MouseEvent | React.KeyboardEvent
+ ) => void | undefined;
+}
+/**
+ * The props when the single selection is optional.
+ *
+ * @group Props
+ */
+export interface PropsSingle {
+ mode: "single";
+ required?: false | undefined;
+ selected?: Date | undefined;
+ onSelect?: (
+ selected: Date | undefined,
+ triggerDate: Date,
+ modifiers: Modifiers,
+ e: React.MouseEvent | React.KeyboardEvent
+ ) => void;
+}
+/**
+ * The props when the multiple selection is required.
+ *
+ * @group Props
+ */
+export interface PropsMultiRequired {
+ mode: "multiple";
+ required: true;
+ selected: Date[];
+ onSelect?: (
+ selected: Date[],
+ triggerDate: Date,
+ modifiers: Modifiers,
+ e: React.MouseEvent | React.KeyboardEvent
+ ) => void;
+ min?: number;
+ max?: number;
+}
+/**
+ * The props when the multiple selection is optional.
+ *
+ * @group Props
+ */
+export interface PropsMulti {
+ mode: "multiple";
+ required?: false | undefined;
+ selected?: Date[] | undefined;
+ onSelect?: (
+ selected: Date[] | undefined,
+ triggerDate: Date,
+ modifiers: Modifiers,
+ e: React.MouseEvent | React.KeyboardEvent
+ ) => void;
+ min?: number;
+ max?: number;
+}
+/**
+ * The props when the range selection is required.
+ *
+ * @group Props
+ */
+export interface PropsRangeRequired {
+ mode: "range";
+ required: true;
+ selected: DateRange;
+ onSelect?: (
+ selected: DateRange,
+ triggerDate: Date,
+ modifiers: Modifiers,
+ e: React.MouseEvent | React.KeyboardEvent
+ ) => void;
+ min?: number;
+ max?: number;
+}
+/**
+ * The props when the range selection is optional.
+ *
+ * @group Props
+ */
+export interface PropsRange {
+ mode: "range";
+ required?: false | undefined;
+ selected?: DateRange | undefined;
+ onSelect?: (
+ selected: DateRange | undefined,
+ triggerDate: Date,
+ modifiers: Modifiers,
+ e: React.MouseEvent | React.KeyboardEvent
+ ) => void | undefined;
+ min?: number;
+ max?: number;
+}
diff --git a/src/types/shared.ts b/src/types/shared.ts
new file mode 100644
index 0000000000..c65716394c
--- /dev/null
+++ b/src/types/shared.ts
@@ -0,0 +1,291 @@
+/* eslint-disable @typescript-eslint/ban-types */
+import type { MouseEvent, CSSProperties } from "react";
+
+import {
+ UI,
+ DayFlag,
+ CalendarFlag,
+ ChevronFlag,
+ WeekNumberFlag,
+ SelectionState
+} from "../UI";
+import * as components from "../components/custom-components";
+import {
+ formatCaption,
+ formatDay,
+ formatMonthCaption,
+ formatMonthDropdown,
+ formatWeekdayName,
+ formatWeekNumber,
+ formatYearCaption,
+ formatYearDropdown
+} from "../formatters";
+import {
+ labelDay,
+ labelCaption,
+ labelMonthDropdown,
+ labelNext,
+ labelPrevious,
+ labelWeekday,
+ labelWeekNumber,
+ labelWeekNumberHeader,
+ labelYearDropdown
+} from "../labels";
+
+/**
+ * Selection modes supported by DayPicker.
+ *
+ * - `single`: use DayPicker to select single days.
+ * - `multiple`: allow selecting multiple days.
+ * - `range`: use DayPicker to select a range of days.
+ *
+ * @see https://react-day-picker.js.org/next/using-daypicker/selection-modes
+ */
+export type Mode = "single" | "multiple" | "range";
+
+/**
+ * The components that can be changed using the `components` prop.
+ *
+ * @see https://github.com/gpbl/react-day-picker/blob/main/src/components/custom-components.ts
+ */
+export type CustomComponents = {
+ [key in keyof typeof components]?: (typeof components)[key];
+};
+
+/** Represent a map of formatters used to render localized content. */
+export type Formatters = {
+ /** Format the caption of a month grid. */
+ formatCaption: typeof formatCaption;
+ /** @deprecated Use {@link Formatters.formatCaption} instead. */
+ formatMonthCaption: typeof formatMonthCaption;
+ /** Format the label in the month dropdown. */
+ formatMonthDropdown: typeof formatMonthDropdown;
+ /** @deprecated Use {@link Formatters.formatYearDropdown} instead. */
+ formatYearCaption: typeof formatYearCaption;
+ /** Format the label in the year dropdown. */
+ formatYearDropdown: typeof formatYearDropdown;
+ /** Format the day in the day cell. */
+ formatDay: typeof formatDay;
+ /** Format the week number. */
+ formatWeekNumber: typeof formatWeekNumber;
+ /** Format the week day name in the header */
+ formatWeekdayName: typeof formatWeekdayName;
+};
+
+/** Map of functions to translate ARIA labels for the relative elements. */
+export type Labels = {
+ /** Return the label for the month dropdown. */
+ labelCaption: typeof labelCaption;
+ /** Return the label for the month dropdown. */
+ labelMonthDropdown: typeof labelMonthDropdown;
+ /** Return the label for the year dropdown. */
+ labelYearDropdown: typeof labelYearDropdown;
+ /** Return the label for the next month button. */
+ labelNext: typeof labelNext;
+ /** Return the label for the previous month button. */
+ labelPrevious: typeof labelPrevious;
+ /** Return the label for the day cell. */
+ labelDay: typeof labelDay;
+ /** Return the label for the weekday. */
+ labelWeekday: typeof labelWeekday;
+ /** Return the label for the week number. */
+ labelWeekNumber: typeof labelWeekNumber;
+ /**
+ * Return the label for the column of the week number.
+ *
+ * @since 9.0.0
+ */
+ labelWeekNumberHeader: typeof labelWeekNumberHeader;
+};
+
+/**
+ * A value or a function that matches a specific day.
+ *
+ * Matchers can be of different types:
+ *
+ * ```tsx
+ * // will always match the day
+ * const booleanMatcher: Matcher = true;
+ *
+ * // will match the today's date
+ * const dateMatcher: Matcher = new Date();
+ *
+ * // will match the days in the array
+ * const arrayMatcher: Matcher = [
+ * new Date(2019, 1, 2),
+ * new Date(2019, 1, 4)
+ * ];
+ *
+ * // will match days after the 2nd of February 2019
+ * const afterMatcher: DateAfter = { after: new Date(2019, 1, 2) };
+ * // will match days before the 2nd of February 2019 }
+ * const beforeMatcher: DateBefore = { before: new Date(2019, 1, 2) };
+ *
+ * // will match Sundays
+ * const dayOfWeekMatcher: DayOfWeek = {
+ * dayOfWeek: 0
+ * };
+ *
+ * // will match the included days, except the two dates
+ * const intervalMatcher: DateInterval = {
+ * after: new Date(2019, 1, 2),
+ * before: new Date(2019, 1, 5)
+ * };
+ *
+ * // will match the included days, including the two dates
+ * const rangeMatcher: DateRange = {
+ * from: new Date(2019, 1, 2),
+ * to: new Date(2019, 1, 5)
+ * };
+ *
+ * // will match when the function return true
+ * const functionMatcher: Matcher = (day: Date) => {
+ * return day.getMonth() === 2; // match when month is March
+ * };
+ * ```
+ */
+export type Matcher =
+ | boolean
+ | ((date: Date) => boolean)
+ | Date
+ | Date[]
+ | DateRange
+ | DateBefore
+ | DateAfter
+ | DateInterval
+ | DayOfWeek;
+
+/**
+ * A matcher to match a day falling after the specified date, with the date not
+ * included.
+ */
+export type DateAfter = { after: Date };
+
+/**
+ * A matcher to match a day falling before the specified date, with the date not
+ * included.
+ */
+export type DateBefore = { before: Date };
+
+/**
+ * A matcher to match a day falling before and/or after two dates, where the
+ * dates are not included.
+ */
+export type DateInterval = { before: Date; after: Date };
+
+/**
+ * A matcher to match a range of dates. The range can be open. Differently from
+ * {@link DateInterval}, the dates here are included.
+ */
+export type DateRange = { from: Date | undefined; to?: Date | undefined };
+
+/**
+ * A matcher to match a date being one of the specified days of the week (`0-6`,
+ * where `0` is Sunday).
+ */
+export type DayOfWeek = { dayOfWeek: number[] };
+
+/** A record with `data-*` attributes passed to ``. */
+export type DataAttributes = Record<`data-${string}`, unknown>;
+
+/**
+ * The event handler triggered when interacting with a day.
+ *
+ * @template EventType - The event type that triggered the day event.
+ */
+export type DayEventHandler = (
+ /** The date that has triggered the event. */
+ date: Date,
+ /** The modifiers belonging to the date. */
+ modifiers: Modifiers,
+ /** The DOM event that triggered this event. */
+ e: EventType
+) => void;
+
+/** The event handler when a month is changed in the calendar. */
+export type MonthChangeEventHandler = (month: Date) => void;
+
+/** The event handler when the week number is clicked. */
+export type WeekNumberMouseEventHandler = (
+ /** The week number that has been clicked. */
+ weekNumber: number,
+ /** The dates in the clicked week. */
+ dates: Date[],
+ /** The mouse event that triggered this event. */
+ e: MouseEvent
+) => void;
+
+/** Maps user interface elements, selection states, and flags to a CSS style. */
+export type Styles = {
+ [element in
+ | UI
+ | SelectionState
+ | DayFlag
+ | CalendarFlag
+ | ChevronFlag
+ | WeekNumberFlag]: CSSProperties | undefined;
+};
+
+/**
+ * Maps user interface elements, selection states, and flags to a CSS class
+ * name.
+ */
+export type ClassNames = {
+ [key in
+ | UI
+ | SelectionState
+ | DayFlag
+ | CalendarFlag
+ | ChevronFlag
+ | WeekNumberFlag]: string;
+};
+
+/** The flags that are matching a day in the calendar. */
+export type DayFlags = Record;
+
+/** The selection state that are matching a day in the calendar. */
+export type SelectionStates = Record;
+
+/** The modifiers that are matching a day in the calendar. */
+export type Modifiers = DayFlags & SelectionStates & CustomModifiers;
+
+/** The custom modifiers matching a day, passed to the `modifiers` prop. */
+export type CustomModifiers = Record;
+
+/** The style to apply to each day element matching a modifier. */
+export type ModifiersStyles = Record &
+ Partial>;
+
+/** The classnames to assign to each day element matching a modifier. */
+export type ModifiersClassNames = Record &
+ Partial>;
+
+/**
+ * The props that have been deprecated since version 9.0.0.
+ *
+ * @since 9.0.0
+ * @see https://react-day-picker.js.org/next/upgrading
+ */
+export type V9DeprecatedProps =
+ /** Use `hidden` prop instead. */
+ | "fromDate"
+ /** Use `hidden` prop instead. */
+ | "toDate"
+ /** Use `startMonth` instead. */
+ | "fromMonth"
+ /** Use `endMonth` instead. */
+ | "toMonth"
+ /** Use `startMonth` instead. */
+ | "fromYear"
+ /** Use `endMonth` instead. */
+ | "toYear";
+
+export type MoveFocusDir = "after" | "before";
+
+export type MoveFocusBy =
+ | "day"
+ | "week"
+ | "startOfWeek"
+ | "endOfWeek"
+ | "month"
+ | "year";
diff --git a/src/utils/addToRange.test.ts b/src/utils/addToRange.test.ts
index f51734d383..830df5bd98 100644
--- a/src/utils/addToRange.test.ts
+++ b/src/utils/addToRange.test.ts
@@ -58,7 +58,7 @@ describe('when "from", "to" and "day" are the same', () => {
result = addToRange(day, range);
});
test("should return an undefined range (reset)", () => {
- expect(result).toBeUndefined();
+ expect(result).toEqual({ from: undefined, to: undefined });
});
});
@@ -85,8 +85,8 @@ describe('when "from" and "day" are the same', () => {
beforeAll(() => {
result = addToRange(day, range);
});
- test("should return an undefined range (reset)", () => {
- expect(result).toBeUndefined();
+ test("should reset the range", () => {
+ expect(result).toEqual({ from: undefined, to: undefined });
});
});
diff --git a/src/utils/addToRange.ts b/src/utils/addToRange.ts
index 210737b052..b995d74b24 100644
--- a/src/utils/addToRange.ts
+++ b/src/utils/addToRange.ts
@@ -12,20 +12,17 @@ import type { DateRange } from "../types";
*
* @group Utilities
*/
-export function addToRange(
- date: Date,
- range?: DateRange
-): DateRange | undefined {
+export function addToRange(date: Date, range?: DateRange): DateRange {
const { from, to } = range || {};
if (from && to) {
if (isSameDay(to, date) && isSameDay(from, date)) {
- return undefined;
+ return { from: undefined, to: undefined };
}
if (isSameDay(to, date)) {
return { from: to, to: undefined };
}
if (isSameDay(from, date)) {
- return undefined;
+ return { from: undefined, to: undefined };
}
if (isAfter(from, date)) {
return { from: date, to };
diff --git a/src/utils/isDateInRange.test.ts b/src/utils/isDateInRange.test.ts
index b55674ad1d..db33f14b23 100644
--- a/src/utils/isDateInRange.test.ts
+++ b/src/utils/isDateInRange.test.ts
@@ -1,4 +1,4 @@
-import { addDays } from "date-fns";
+import { addDays } from "date-fns/addDays";
import { DateRange } from "../types";
diff --git a/test/render.tsx b/test/render.tsx
index 252b281c13..11131105e4 100644
--- a/test/render.tsx
+++ b/test/render.tsx
@@ -1,16 +1,16 @@
import React, { ReactElement } from "react";
import { render as testingLibraryRender } from "@testing-library/react";
-import type { Mode, DayPickerProps } from "react-day-picker";
+import type { DayPickerProps } from "react-day-picker";
-import { ContextProviders } from "../src/contexts/root";
+import { ContextProviders } from "../src/contexts/providers";
/** Render a React Element wrapped with the Root Provider. */
export function render(
/** The element to render. */
element: ReactElement,
/** The initial DayPicker props to pass to the Root Provider. */
- context?: DayPickerProps,
+ context?: DayPickerProps,
/** The options to pass to the testing library render function. */
options?: Parameters[1]
): ReturnType {
diff --git a/test/renderHook.tsx b/test/renderHook.tsx
index 9a60205935..a9647e89b0 100644
--- a/test/renderHook.tsx
+++ b/test/renderHook.tsx
@@ -1,16 +1,17 @@
import React from "react";
import { renderHook as testingLibraryRenderHook } from "@testing-library/react";
+import { DayPickerProps } from "react-day-picker";
-import { ContextProviders } from "../src/contexts/root";
-import { DayPickerProps, Mode } from "../src/types";
+import { ContextProviders } from "../src/contexts/providers";
+import { Mode } from "../src/types";
/** Render a hook wrapped with the {@link ContextProviders} Provider. */
export function renderHook(
/** The hook to render. */
hook: () => TResult,
/** The props to pass to the {@link ContextProviders}. */
- props?: DayPickerProps,
+ props?: DayPickerProps,
/** The options to pass to the testing library render function. */
options?: Omit[1], "wrapper">
) {
diff --git a/website/docs/advanced-guides/custom-components.mdx b/website/docs/advanced-guides/custom-components.mdx
index ab4ed28217..a63b2607bc 100644
--- a/website/docs/advanced-guides/custom-components.mdx
+++ b/website/docs/advanced-guides/custom-components.mdx
@@ -70,12 +70,11 @@ export function MyDatePicker() {
When creating custom components, you will find useful the [DayPicker hooks](../api/index.md#hooks). These utilities provide access to the internal state and methods of the DayPicker component.
-| Hooks | Description |
-| :------------------------------------------------- | :------------------------------------------------------------------------- |
-| [`useCalendar`](../api/functions/useCalendar.md) | Return the calendar state and navigation methods to navigate the calendar. |
-| [`useFocus`](../api/functions/useFocus.md) | Share the focused day and the methods to move the focus. |
-| [`useProps`](../api/functions/useProps.md) | Access to the props passed to DayPicker. |
-| [`useSelection`](../api/functions/useSelection.md) | Access and change the currently selected values. |
+| Hooks | Description |
+| :------------------------------------------------------------- | :------------------------------------------------------------------------- |
+| [`useCalendarContext`](../api/functions/useCalendarContext.md) | Return the calendar state and navigation methods to navigate the calendar. |
+| [`useFocusContext`](../api/functions/useFocusContext.md) | Share the focused day and the methods to move the focus. |
+| [`usePropsContext`](../api/functions/usePropsContext.md) | Access to the props passed to DayPicker. |
### Example: Range with Shift Key
@@ -88,11 +87,11 @@ import {
DateRange,
DayPicker,
type DayProps,
- useSelection
+ useRangeContext
} from "react-day-picker";
function DayWithShiftKey(props: DayProps) {
- const { selected } = useSelection<"range">();
+ const { selected } = useRangeContext();
const onClick = props.htmlAttributes?.onClick;
const handleClick: MouseEventHandler = (e) => {
diff --git a/website/docs/advanced-guides/custom-modifiers.mdx b/website/docs/advanced-guides/custom-modifiers.mdx
index 15ffbf299f..fe82fd377c 100644
--- a/website/docs/advanced-guides/custom-modifiers.mdx
+++ b/website/docs/advanced-guides/custom-modifiers.mdx
@@ -23,7 +23,7 @@ In DayPicker, a **modifier** is added to a day when the day matches a specific c
### Understanding Modifiers
- Use modifiers to change the appearance of the days in the calendar or to inspect the days the user has interacted with (e.g. picking a day)
-- DayPicker comes with some [pre-built modifiers](../api/type-aliases/InternalModifier.md), such as `disabled`, `selected`, `hidden`, `today`, `range_start`, etc. designed to cover the most common use cases.
+- DayPicker comes with some [pre-built modifiers](../api/type-aliases/Modifiers.md), such as `disabled`, `selected`, `hidden`, `today`, `range_start`, etc. designed to cover the most common use cases.
- It is possible to implement custom modifiers, extending the behavior of DayPicker: see [Custom Modifiers](#custom-modifiers) below for more details.
## The `selected` Modifier
diff --git a/website/docs/upgrading.mdx b/website/docs/upgrading.mdx
index 9902363c5f..9c292fee63 100644
--- a/website/docs/upgrading.mdx
+++ b/website/docs/upgrading.mdx
@@ -81,8 +81,8 @@ In case you are using DayPicker hooks in your custom components, you need to upd
| Hook | Upgrade Note |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------- |
-| ~`useDayPicker`~ | Renamed to [`useProps`](./api/functions/useProps.md). |
-| ~`useNavigation`~ | Renamed to [`useCalendar`](./api/functions/useCalendar.md). |
+| ~`useDayPicker`~ | Renamed to [`usePropsContext`](./api/functions/usePropsContext.md). |
+| ~`useNavigation`~ | Renamed to [`useCalendarContext`](./api/functions/useCalendarContext.md). |
| ~`useDayPicker`~ | Removed in favor of the `Day` custom component. See [custom components guide](./advanced-guides/custom-components.mdx). |
### 7. TypeScript: check for deprecated types
@@ -91,7 +91,7 @@ Many typings have been deprecated in favor of clarity and shorter names. If you
```diff
- import type { DayPickerDefaultProps } from 'react-day-picker';
-+ import type { PropsDefault } from 'react-day-picker';
++ import type { PropsBase } from 'react-day-picker';
```
See also the source of [types-deprecated.ts](https://github.com/gpbl/react-day-picker/blob/next/src/types-deprecated.ts).
@@ -104,15 +104,15 @@ See also the source of [types-deprecated.ts](https://github.com/gpbl/react-day-p
| ~`DayPickerSingleProps`~ | This type has been renamed. Use [`PropsSingle`](./api/interfaces/PropsSingle.md) instead. |
| ~`DayPickerMultipleProps`~ | This type has been renamed. Use [`PropsMulti`](./api/interfaces/PropsMulti.md) instead. |
| ~`DayPickerRangeProps`~ | This type has been renamed. Use [`PropsRange`](./api/interfaces/PropsRange.md) instead. |
-| ~`DayPickerDefaultProps`~ | This type has been renamed. Use [`PropsDefault`](./api/interfaces/PropsDefault.md) instead. |
+| ~`DayPickerDefaultProps`~ | This type has been renamed. Use [`PropsBase`](./api/interfaces/PropsBase.md) instead. |
| ~`DaySelectionMode`~ | This type has been renamed. Use [`Mode`](./api/type-aliases/Mode.md) instead. |
| ~`Modifier`~ | This type will be removed. Use `string` instead. |
-| ~`SelectSingleEventHandler`~ | This type will be removed. Use [`SelectHandler<"single">`](./api/type-aliases/SelectHandler.md) instead. |
-| ~`SelectMultipleEventHandler`~ | This type will be removed. Use [`SelectHandler<"multiple">`](./api/type-aliases/SelectHandler.md) instead. |
-| ~`SelectRangeEventHandler`~ | This type will be removed. Use [`SelectHandler<"range">`](./api/type-aliases/SelectHandler.md) instead. |
+| ~`SelectSingleEventHandler`~ | This type will be removed. Use [`PropsSingle["onSelect]`](./api/interfaces/PropsSingle.md) instead. |
+| ~`SelectMultipleEventHandler`~ | This type will be removed. Use [`PropsMulti["onSelect]`](./api/interfaces/PropsMulti.md) instead. |
+| ~`SelectRangeEventHandler`~ | This type will be removed. Use [`PropsRange["onSelect]`](./api/interfaces/PropsRange.md) instead. |
| ~`DayPickerProviderProps`~ | This type is not used anymore. |
-| ~`useDayPicker`~ | This type has been renamed to [`useProps`](./api/functions/useProps.md). |
-| ~`useNavigation`~ | This type has been renamed to [`useCalendar`](./api/functions/useCalendar.md). |
+| ~`useDayPicker`~ | This type has been renamed to [`usePropsContext`](./api/functions/usePropsContext.md). |
+| ~`useNavigation`~ | This type has been renamed to [`useCalendarContext`](./api/functions/useCalendarContext.md). |
| ~`useDayRender`~ | This hook has been removed. To customize the rendering of a day, use the `htmlAttributes` prop in a custom `Day` component. |
| ~`ContextProvidersProps`~ | This type is not used anymore. |
| ~`DayLabel`~ | Use `typeof labelDay` instead. |
diff --git a/website/docs/using-daypicker/selection-modes.mdx b/website/docs/using-daypicker/selection-modes.mdx
index b9b8573d49..07fe5a2f7e 100644
--- a/website/docs/using-daypicker/selection-modes.mdx
+++ b/website/docs/using-daypicker/selection-modes.mdx
@@ -12,12 +12,12 @@ DayPicker provides predefined rules for day selection:
The `mode` prop determines the selection mode. The `disabled` prop can be used to prevent the selection of specific days. The `selected` and `onSelect` props offer customization of the selection process.
-| Prop Name | Type | Description |
-| ---------- | --------------------------------------------------------------------- | ------------------------------------------ |
-| `mode` | \| `"single"`
\| `"multiple"`
\| `"range"`
\| `"none"` | Set a selection mode. Default is `"none"`. |
-| `disabled` | [`Matcher`](../api/type-aliases/Matcher.md) \| `Matcher[]` | Disabled days that cannot be selected. |
-| `selected` | [`Selected`](../api/type-aliases/Selected.md) | The selected day(s). |
-| `onSelect` | [`SelectHandler`](../api/type-aliases/SelectHandler.md) | Event callback when a date is selected. |
+| Prop Name | Type | Description |
+| ---------- | ---------------------------------------------------------------------------------- | ------------------------------------------ |
+| `mode` | \| `"single"`
\| `"multiple"`
\| `"range"`
\| `"none"` | Set a selection mode. Default is `"none"`. |
+| `disabled` | [`Matcher`](../api/type-aliases/Matcher.md) \| `Matcher[]` | Disabled days that cannot be selected. |
+| `selected` | `Date` \| `Date[]` \| [`DateRange`](../api/type-aliases/DateRange.md) \| undefined | The selected day(s). |
+| `onSelect` | `(selected, triggerDate, modifiers, e) => void` | Event callback when a date is selected. |
## Single Mode
@@ -33,11 +33,11 @@ When the `mode` prop is set to `"single"`, only one day can be selected.
### Single Mode Props
-| Prop Name | Type | Description |
-| ---------- | ----------------------------------------------------------------- | --------------------------------------- |
-| `selected` | `Date \| undefined` | The selected date. |
-| `onSelect` | [`SelectHandler<'single'>`](../api/type-aliases/SelectHandler.md) | Event callback when a date is selected. |
-| `required` | `boolean` | Make the selection required. |
+| Prop Name | Type | Description |
+| ---------- | ----------------------------------------------- | --------------------------------------- |
+| `selected` | `Date \| undefined` | The selected date. |
+| `onSelect` | `(selected, triggerDate, modifiers, e) => void` | Event callback when a date is selected. |
+| `required` | `boolean` | Make the selection required. |
The following code snippet will render a date picker with a single selected date. When a day is clicked, the `selectedDate` state is updated.
@@ -85,12 +85,12 @@ By setting the `mode` prop to `"multiple"`, DayPicker allows selecting multiple
### Multiple Mode Props
-| Prop Name | Type | Description |
-| ---------- | ------------------------------------------------------------------- | --------------------------------------- |
-| `selected` | `Date[] \| undefined` | The selected dates. |
-| `onSelect` | [`SelectHandler<'multiple'>`](../api/type-aliases/SelectHandler.md) | Event callback when a date is selected. |
-| `min` | `number` | The minimum dates that can be selected. |
-| `max` | `number` | The maximum dates that can be selected. |
+| Prop Name | Type | Description |
+| ---------- | ----------------------------------------------- | --------------------------------------- |
+| `selected` | `Date[] \| undefined` | The selected dates. |
+| `onSelect` | `(selected, triggerDate, modifiers, e) => void` | Event callback when a date is selected. |
+| `min` | `number` | The minimum dates that can be selected. |
+| `max` | `number` | The maximum dates that can be selected. |
### Min and Max Dates
@@ -125,12 +125,12 @@ When the `mode` prop is set to `"range"`, DayPicker allows selecting a continuou
```
-| Prop Name | Type | Description |
-| ---------- | ---------------------------------------------------------------- | --------------------------------------- |
-| `selected` | [`DateRange`](../api/type-aliases/DateRange.md) | The selected range. |
-| `onSelect` | [`SelectHandler<'range'>`](../api/type-aliases/SelectHandler.md) | Event callback when a date is selected. |
-| `min` | `number` | The minimum dates that can be selected. |
-| `max` | `number` | The maximum dates that can be selected. |
+| Prop Name | Type | Description |
+| ---------- | ----------------------------------------------- | --------------------------------------- |
+| `selected` | [`DateRange`](../api/type-aliases/DateRange.md) | The selected range. |
+| `onSelect` | `(selected, triggerDate, modifiers, e) => void` | Event callback when a date is selected. |
+| `min` | `number` | The minimum dates that can be selected. |
+| `max` | `number` | The maximum dates that can be selected. |
```tsx
import { useState } from "react";
diff --git a/website/docs/using-daypicker/styling.mdx b/website/docs/using-daypicker/styling.mdx
index 4f2d419ac8..3633d026e4 100644
--- a/website/docs/using-daypicker/styling.mdx
+++ b/website/docs/using-daypicker/styling.mdx
@@ -125,7 +125,9 @@ export function MyDatePicker() {
## Custom Class Names
-Use the `classNames` prop to use other classnames instead of the default ones. The [`ClassNames`](../api/type-aliases/ClassNames.md) type lists all the class names used by DayPicker. They are the value of the [`UI`](../api/enumerations/UI.md), [`DayModifiers`](../api/enumerations/DayModifier.md) and [`CalendarFlag`](../api/enumerations/CalendarFlag.md) enums.
+Use the `classNames` prop to use other classnames instead of the default ones. The [`ClassNames`](../api/type-aliases/ClassNames.md) type lists all the class names used by DayPicker.
+
+They are the value of the [`UI`](../api/enumerations/UI.md), [`DayFlag`](../api/enumerations/DayFlag.md), [`SelectionState`](../api/enumerations/SelectionState.md), and [`CalendarFlag`](../api/enumerations/CalendarFlag.md) enums.
For example, to change the class name of the calendar container:
diff --git a/website/typedoc.mjs b/website/typedoc.mjs
index 40152b7523..b45bf06606 100644
--- a/website/typedoc.mjs
+++ b/website/typedoc.mjs
@@ -17,11 +17,11 @@ const options = {
"Props",
"Classes",
"Components",
- "Hooks",
- "Contexts",
- "Utilities",
"Formatters",
"Labels",
+ "Utilities",
+ "Hooks",
+ "Contexts",
"*"
],
readme: "none",