From 9e7aff3cee5a822cb030471d8dff97c04aeade6e Mon Sep 17 00:00:00 2001 From: Giampaolo Bellavite Date: Thu, 20 Jun 2024 07:15:34 -0500 Subject: [PATCH] refactor: simplify props, contexts (#2211) --- examples/CustomCaption.tsx | 8 +- examples/CustomMultiple.tsx | 7 +- examples/CustomSingle.tsx | 2 +- examples/CustomWeek.test.tsx | 4 +- examples/Dialog.tsx | 4 +- examples/Input.test.tsx | 2 +- examples/Keyboard.tsx | 8 +- examples/MultipleMinMax.tsx | 11 +- examples/None.tsx | 2 +- examples/Range.test.tsx | 2 +- examples/RangeShiftKey.tsx | 6 +- examples/Single.test.tsx | 5 +- examples/TestCase2047.tsx | 2 +- examples/Testcase1567.tsx | 6 +- examples/__snapshots__/Range.test.tsx.snap | 10 +- react-day-picker.code-workspace | 12 +- src/DayPicker.tsx | 11 +- src/UI.ts | 40 +- src/components/Button.tsx | 5 +- src/components/Calendar.tsx | 14 +- src/components/Chevron.tsx | 4 +- src/components/Day.tsx | 6 +- src/components/DayDate.tsx | 15 +- src/components/DayWrapper.tsx | 107 +-- src/components/Dropdown.tsx | 6 +- src/components/DropdownNav.tsx | 4 +- src/components/Footer.tsx | 2 +- src/components/Month.tsx | 8 +- src/components/MonthCaption.tsx | 4 +- src/components/MonthsDropdown.tsx | 10 +- src/components/Nav.tsx | 8 +- src/components/Week.tsx | 4 +- src/components/WeekNumber.tsx | 6 +- src/components/Weekday.tsx | 4 +- src/components/Weekdays.tsx | 4 +- src/components/YearsDropdown.tsx | 10 +- .../{calendar.test.tsx => calendar.test.ts} | 14 +- src/contexts/calendar.tsx | 146 ++-- src/contexts/focus.test.tsx | 12 +- src/contexts/focus.tsx | 135 ++-- src/contexts/modifiers.tsx | 242 +++--- src/contexts/props.tsx | 162 ++-- src/contexts/providers.tsx | 48 ++ src/contexts/root.tsx | 33 - src/contexts/selection.tsx | 264 ------- src/formatters/formatMonthDropdown.test.ts | 7 +- src/formatters/formatMonthDropdown.ts | 7 +- src/helpers/calculateMonthWeeks.test.ts | 47 -- src/helpers/calculateMonthWeeks.ts | 60 -- src/helpers/getClassNamesForModifiers.ts | 19 +- src/helpers/getDataAttributes.tsx | 8 +- src/helpers/getDates.ts | 13 +- src/helpers/getDefaultClassNames.ts | 16 +- src/helpers/getDisplayMonths.test.ts | 22 +- src/helpers/getDisplayMonths.ts | 17 +- src/helpers/getDropdownMonths.test.ts | 93 +-- src/helpers/getDropdownMonths.ts | 41 +- src/helpers/getDropdownYears.test.ts | 90 +-- src/helpers/getDropdownYears.ts | 33 +- src/helpers/getFormatters.test.ts | 1 - src/helpers/getFormatters.ts | 4 +- ...tMonth.test.ts => getInitialMonth.test.ts} | 12 +- .../{getStartMonth.ts => getInitialMonth.ts} | 7 +- src/helpers/getMonthWeeks.test.ts | 100 --- src/helpers/getMonthWeeks.ts | 55 -- src/helpers/getMonths.test.ts | 168 ++--- src/helpers/getMonths.ts | 33 +- src/helpers/getNextFocus.test.tsx | 7 +- src/helpers/getNextFocus.tsx | 9 +- src/helpers/getNextMonth.test.ts | 3 + src/helpers/getNextMonth.ts | 33 +- src/helpers/getPossibleFocusDate.test.ts | 7 +- src/helpers/getPossibleFocusDate.ts | 8 +- src/helpers/getPreviousMonth.test.ts | 116 ++- src/helpers/getPreviousMonth.ts | 22 +- src/helpers/getStartEndMonths.ts | 6 +- src/helpers/getStyleForModifiers.test.ts | 12 +- src/helpers/getStyleForModifiers.ts | 4 +- src/index.ts | 11 +- src/labels/labelDay.test.ts | 4 +- src/labels/labelDay.ts | 4 +- src/selection/multi.tsx | 110 +++ src/selection/range.tsx | 117 +++ src/selection/single.tsx | 84 +++ src/types.test.tsx | 52 -- src/types.ts | 699 ------------------ .../deprecated.ts} | 55 +- src/types/index.ts | 3 + src/types/props.test.tsx | 64 ++ src/types/props.ts | 416 +++++++++++ src/types/shared.ts | 291 ++++++++ src/utils/addToRange.test.ts | 6 +- src/utils/addToRange.ts | 9 +- src/utils/isDateInRange.test.ts | 2 +- test/render.tsx | 6 +- test/renderHook.tsx | 7 +- .../advanced-guides/custom-components.mdx | 15 +- .../docs/advanced-guides/custom-modifiers.mdx | 2 +- website/docs/upgrading.mdx | 18 +- .../docs/using-daypicker/selection-modes.mdx | 46 +- website/docs/using-daypicker/styling.mdx | 4 +- website/typedoc.mjs | 6 +- 102 files changed, 2187 insertions(+), 2343 deletions(-) rename src/contexts/{calendar.test.tsx => calendar.test.ts} (77%) create mode 100644 src/contexts/providers.tsx delete mode 100644 src/contexts/root.tsx delete mode 100644 src/contexts/selection.tsx delete mode 100644 src/helpers/calculateMonthWeeks.test.ts delete mode 100644 src/helpers/calculateMonthWeeks.ts rename src/helpers/{getStartMonth.test.ts => getInitialMonth.test.ts} (83%) rename src/helpers/{getStartMonth.ts => getInitialMonth.ts} (90%) delete mode 100644 src/helpers/getMonthWeeks.test.ts delete mode 100644 src/helpers/getMonthWeeks.ts create mode 100644 src/selection/multi.tsx create mode 100644 src/selection/range.tsx create mode 100644 src/selection/single.tsx delete mode 100644 src/types.test.tsx delete mode 100644 src/types.ts rename src/{types-deprecated.ts => types/deprecated.ts} (80%) create mode 100644 src/types/index.ts create mode 100644 src/types/props.test.tsx create mode 100644 src/types/props.ts create mode 100644 src/types/shared.ts 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",