UNPKG

react-day-picker

Version:

Customizable Date Picker for React

230 lines 16.9 kB
import React, { useCallback, useMemo, useRef } from "react"; import { UI, DayFlag, SelectionState } from "./UI.js"; import { DateLib, defaultLocale } from "./classes/DateLib.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 { useAnimation } from "./useAnimation.js"; import { useCalendar } from "./useCalendar.js"; import { dayPickerContext } from "./useDayPicker.js"; import { useFocus } from "./useFocus.js"; import { useGetModifiers } from "./useGetModifiers.js"; import { useSelection } from "./useSelection.js"; import { rangeIncludesDate } from "./utils/rangeIncludesDate.js"; import { isDateRange } from "./utils/typeguards.js"; /** * Render the date picker calendar. * * @group DayPicker * @see https://daypicker.dev */ export function DayPicker(props) { 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, 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 = useGetModifiers(days, props, 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, 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, m) => (e) => { e.preventDefault(); e.stopPropagation(); setFocused(day); select?.(day.date, m, e); onDayClick?.(day.date, m, e); }, [select, onDayClick, setFocused]); const handleDayFocus = useCallback((day, m) => (e) => { setFocused(day); onDayFocus?.(day.date, m, e); }, [onDayFocus, setFocused]); const handleDayBlur = useCallback((day, m) => (e) => { blur(); onDayBlur?.(day.date, m, e); }, [blur, onDayBlur]); const handleDayKeyDown = useCallback((day, modifiers) => (e) => { const keyMap = { ArrowLeft: ["day", props.dir === "rtl" ? "after" : "before"], ArrowRight: ["day", props.dir === "rtl" ? "before" : "after"], ArrowDown: ["week", "after"], ArrowUp: ["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, modifiers) => (e) => { onDayMouseEnter?.(day.date, modifiers, e); }, [onDayMouseEnter]); const handleDayMouseLeave = useCallback((day, modifiers) => (e) => { onDayMouseLeave?.(day.date, modifiers, e); }, [onDayMouseLeave]); const handleMonthChange = useCallback((date) => (e) => { const selectedMonth = Number(e.target.value); const month = dateLib.setMonth(dateLib.startOfMonth(date), selectedMonth); goToMonth(month); }, [dateLib, goToMonth]); const handleYearChange = useCallback((date) => (e) => { 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 = { dayPickerProps: props, selected: selectedValue, select: select, isSelected, months, nextMonth, previousMonth, goToMonth, getModifiers, components, classNames, styles, labels, formatters }; return (React.createElement(dayPickerContext.Provider, { value: contextValue }, React.createElement(components.Root, { rootRef: props.animate ? rootElRef : undefined, className: className, style: style, dir: props.dir, id: props.id, lang: props.lang, nonce: props.nonce, title: props.title, role: props.role, "aria-label": props["aria-label"], ...dataAttributes }, React.createElement(components.Months, { className: classNames[UI.Months], style: styles?.[UI.Months] }, !props.hideNavigation && (React.createElement(components.Nav, { "data-animated-nav": props.animate ? "true" : undefined, className: classNames[UI.Nav], style: styles?.[UI.Nav], "aria-label": labelNav(), onPreviousClick: handlePreviousClick, onNextClick: handleNextClick, previousMonth: previousMonth, nextMonth: nextMonth })), months.map((calendarMonth, displayIndex) => { const dropdownMonths = getMonthOptions(calendarMonth.date, navStart, navEnd, formatters, dateLib); const dropdownYears = getYearOptions(navStart, navEnd, formatters, dateLib); return (React.createElement(components.Month, { "data-animated-month": props.animate ? "true" : undefined, className: classNames[UI.Month], style: styles?.[UI.Month], key: displayIndex, displayIndex: displayIndex, calendarMonth: calendarMonth }, React.createElement(components.MonthCaption, { "data-animated-caption": props.animate ? "true" : undefined, className: classNames[UI.MonthCaption], style: styles?.[UI.MonthCaption], calendarMonth: calendarMonth, displayIndex: displayIndex }, captionLayout?.startsWith("dropdown") ? (React.createElement(components.DropdownNav, { className: classNames[UI.Dropdowns], style: styles?.[UI.Dropdowns] }, captionLayout === "dropdown" || captionLayout === "dropdown-months" ? (React.createElement(components.MonthsDropdown, { className: classNames[UI.MonthsDropdown], "aria-label": labelMonthDropdown(), classNames: classNames, components: components, disabled: Boolean(props.disableNavigation), onChange: handleMonthChange(calendarMonth.date), options: dropdownMonths, style: styles?.[UI.Dropdown], value: dateLib.getMonth(calendarMonth.date) })) : (React.createElement("span", null, formatMonthDropdown(calendarMonth.date, dateLib))), captionLayout === "dropdown" || captionLayout === "dropdown-years" ? (React.createElement(components.YearsDropdown, { className: classNames[UI.YearsDropdown], "aria-label": labelYearDropdown(dateLib.options), classNames: classNames, components: components, disabled: Boolean(props.disableNavigation), onChange: handleYearChange(calendarMonth.date), options: dropdownYears, style: styles?.[UI.Dropdown], value: dateLib.getYear(calendarMonth.date) })) : (React.createElement("span", null, formatYearDropdown(calendarMonth.date, dateLib))), React.createElement("span", { role: "status", "aria-live": "polite", style: { border: 0, clip: "rect(0 0 0 0)", height: "1px", margin: "-1px", overflow: "hidden", padding: 0, position: "absolute", width: "1px", whiteSpace: "nowrap", wordWrap: "normal" } }, formatCaption(calendarMonth.date, dateLib.options, dateLib)))) : (React.createElement(components.CaptionLabel, { className: classNames[UI.CaptionLabel], role: "status", "aria-live": "polite" }, formatCaption(calendarMonth.date, dateLib.options, dateLib)))), React.createElement(components.MonthGrid, { role: "grid", "aria-multiselectable": mode === "multiple" || mode === "range", "aria-label": labelGrid(calendarMonth.date, dateLib.options, dateLib) || undefined, className: classNames[UI.MonthGrid], style: styles?.[UI.MonthGrid] }, !props.hideWeekdays && (React.createElement(components.Weekdays, { "data-animated-weekdays": props.animate ? "true" : undefined, className: classNames[UI.Weekdays], style: styles?.[UI.Weekdays] }, showWeekNumber && (React.createElement(components.WeekNumberHeader, { "aria-label": labelWeekNumberHeader(dateLib.options), className: classNames[UI.WeekNumberHeader], style: styles?.[UI.WeekNumberHeader], scope: "col" }, formatWeekNumberHeader())), weekdays.map((weekday, i) => (React.createElement(components.Weekday, { "aria-label": labelWeekday(weekday, dateLib.options, dateLib), className: classNames[UI.Weekday], key: i, style: styles?.[UI.Weekday], scope: "col" }, formatWeekdayName(weekday, dateLib.options, dateLib)))))), React.createElement(components.Weeks, { "data-animated-weeks": props.animate ? "true" : undefined, className: classNames[UI.Weeks], style: styles?.[UI.Weeks] }, calendarMonth.weeks.map((week, weekIndex) => { return (React.createElement(components.Week, { className: classNames[UI.Week], key: week.weekNumber, style: styles?.[UI.Week], week: week }, showWeekNumber && (React.createElement(components.WeekNumber, { week: week, style: styles?.[UI.WeekNumber], "aria-label": labelWeekNumber(week.weekNumber, { locale }), className: classNames[UI.WeekNumber], scope: "row", role: "rowheader" }, formatWeekNumber(week.weekNumber))), week.days.map((day) => { 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 (React.createElement(components.Day, { key: `${dateLib.format(date, "yyyy-MM-dd")}_${dateLib.format(day.displayMonth, "yyyy-MM")}`, day: day, modifiers: modifiers, className: className.join(" "), style: style, role: "gridcell", "aria-selected": modifiers.selected || undefined, "aria-label": ariaLabel, "data-day": dateLib.format(date, "yyyy-MM-dd"), "data-month": day.outside ? dateLib.format(date, "yyyy-MM") : undefined, "data-selected": modifiers.selected || undefined, "data-disabled": modifiers.disabled || undefined, "data-hidden": modifiers.hidden || undefined, "data-outside": day.outside || undefined, "data-focused": modifiers.focused || undefined, "data-today": modifiers.today || undefined }, !modifiers.hidden && isInteractive ? (React.createElement(components.DayButton, { className: classNames[UI.DayButton], style: styles?.[UI.DayButton], type: "button", day: day, modifiers: modifiers, disabled: modifiers.disabled || undefined, tabIndex: isFocusTarget(day) ? 0 : -1, "aria-label": labelDayButton(date, modifiers, dateLib.options, dateLib), onClick: handleDayClick(day, modifiers), onBlur: handleDayBlur(day, modifiers), onFocus: handleDayFocus(day, modifiers), onKeyDown: handleDayKeyDown(day, modifiers), onMouseEnter: handleDayMouseEnter(day, modifiers), onMouseLeave: handleDayMouseLeave(day, modifiers) }, formatDay(date, dateLib.options, dateLib))) : (!modifiers.hidden && formatDay(day.date, dateLib.options, dateLib)))); }))); }))))); })), props.footer && (React.createElement(components.Footer, { className: classNames[UI.Footer], style: styles?.[UI.Footer], role: "status", "aria-live": "polite" }, props.footer))))); } //# sourceMappingURL=DayPicker.js.map