import { useEffect, useMemo } from "react"; import type { CalendarDay, CalendarMonth, CalendarWeek, DateLib, } from "./classes/index.js"; import { getDates } from "./helpers/getDates.js"; import { getDays } from "./helpers/getDays.js"; import { getDisplayMonths } from "./helpers/getDisplayMonths.js"; import { getInitialMonth } from "./helpers/getInitialMonth.js"; import { getMonths } from "./helpers/getMonths.js"; import { getNavMonths } from "./helpers/getNavMonth.js"; import { getNextMonth } from "./helpers/getNextMonth.js"; import { getPreviousMonth } from "./helpers/getPreviousMonth.js"; import { getWeeks } from "./helpers/getWeeks.js"; import { useControlledValue } from "./helpers/useControlledValue.js"; import type { DayPickerProps } from "./types/props.js"; /** * Return the calendar object to work with the calendar in custom components. * * @see https://daypicker.dev/guides/custom-components */ export interface Calendar { /** * All the days displayed in the calendar. As opposite from * {@link CalendarContext.dates}, it may return duplicated dates when shown * outside the month. */ days: CalendarDay[]; /** The months displayed in the calendar. */ weeks: CalendarWeek[]; /** The months displayed in the calendar. */ months: CalendarMonth[]; /** The next month to display. */ nextMonth: Date | undefined; /** The previous month to display. */ previousMonth: Date | undefined; /** * The month where the navigation starts. `undefined` if the calendar can be * navigated indefinitely to the past. */ navStart: Date | undefined; /** * The month where the navigation ends. `undefined` if the calendar can be * navigated indefinitely to the past. */ navEnd: Date | undefined; /** Navigate to the specified month. Will fire the `onMonthChange` callback. */ goToMonth: (month: Date) => void; /** * Navigate to the specified date. If the second parameter (refDate) is * provided and the date is before the refDate, then the month is set to one * month before the date. * * @param day - The date to navigate to. * @param dateToCompare - Optional. If `date` is before `dateToCompare`, the * month is set to one month before the date. */ goToDay: (day: CalendarDay) => void; } /** * Provides the calendar object to work with the calendar in custom components. * * @private * @param props - The DayPicker props related to calendar configuration. * @param dateLib - The date utility library instance. * @returns The calendar object containing displayed days, weeks, months, and * navigation methods. */ export function useCalendar( props: Pick< DayPickerProps, | "captionLayout" | "endMonth" | "startMonth" | "today" | "fixedWeeks" | "ISOWeek" | "numberOfMonths" | "pagedNavigation" | "reverseMonths" | "disableNavigation" | "onMonthChange" | "month" | "defaultMonth" | "timeZone" | "broadcastCalendar" // Deprecated: | "fromMonth" | "fromYear" | "toMonth" | "toYear" >, dateLib: DateLib, ): Calendar { const [navStart, navEnd] = getNavMonths(props, dateLib); const { startOfMonth, endOfMonth } = dateLib; const initialMonth = getInitialMonth(props, navStart, navEnd, dateLib); const [firstMonth, setFirstMonth] = useControlledValue( initialMonth, // initialMonth is always computed from props.month if provided props.month ? initialMonth : undefined, ); // biome-ignore lint/correctness/useExhaustiveDependencies: change the initial month when the time zone changes. useEffect(() => { const newInitialMonth = getInitialMonth(props, navStart, navEnd, dateLib); setFirstMonth(newInitialMonth); }, [props.timeZone]); /** The months displayed in the calendar. */ // biome-ignore lint/correctness/useExhaustiveDependencies: We want to recompute only when specific props change. const { months, weeks, days, previousMonth, nextMonth } = useMemo(() => { const displayMonths = getDisplayMonths( firstMonth, navEnd, { numberOfMonths: props.numberOfMonths }, dateLib, ); const dates = getDates( displayMonths, props.endMonth ? endOfMonth(props.endMonth) : undefined, { ISOWeek: props.ISOWeek, fixedWeeks: props.fixedWeeks, broadcastCalendar: props.broadcastCalendar, }, dateLib, ); const months = getMonths( displayMonths, dates, { broadcastCalendar: props.broadcastCalendar, fixedWeeks: props.fixedWeeks, ISOWeek: props.ISOWeek, reverseMonths: props.reverseMonths, }, dateLib, ); const weeks = getWeeks(months); const days = getDays(months); const previousMonth = getPreviousMonth( firstMonth, navStart, props, dateLib, ); const nextMonth = getNextMonth(firstMonth, navEnd, props, dateLib); return { months, weeks, days, previousMonth, nextMonth, }; }, [ dateLib, firstMonth.getTime(), navEnd?.getTime(), navStart?.getTime(), props.disableNavigation, props.broadcastCalendar, props.endMonth?.getTime(), props.fixedWeeks, props.ISOWeek, props.numberOfMonths, props.pagedNavigation, props.reverseMonths, ]); const { disableNavigation, onMonthChange } = props; const isDayInCalendar = (day: CalendarDay) => weeks.some((week: CalendarWeek) => week.days.some((d) => d.isEqualTo(day))); const goToMonth = (date: Date) => { if (disableNavigation) { return; } let newMonth = startOfMonth(date); // if month is before start, use the first month instead if (navStart && newMonth < startOfMonth(navStart)) { newMonth = startOfMonth(navStart); } // if month is after endMonth, use the last month instead if (navEnd && newMonth > startOfMonth(navEnd)) { newMonth = startOfMonth(navEnd); } setFirstMonth(newMonth); onMonthChange?.(newMonth); }; const goToDay = (day: CalendarDay) => { // is this check necessary? if (isDayInCalendar(day)) { return; } goToMonth(day.date); }; const calendar = { months, weeks, days, navStart, navEnd, previousMonth, nextMonth, goToMonth, goToDay, }; return calendar; }