import React, { useCallback, useMemo, useRef } from "react"; import type { MouseEvent, FocusEvent, KeyboardEvent, ChangeEvent } from "react"; import { TZDate } from "@date-fns/tz"; import { UI, DayFlag, SelectionState } from "./UI.js"; import type { CalendarDay } from "./classes/CalendarDay.js"; import { DateLib, defaultLocale } from "./classes/DateLib.js"; import { createGetModifiers } from "./helpers/createGetModifiers.js"; import { getClassNamesForModifiers } from "./helpers/getClassNamesForModifiers.js"; import { getComponents } from "./helpers/getComponents.js"; import { getDataAttributes } from "./helpers/getDataAttributes.js"; import { getDefaultClassNames } from "./helpers/getDefaultClassNames.js"; import { getFormatters } from "./helpers/getFormatters.js"; import { getMonthOptions } from "./helpers/getMonthOptions.js"; import { getStyleForModifiers } from "./helpers/getStyleForModifiers.js"; import { getWeekdays } from "./helpers/getWeekdays.js"; import { getYearOptions } from "./helpers/getYearOptions.js"; import * as defaultLabels from "./labels/index.js"; import type { DayPickerProps, Modifiers, MoveFocusBy, MoveFocusDir, SelectedValue, SelectHandler } from "./types/index.js"; import { useAnimation } from "./useAnimation.js"; import { useCalendar } from "./useCalendar.js"; import { type DayPickerContext, dayPickerContext } from "./useDayPicker.js"; import { useFocus } from "./useFocus.js"; import { useSelection } from "./useSelection.js"; import { rangeIncludesDate } from "./utils/rangeIncludesDate.js"; import { isDateRange } from "./utils/typeguards.js"; /** * Renders the DayPicker calendar component. * * @param initialProps - The props for the DayPicker component. * @returns The rendered DayPicker component. * @group DayPicker * @see https://daypicker.dev */ export function DayPicker(initialProps: DayPickerProps) { let props = initialProps; if (props.timeZone) { props = { ...initialProps }; if (props.today) { props.today = new TZDate(props.today, props.timeZone); } if (props.month) { props.month = new TZDate(props.month, props.timeZone); } if (props.defaultMonth) { props.defaultMonth = new TZDate(props.defaultMonth, props.timeZone); } if (props.startMonth) { props.startMonth = new TZDate(props.startMonth, props.timeZone); } if (props.endMonth) { props.endMonth = new TZDate(props.endMonth, props.timeZone); } if (props.mode === "single" && props.selected) { props.selected = new TZDate(props.selected, props.timeZone); } else if (props.mode === "multiple" && props.selected) { props.selected = props.selected?.map( (date) => new TZDate(date, props.timeZone) ); } else if (props.mode === "range" && props.selected) { props.selected = { from: props.selected.from ? new TZDate(props.selected.from, props.timeZone) : undefined, to: props.selected.to ? new TZDate(props.selected.to, props.timeZone) : undefined }; } } const { components, formatters, labels, dateLib, locale, classNames } = useMemo(() => { const locale = { ...defaultLocale, ...props.locale }; const dateLib = new DateLib( { locale, weekStartsOn: props.broadcastCalendar ? 1 : props.weekStartsOn, firstWeekContainsDate: props.firstWeekContainsDate, useAdditionalWeekYearTokens: props.useAdditionalWeekYearTokens, useAdditionalDayOfYearTokens: props.useAdditionalDayOfYearTokens, timeZone: props.timeZone, numerals: props.numerals }, props.dateLib ); return { dateLib, components: getComponents(props.components), formatters: getFormatters(props.formatters), labels: { ...defaultLabels, ...props.labels }, locale, classNames: { ...getDefaultClassNames(), ...props.classNames } }; }, [ props.locale, props.broadcastCalendar, props.weekStartsOn, props.firstWeekContainsDate, props.useAdditionalWeekYearTokens, props.useAdditionalDayOfYearTokens, props.timeZone, props.numerals, props.dateLib, props.components, props.formatters, props.labels, props.classNames ]); const { captionLayout, mode, navLayout, numberOfMonths = 1, onDayBlur, onDayClick, onDayFocus, onDayKeyDown, onDayMouseEnter, onDayMouseLeave, onNextClick, onPrevClick, showWeekNumber, styles } = props; const { formatCaption, formatDay, formatMonthDropdown, formatWeekNumber, formatWeekNumberHeader, formatWeekdayName, formatYearDropdown } = formatters; const calendar = useCalendar(props, dateLib); const { days, months, navStart, navEnd, previousMonth, nextMonth, goToMonth } = calendar; const getModifiers = createGetModifiers( days, props, navStart, navEnd, dateLib ); const { isSelected, select, selected: selectedValue } = useSelection(props, dateLib) ?? {}; const { blur, focused, isFocusTarget, moveFocus, setFocused } = useFocus( props, calendar, getModifiers, isSelected ?? (() => false), dateLib ); const { labelDayButton, labelGridcell, labelGrid, labelMonthDropdown, labelNav, labelPrevious, labelNext, labelWeekday, labelWeekNumber, labelWeekNumberHeader, labelYearDropdown } = labels; const weekdays = useMemo( () => getWeekdays(dateLib, props.ISOWeek), [dateLib, props.ISOWeek] ); const isInteractive = mode !== undefined || onDayClick !== undefined; const handlePreviousClick = useCallback(() => { if (!previousMonth) return; goToMonth(previousMonth); onPrevClick?.(previousMonth); }, [previousMonth, goToMonth, onPrevClick]); const handleNextClick = useCallback(() => { if (!nextMonth) return; goToMonth(nextMonth); onNextClick?.(nextMonth); }, [goToMonth, nextMonth, onNextClick]); const handleDayClick = useCallback( (day: CalendarDay, m: Modifiers) => (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); setFocused(day); select?.(day.date, m, e); onDayClick?.(day.date, m, e); }, [select, onDayClick, setFocused] ); const handleDayFocus = useCallback( (day: CalendarDay, m: Modifiers) => (e: FocusEvent) => { setFocused(day); onDayFocus?.(day.date, m, e); }, [onDayFocus, setFocused] ); const handleDayBlur = useCallback( (day: CalendarDay, m: Modifiers) => (e: FocusEvent) => { blur(); onDayBlur?.(day.date, m, e); }, [blur, onDayBlur] ); const handleDayKeyDown = useCallback( (day: CalendarDay, modifiers: Modifiers) => (e: KeyboardEvent) => { const keyMap: Record = { ArrowLeft: [ e.shiftKey ? "month" : "day", props.dir === "rtl" ? "after" : "before" ], ArrowRight: [ e.shiftKey ? "month" : "day", props.dir === "rtl" ? "before" : "after" ], ArrowDown: [e.shiftKey ? "year" : "week", "after"], ArrowUp: [e.shiftKey ? "year" : "week", "before"], PageUp: [e.shiftKey ? "year" : "month", "before"], PageDown: [e.shiftKey ? "year" : "month", "after"], Home: ["startOfWeek", "before"], End: ["endOfWeek", "after"] }; if (keyMap[e.key]) { e.preventDefault(); e.stopPropagation(); const [moveBy, moveDir] = keyMap[e.key]; moveFocus(moveBy, moveDir); } onDayKeyDown?.(day.date, modifiers, e); }, [moveFocus, onDayKeyDown, props.dir] ); const handleDayMouseEnter = useCallback( (day: CalendarDay, modifiers: Modifiers) => (e: MouseEvent) => { onDayMouseEnter?.(day.date, modifiers, e); }, [onDayMouseEnter] ); const handleDayMouseLeave = useCallback( (day: CalendarDay, modifiers: Modifiers) => (e: MouseEvent) => { onDayMouseLeave?.(day.date, modifiers, e); }, [onDayMouseLeave] ); const handleMonthChange = useCallback( (date: Date) => (e: ChangeEvent) => { const selectedMonth = Number(e.target.value); const month = dateLib.setMonth(dateLib.startOfMonth(date), selectedMonth); goToMonth(month); }, [dateLib, goToMonth] ); const handleYearChange = useCallback( (date: Date) => (e: ChangeEvent) => { const selectedYear = Number(e.target.value); const month = dateLib.setYear(dateLib.startOfMonth(date), selectedYear); goToMonth(month); }, [dateLib, goToMonth] ); const { className, style } = useMemo( () => ({ className: [classNames[UI.Root], props.className] .filter(Boolean) .join(" "), style: { ...styles?.[UI.Root], ...props.style } }), [classNames, props.className, props.style, styles] ); const dataAttributes = getDataAttributes(props); const rootElRef = useRef(null); useAnimation(rootElRef, Boolean(props.animate), { classNames, months, focused, dateLib }); const contextValue: DayPickerContext = { dayPickerProps: props, selected: selectedValue as SelectedValue, select: select as SelectHandler, isSelected, months, nextMonth, previousMonth, goToMonth, getModifiers, components, classNames, styles, labels, formatters }; return ( {!props.hideNavigation && !navLayout && ( )} {months.map((calendarMonth, displayIndex) => { const dropdownMonths = getMonthOptions( calendarMonth.date, navStart, navEnd, formatters, dateLib ); const dropdownYears = getYearOptions( navStart, navEnd, formatters, dateLib ); return ( {navLayout === "around" && !props.hideNavigation && displayIndex === 0 && ( )} {captionLayout?.startsWith("dropdown") ? ( {captionLayout === "dropdown" || captionLayout === "dropdown-months" ? ( ) : ( {formatMonthDropdown(calendarMonth.date, dateLib)} )} {captionLayout === "dropdown" || captionLayout === "dropdown-years" ? ( ) : ( {formatYearDropdown(calendarMonth.date, dateLib)} )} {formatCaption( calendarMonth.date, dateLib.options, dateLib )} ) : ( {formatCaption( calendarMonth.date, dateLib.options, dateLib )} )} {navLayout === "around" && !props.hideNavigation && displayIndex === numberOfMonths - 1 && ( )} {displayIndex === numberOfMonths - 1 && navLayout === "after" && !props.hideNavigation && ( )} {!props.hideWeekdays && ( {showWeekNumber && ( {formatWeekNumberHeader()} )} {weekdays.map((weekday, i) => ( {formatWeekdayName(weekday, dateLib.options, dateLib)} ))} )} {calendarMonth.weeks.map((week, weekIndex) => { return ( {showWeekNumber && ( {formatWeekNumber(week.weekNumber, dateLib)} )} {week.days.map((day: CalendarDay) => { const { date } = day; const modifiers = getModifiers(day); modifiers[DayFlag.focused] = !modifiers.hidden && Boolean(focused?.isEqualTo(day)); modifiers[SelectionState.selected] = isSelected?.(date) || modifiers.selected; if (isDateRange(selectedValue)) { // add range modifiers const { from, to } = selectedValue; modifiers[SelectionState.range_start] = Boolean( from && to && dateLib.isSameDay(date, from) ); modifiers[SelectionState.range_end] = Boolean( from && to && dateLib.isSameDay(date, to) ); modifiers[SelectionState.range_middle] = rangeIncludesDate( selectedValue, date, true, dateLib ); } const style = getStyleForModifiers( modifiers, styles, props.modifiersStyles ); const className = getClassNamesForModifiers( modifiers, classNames, props.modifiersClassNames ); const ariaLabel = !isInteractive && !modifiers.hidden ? labelGridcell( date, modifiers, dateLib.options, dateLib ) : undefined; return ( {!modifiers.hidden && isInteractive ? ( {formatDay(date, dateLib.options, dateLib)} ) : ( !modifiers.hidden && formatDay(day.date, dateLib.options, dateLib) )} ); })} ); })} ); })} {props.footer && ( {props.footer} )} ); }