react-calendar-plus
Version:
A flexible, themeable, and internationalized React calendar component with date and time selection. Includes month/week/day views, range selection, and custom event handling.
1,196 lines (1,186 loc) • 39.4 kB
JavaScript
// src/components/Calendar.tsx
import { useState as useState3, useEffect as useEffect3, useMemo } from "react";
import { format as format4, startOfWeek as startOfWeek2, isSameDay as isSameDay2 } from "date-fns";
import { enUS as enUS4 } from "date-fns/locale";
import clsx5 from "clsx";
// src/components/CalendarHeader.tsx
import clsx2 from "clsx";
// src/utils/dateUtils.ts
import {
format,
startOfMonth,
endOfMonth,
startOfWeek,
endOfWeek,
addDays,
addWeeks,
addMonths,
subDays,
subWeeks,
subMonths,
setYear,
setMonth,
isSameDay,
isSameMonth,
isWithinInterval,
parseISO,
isValid,
getDay,
eachDayOfInterval,
getWeek
} from "date-fns";
import { enUS } from "date-fns/locale";
var DEFAULT_LOCALE = {
locale: "en-US",
weekStartsOn: 0,
// Sunday
formatOptions: {
weekday: "short",
month: "long",
day: "numeric"
}
};
var getCalendarDays = (date, locale = DEFAULT_LOCALE) => {
const start = startOfWeek(startOfMonth(date), { weekStartsOn: locale.weekStartsOn });
const end = endOfWeek(endOfMonth(date), { weekStartsOn: locale.weekStartsOn });
return eachDayOfInterval({ start, end });
};
var getWeekDays = (date, locale = DEFAULT_LOCALE) => {
const start = startOfWeek(date, { weekStartsOn: locale.weekStartsOn });
const end = endOfWeek(date, { weekStartsOn: locale.weekStartsOn });
return eachDayOfInterval({ start, end });
};
var formatDate = (date, formatString) => {
return format(date, formatString, { locale: enUS });
};
var formatDateForDisplay = (date, locale = DEFAULT_LOCALE) => {
return new Intl.DateTimeFormat(locale.locale, locale.formatOptions).format(date);
};
var isDateInRange = (date, range) => {
if (!range.start || !range.end) return false;
return isWithinInterval(date, { start: range.start, end: range.end });
};
var isDateSelected = (date, selectedValue) => {
if (!selectedValue) return false;
if (selectedValue instanceof Date) {
return isSameDay(date, selectedValue);
}
if (Array.isArray(selectedValue)) {
return selectedValue.some((selectedDate) => isSameDay(date, selectedDate));
}
if (selectedValue.start && selectedValue.end) {
return isDateInRange(date, selectedValue);
} else if (selectedValue.start) {
return isSameDay(date, selectedValue.start);
}
return false;
};
var isDateDisabled = (date, minDate, maxDate, disabledDates, disabledDaysOfWeek, disableWeekends) => {
if (minDate && date < minDate) return true;
if (maxDate && date > maxDate) return true;
if (disabledDates && disabledDates.some((disabledDate) => isSameDay(date, disabledDate))) {
return true;
}
const dayOfWeek = getDay(date);
if (disabledDaysOfWeek && disabledDaysOfWeek.includes(dayOfWeek)) {
return true;
}
if (disableWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) {
return true;
}
return false;
};
var getViewTitle = (date, view) => {
switch (view) {
case "month":
return format(date, "MMMM yyyy", { locale: enUS });
case "week":
const weekStart = startOfWeek(date);
const weekEnd = endOfWeek(date);
if (isSameMonth(weekStart, weekEnd)) {
return `${format(weekStart, "MMM d", { locale: enUS })} - ${format(weekEnd, "d, yyyy", { locale: enUS })}`;
} else {
return `${format(weekStart, "MMM d", { locale: enUS })} - ${format(weekEnd, "MMM d, yyyy", { locale: enUS })}`;
}
case "day":
return format(date, "EEEE, MMMM d, yyyy", { locale: enUS });
default:
return format(date, "MMMM yyyy", { locale: enUS });
}
};
var navigateDate = (date, direction, view) => {
switch (view) {
case "month":
return direction === "previous" ? subMonths(date, 1) : addMonths(date, 1);
case "week":
return direction === "previous" ? subWeeks(date, 1) : addWeeks(date, 1);
case "day":
return direction === "previous" ? subDays(date, 1) : addDays(date, 1);
default:
return date;
}
};
var parseDateTime = (dateTimeString) => {
try {
const parsed = parseISO(dateTimeString);
return isValid(parsed) ? parsed : null;
} catch {
return null;
}
};
var combineDateTime = (date, time) => {
const newDate = new Date(date);
newDate.setHours(time.getHours(), time.getMinutes(), time.getSeconds());
return newDate;
};
var getWeekNumber = (date) => {
return getWeek(date);
};
var navigateToYear = (date, year) => {
return setYear(date, year);
};
var navigateToMonth = (date, month) => {
return setMonth(date, month);
};
var navigateToYearMonth = (date, year, month) => {
return setMonth(setYear(date, year), month);
};
var getYearRange = (centerYear, range = 50) => {
return Array.from({ length: range * 2 + 1 }, (_, i) => centerYear - range + i);
};
var getMonthNames = () => {
return Array.from(
{ length: 12 },
(_, i) => format(new Date(2024, i, 1), "MMMM", { locale: enUS })
);
};
// src/components/YearMonthSelector.tsx
import { useState, useRef, useEffect } from "react";
import clsx from "clsx";
import { format as format2, setYear as setYear2, setMonth as setMonth2, getYear, getMonth } from "date-fns";
import { enUS as enUS2 } from "date-fns/locale";
import { jsx, jsxs } from "react/jsx-runtime";
var YearMonthSelector = ({
currentDate,
onDateChange,
minDate,
maxDate,
className
}) => {
const [isYearOpen, setIsYearOpen] = useState(false);
const [isMonthOpen, setIsMonthOpen] = useState(false);
const yearRef = useRef(null);
const monthRef = useRef(null);
const currentYear = getYear(currentDate);
const currentMonth = getMonth(currentDate);
const yearOptions = Array.from({ length: 101 }, (_, i) => currentYear - 50 + i);
const monthOptions = Array.from({ length: 12 }, (_, i) => ({
value: i,
label: format2(new Date(2024, i, 1), "MMMM", { locale: enUS2 })
}));
const filteredYearOptions = yearOptions.filter((year) => {
if (minDate && year < getYear(minDate)) return false;
if (maxDate && year > getYear(maxDate)) return false;
return true;
});
const filteredMonthOptions = monthOptions.filter(({ value }) => {
const testDate = new Date(currentYear, value, 1);
if (minDate && testDate < minDate) return false;
if (maxDate && testDate > maxDate) return false;
return true;
});
useEffect(() => {
const handleClickOutside = (event) => {
if (yearRef.current && !yearRef.current.contains(event.target)) {
setIsYearOpen(false);
}
if (monthRef.current && !monthRef.current.contains(event.target)) {
setIsMonthOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleYearChange = (year) => {
const newDate = setYear2(currentDate, year);
onDateChange(newDate);
setIsYearOpen(false);
};
const handleMonthChange = (month) => {
const newDate = setMonth2(currentDate, month);
onDateChange(newDate);
setIsMonthOpen(false);
};
return /* @__PURE__ */ jsxs("div", { className: clsx("rcp-year-month-selector", className), children: [
/* @__PURE__ */ jsxs("div", { className: "rcp-year-month-selector__group", ref: monthRef, children: [
/* @__PURE__ */ jsxs(
"button",
{
type: "button",
className: "rcp-year-month-selector__button",
onClick: () => setIsMonthOpen(!isMonthOpen),
"aria-label": "Select month",
children: [
format2(currentDate, "MMMM", { locale: enUS2 }),
/* @__PURE__ */ jsx(
"svg",
{
className: clsx(
"rcp-year-month-selector__arrow",
isMonthOpen && "rcp-year-month-selector__arrow--open"
),
width: "12",
height: "12",
viewBox: "0 0 12 12",
fill: "currentColor",
children: /* @__PURE__ */ jsx("path", { d: "M3 4.5L6 7.5L9 4.5", stroke: "currentColor", strokeWidth: "1.5", fill: "none", strokeLinecap: "round", strokeLinejoin: "round" })
}
)
]
}
),
isMonthOpen && /* @__PURE__ */ jsx("div", { className: "rcp-year-month-selector__dropdown", children: /* @__PURE__ */ jsx("div", { className: "rcp-year-month-selector__dropdown-content", children: filteredMonthOptions.map(({ value, label }) => /* @__PURE__ */ jsx(
"button",
{
type: "button",
className: clsx(
"rcp-year-month-selector__option",
currentMonth === value && "rcp-year-month-selector__option--selected"
),
onClick: () => handleMonthChange(value),
children: label
},
value
)) }) })
] }),
/* @__PURE__ */ jsxs("div", { className: "rcp-year-month-selector__group", ref: yearRef, children: [
/* @__PURE__ */ jsxs(
"button",
{
type: "button",
className: "rcp-year-month-selector__button",
onClick: () => setIsYearOpen(!isYearOpen),
"aria-label": "Select year",
children: [
currentYear,
/* @__PURE__ */ jsx(
"svg",
{
className: clsx(
"rcp-year-month-selector__arrow",
isYearOpen && "rcp-year-month-selector__arrow--open"
),
width: "12",
height: "12",
viewBox: "0 0 12 12",
fill: "currentColor",
children: /* @__PURE__ */ jsx("path", { d: "M3 4.5L6 7.5L9 4.5", stroke: "currentColor", strokeWidth: "1.5", fill: "none", strokeLinecap: "round", strokeLinejoin: "round" })
}
)
]
}
),
isYearOpen && /* @__PURE__ */ jsx("div", { className: "rcp-year-month-selector__dropdown", children: /* @__PURE__ */ jsx("div", { className: "rcp-year-month-selector__dropdown-content rcp-year-month-selector__dropdown-content--years", children: filteredYearOptions.map((year) => /* @__PURE__ */ jsx(
"button",
{
type: "button",
className: clsx(
"rcp-year-month-selector__option",
currentYear === year && "rcp-year-month-selector__option--selected"
),
onClick: () => handleYearChange(year),
children: year
},
year
)) }) })
] })
] });
};
// src/components/CalendarHeader.tsx
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
var CalendarHeader = ({
currentDate,
view,
onPrevious,
onNext,
onViewChange,
onDateChange,
minDate,
maxDate,
className
}) => {
const viewOptions = [
{ value: "month", label: "Month" },
{ value: "week", label: "Week" },
{ value: "day", label: "Day" }
];
return /* @__PURE__ */ jsxs2("div", { className: clsx2("rcp-calendar-header", className), children: [
/* @__PURE__ */ jsxs2("div", { className: "rcp-calendar-header__navigation", children: [
/* @__PURE__ */ jsx2(
"button",
{
type: "button",
onClick: onPrevious,
className: "rcp-calendar-header__nav-button",
"aria-label": "Previous",
children: /* @__PURE__ */ jsx2("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsx2("path", { d: "M10.5 13L5.5 8L10.5 3", stroke: "currentColor", strokeWidth: "2", fill: "none", strokeLinecap: "round", strokeLinejoin: "round" }) })
}
),
/* @__PURE__ */ jsx2("div", { className: "rcp-calendar-header__title-section", children: view === "month" && onDateChange ? /* @__PURE__ */ jsx2(
YearMonthSelector,
{
currentDate,
onDateChange,
minDate,
maxDate,
className: "rcp-calendar-header__year-month-selector"
}
) : /* @__PURE__ */ jsx2("h2", { className: "rcp-calendar-header__title", children: getViewTitle(currentDate, view) }) }),
/* @__PURE__ */ jsx2(
"button",
{
type: "button",
onClick: onNext,
className: "rcp-calendar-header__nav-button",
"aria-label": "Next",
children: /* @__PURE__ */ jsx2("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "currentColor", children: /* @__PURE__ */ jsx2("path", { d: "M5.5 3L10.5 8L5.5 13", stroke: "currentColor", strokeWidth: "2", fill: "none", strokeLinecap: "round", strokeLinejoin: "round" }) })
}
)
] }),
/* @__PURE__ */ jsx2("div", { className: "rcp-calendar-header__view-selector", children: viewOptions.map(({ value, label }) => /* @__PURE__ */ jsx2(
"button",
{
type: "button",
onClick: () => onViewChange(value),
className: clsx2(
"rcp-calendar-header__view-button",
view === value && "rcp-calendar-header__view-button--active"
),
children: label
},
value
)) })
] });
};
// src/components/CalendarDay.tsx
import { isToday, isSameMonth as isSameMonth2 } from "date-fns";
import clsx3 from "clsx";
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
var CalendarDay = ({
date,
currentMonth,
selectedValue,
events,
onDateClick,
dayRenderer,
minDate,
maxDate,
disabledDates,
disabledDaysOfWeek,
disableWeekends,
showOtherMonths = true,
className
}) => {
const isCurrentDay = isToday(date);
const isSelected = isDateSelected(date, selectedValue);
const isDisabled = isDateDisabled(
date,
minDate,
maxDate,
disabledDates,
disabledDaysOfWeek,
disableWeekends
);
const isOutOfMonth = !isSameMonth2(date, currentMonth);
const dayEvents = events.filter(
(event) => event.start <= date && (!event.end || event.end >= date)
);
const handleClick = (event) => {
if (isDisabled) return;
onDateClick(date, event);
};
const dayProps = {
date,
isToday: isCurrentDay,
isSelected,
isInRange: false,
// This will be handled by the parent for range selection
isDisabled,
isOutOfMonth,
events: dayEvents,
onClick: (date2) => onDateClick(date2, {})
};
if (dayRenderer) {
return /* @__PURE__ */ jsx3(Fragment, { children: dayRenderer(dayProps) });
}
if (isOutOfMonth && !showOtherMonths) {
return /* @__PURE__ */ jsx3("div", { className: "rcp-calendar-day rcp-calendar-day--empty" });
}
return /* @__PURE__ */ jsxs3(
"div",
{
className: clsx3(
"rcp-calendar-day",
{
"rcp-calendar-day--today": isCurrentDay,
"rcp-calendar-day--selected": isSelected,
"rcp-calendar-day--disabled": isDisabled,
"rcp-calendar-day--out-of-month": isOutOfMonth,
"rcp-calendar-day--has-events": dayEvents.length > 0
},
className
),
onClick: handleClick,
role: "button",
tabIndex: isDisabled ? -1 : 0,
"aria-label": `${date.getDate()} ${date.toLocaleDateString()}`,
"aria-selected": isSelected,
"aria-disabled": isDisabled,
onKeyDown: (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleClick(e);
}
},
children: [
/* @__PURE__ */ jsx3("span", { className: "rcp-calendar-day__number", children: date.getDate() }),
dayEvents.length > 0 && /* @__PURE__ */ jsxs3("div", { className: "rcp-calendar-day__events", children: [
dayEvents.slice(0, 3).map((event, index) => /* @__PURE__ */ jsx3(
"div",
{
className: "rcp-calendar-day__event",
style: {
backgroundColor: event.color,
color: event.textColor
},
title: event.title,
children: event.title
},
event.id || index
)),
dayEvents.length > 3 && /* @__PURE__ */ jsxs3("div", { className: "rcp-calendar-day__event-more", children: [
"+",
dayEvents.length - 3,
" more"
] })
] })
]
}
);
};
// src/components/TimePicker.tsx
import { useState as useState2, useEffect as useEffect2 } from "react";
import { format as format3 } from "date-fns";
import { enUS as enUS3 } from "date-fns/locale";
import clsx4 from "clsx";
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
var TimePicker = ({
value,
onChange,
format: timeFormat = "24h",
minuteStep = 1,
disabled = false,
className
}) => {
const [hours, setHours] = useState2(value ? value.getHours() : 0);
const [minutes, setMinutes] = useState2(value ? value.getMinutes() : 0);
const [period, setPeriod] = useState2("AM");
useEffect2(() => {
if (value) {
const date = new Date(value);
setHours(date.getHours());
setMinutes(date.getMinutes());
setPeriod(date.getHours() >= 12 ? "PM" : "AM");
}
}, [value]);
const handleTimeChange = (newHours, newMinutes) => {
if (disabled) return;
const date = new Date(value || /* @__PURE__ */ new Date());
date.setHours(newHours, newMinutes, 0, 0);
onChange?.(date);
};
const handleHourChange = (newHours) => {
let adjustedHours = newHours;
if (timeFormat === "12h") {
if (period === "PM" && newHours !== 12) {
adjustedHours = newHours + 12;
} else if (period === "AM" && newHours === 12) {
adjustedHours = 0;
}
}
setHours(adjustedHours);
handleTimeChange(adjustedHours, minutes);
};
const handleMinuteChange = (newMinutes) => {
setMinutes(newMinutes);
handleTimeChange(hours, newMinutes);
};
const handlePeriodChange = (newPeriod) => {
setPeriod(newPeriod);
let adjustedHours = hours;
if (newPeriod === "PM" && hours < 12) {
adjustedHours = hours + 12;
} else if (newPeriod === "AM" && hours >= 12) {
adjustedHours = hours - 12;
}
setHours(adjustedHours);
handleTimeChange(adjustedHours, minutes);
};
const displayHours = timeFormat === "12h" ? hours === 0 ? 12 : hours > 12 ? hours - 12 : hours : hours;
const hourOptions = Array.from(
{ length: timeFormat === "12h" ? 12 : 24 },
(_, i) => timeFormat === "12h" ? i + 1 : i
);
const minuteOptions = Array.from(
{ length: Math.floor(60 / minuteStep) },
(_, i) => i * minuteStep
);
return /* @__PURE__ */ jsxs4("div", { className: clsx4("rcp-time-picker", className), children: [
/* @__PURE__ */ jsxs4("div", { className: "rcp-time-picker__container", children: [
/* @__PURE__ */ jsxs4("div", { className: "rcp-time-picker__field", children: [
/* @__PURE__ */ jsx4("label", { className: "rcp-time-picker__label", children: "Hour" }),
/* @__PURE__ */ jsx4(
"select",
{
value: displayHours,
onChange: (e) => handleHourChange(parseInt(e.target.value)),
disabled,
className: "rcp-time-picker__select",
children: hourOptions.map((hour) => /* @__PURE__ */ jsx4("option", { value: hour, children: hour.toString().padStart(2, "0") }, hour))
}
)
] }),
/* @__PURE__ */ jsx4("div", { className: "rcp-time-picker__separator", children: ":" }),
/* @__PURE__ */ jsxs4("div", { className: "rcp-time-picker__field", children: [
/* @__PURE__ */ jsx4("label", { className: "rcp-time-picker__label", children: "Minute" }),
/* @__PURE__ */ jsx4(
"select",
{
value: minutes,
onChange: (e) => handleMinuteChange(parseInt(e.target.value)),
disabled,
className: "rcp-time-picker__select",
children: minuteOptions.map((minute) => /* @__PURE__ */ jsx4("option", { value: minute, children: minute.toString().padStart(2, "0") }, minute))
}
)
] }),
timeFormat === "12h" && /* @__PURE__ */ jsxs4("div", { className: "rcp-time-picker__field", children: [
/* @__PURE__ */ jsx4("label", { className: "rcp-time-picker__label", children: "Period" }),
/* @__PURE__ */ jsxs4(
"select",
{
value: period,
onChange: (e) => handlePeriodChange(e.target.value),
disabled,
className: "rcp-time-picker__select",
children: [
/* @__PURE__ */ jsx4("option", { value: "AM", children: "AM" }),
/* @__PURE__ */ jsx4("option", { value: "PM", children: "PM" })
]
}
)
] })
] }),
/* @__PURE__ */ jsx4("div", { className: "rcp-time-picker__display", children: value && format3(value, timeFormat === "12h" ? "h:mm a" : "HH:mm", { locale: enUS3 }) })
] });
};
// src/components/Calendar.tsx
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
var Calendar = ({
value,
defaultValue,
onChange,
view: controlledView,
defaultView = "month",
onViewChange,
selectionMode = "single",
showTimePicker = false,
timeFormat = "24h",
minuteStep = 1,
locale = DEFAULT_LOCALE,
className,
theme,
onDateClick,
onRangeSelect,
onMonthChange,
onWeekChange,
onDayChange,
dayRenderer,
headerRenderer,
minDate,
maxDate,
disabledDates,
disabledDaysOfWeek,
disableWeekends,
events = [],
responsive = true,
showWeekNumbers = false,
showOtherMonths = true
}) => {
const [currentView, setCurrentView] = useState3(controlledView || defaultView);
const [currentDate, setCurrentDate] = useState3(/* @__PURE__ */ new Date());
const [selectedValue, setSelectedValue] = useState3(value || defaultValue || null);
const [rangeStart, setRangeStart] = useState3(null);
const [hoveredDate, setHoveredDate] = useState3(null);
useEffect3(() => {
if (controlledView !== void 0) {
setCurrentView(controlledView);
}
}, [controlledView]);
useEffect3(() => {
if (value !== void 0) {
setSelectedValue(value);
}
}, [value]);
const displayDays = useMemo(() => {
switch (currentView) {
case "month":
return getCalendarDays(currentDate, locale);
case "week":
return getWeekDays(currentDate, locale);
case "day":
return [currentDate];
default:
return getCalendarDays(currentDate, locale);
}
}, [currentDate, currentView, locale]);
const weekHeaders = useMemo(() => {
const startOfCurrentWeek = startOfWeek2(/* @__PURE__ */ new Date(), { weekStartsOn: locale.weekStartsOn });
return getWeekDays(startOfCurrentWeek, locale);
}, [locale]);
const handleViewChange = (newView) => {
setCurrentView(newView);
onViewChange?.(newView);
switch (newView) {
case "month":
onMonthChange?.(currentDate);
break;
case "week":
onWeekChange?.(startOfWeek2(currentDate, { weekStartsOn: locale.weekStartsOn }));
break;
case "day":
onDayChange?.(currentDate);
break;
}
};
const handlePrevious = () => {
const newDate = navigateDate(currentDate, "previous", currentView);
setCurrentDate(newDate);
switch (currentView) {
case "month":
onMonthChange?.(newDate);
break;
case "week":
onWeekChange?.(startOfWeek2(newDate, { weekStartsOn: locale.weekStartsOn }));
break;
case "day":
onDayChange?.(newDate);
break;
}
};
const handleNext = () => {
const newDate = navigateDate(currentDate, "next", currentView);
setCurrentDate(newDate);
switch (currentView) {
case "month":
onMonthChange?.(newDate);
break;
case "week":
onWeekChange?.(startOfWeek2(newDate, { weekStartsOn: locale.weekStartsOn }));
break;
case "day":
onDayChange?.(newDate);
break;
}
};
const handleDateChange = (newDate) => {
setCurrentDate(newDate);
switch (currentView) {
case "month":
onMonthChange?.(newDate);
break;
case "week":
onWeekChange?.(startOfWeek2(newDate, { weekStartsOn: locale.weekStartsOn }));
break;
case "day":
onDayChange?.(newDate);
break;
}
};
const handleDateClick = (date, event) => {
onDateClick?.(date, event);
let newValue = null;
switch (selectionMode) {
case "single":
newValue = date;
if (showTimePicker && selectedValue instanceof Date) {
newValue = combineDateTime(date, selectedValue);
}
break;
case "multiple":
if (Array.isArray(selectedValue)) {
const exists = selectedValue.find((d) => isSameDay2(d, date));
if (exists) {
newValue = selectedValue.filter((d) => !isSameDay2(d, date));
} else {
newValue = [...selectedValue, date];
}
} else {
newValue = [date];
}
break;
case "range":
if (!rangeStart || rangeStart && hoveredDate) {
setRangeStart(date);
setHoveredDate(null);
newValue = { start: date, end: null };
} else {
const start = rangeStart < date ? rangeStart : date;
const end = rangeStart < date ? date : rangeStart;
newValue = { start, end };
setRangeStart(null);
onRangeSelect?.({ start, end });
}
break;
}
setSelectedValue(newValue);
onChange?.(newValue);
};
const handleTimeChange = (time) => {
if (selectedValue instanceof Date) {
const newValue = combineDateTime(selectedValue, time);
setSelectedValue(newValue);
onChange?.(newValue);
}
};
const isInRange = (date) => {
if (selectionMode !== "range") return false;
if (selectedValue && typeof selectedValue === "object" && "start" in selectedValue) {
return isDateInRange(date, selectedValue);
}
if (rangeStart && hoveredDate) {
const start = rangeStart < hoveredDate ? rangeStart : hoveredDate;
const end = rangeStart < hoveredDate ? hoveredDate : rangeStart;
return isDateInRange(date, { start, end });
}
return false;
};
const renderWeekNumber = (date) => {
if (!showWeekNumbers) return null;
return /* @__PURE__ */ jsx5("div", { className: "rcp-calendar__week-number", children: getWeekNumber(date) });
};
const renderCalendarGrid = () => {
const weeks = [];
let currentWeek = [];
displayDays.forEach((day, index) => {
currentWeek.push(day);
if (currentWeek.length === 7 || index === displayDays.length - 1) {
weeks.push([...currentWeek]);
currentWeek = [];
}
});
return weeks.map((week, weekIndex) => /* @__PURE__ */ jsxs5("div", { className: "rcp-calendar__week", children: [
showWeekNumbers && renderWeekNumber(week[0]),
week.map((day) => /* @__PURE__ */ jsx5(
CalendarDay,
{
date: day,
currentMonth: currentDate,
selectedValue: Array.isArray(selectedValue) ? selectedValue : selectedValue instanceof Date ? selectedValue : null,
events,
onDateClick: handleDateClick,
dayRenderer,
minDate,
maxDate,
disabledDates,
disabledDaysOfWeek,
disableWeekends,
showOtherMonths,
className: clsx5(
isInRange(day) && "rcp-calendar-day--in-range"
)
},
day.toISOString()
))
] }, weekIndex));
};
const themeStyles = theme ? {
"--rcp-primary": theme.primary,
"--rcp-secondary": theme.secondary,
"--rcp-background": theme.background,
"--rcp-text": theme.text,
"--rcp-text-secondary": theme.textSecondary,
"--rcp-border": theme.border,
"--rcp-hover": theme.hover,
"--rcp-selected": theme.selected,
"--rcp-disabled": theme.disabled,
"--rcp-today": theme.today
} : {};
return /* @__PURE__ */ jsxs5(
"div",
{
className: clsx5(
"rcp-calendar",
`rcp-calendar--${currentView}`,
responsive && "rcp-calendar--responsive",
showTimePicker && "rcp-calendar--with-time",
className
),
style: themeStyles,
children: [
headerRenderer ? headerRenderer({
date: currentDate,
view: currentView,
onPrevious: handlePrevious,
onNext: handleNext,
onViewChange: handleViewChange
}) : /* @__PURE__ */ jsx5(
CalendarHeader,
{
currentDate,
view: currentView,
onPrevious: handlePrevious,
onNext: handleNext,
onViewChange: handleViewChange,
onDateChange: handleDateChange,
minDate,
maxDate
}
),
/* @__PURE__ */ jsxs5("div", { className: "rcp-calendar__body", children: [
currentView !== "day" && /* @__PURE__ */ jsxs5("div", { className: "rcp-calendar__weekdays", children: [
showWeekNumbers && /* @__PURE__ */ jsx5("div", { className: "rcp-calendar__week-number-header", children: "#" }),
weekHeaders.map((day) => /* @__PURE__ */ jsx5("div", { className: "rcp-calendar__weekday", children: format4(day, "EEE", { locale: enUS4 }) }, day.toISOString()))
] }),
/* @__PURE__ */ jsx5(
"div",
{
className: "rcp-calendar__grid",
onMouseLeave: () => setHoveredDate(null),
children: renderCalendarGrid()
}
)
] }),
showTimePicker && selectedValue instanceof Date && /* @__PURE__ */ jsx5("div", { className: "rcp-calendar__time-picker", children: /* @__PURE__ */ jsx5(
TimePicker,
{
value: selectedValue,
onChange: handleTimeChange,
format: timeFormat,
minuteStep
}
) })
]
}
);
};
// src/components/CalendarInput.tsx
import { useState as useState4, useRef as useRef2, useEffect as useEffect4 } from "react";
import { format as format5 } from "date-fns";
import clsx6 from "clsx";
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
var CalendarInput = ({
value,
defaultValue,
onChange,
placeholder = "Select date...",
inputClassName,
calendarClassName,
showInput = true,
inputFormat = "MM/dd/yyyy",
disabled = false,
readOnly = false,
clearable = false,
onClear,
dropdownPosition = "auto",
dropdownOffset = 4,
closeOnSelect = true,
closeOnOutsideClick = true,
onOpen,
onClose,
name,
id,
autoComplete = "off",
required = false,
selectionMode = "single",
showTimePicker = false,
...calendarProps
}) => {
const [isOpen, setIsOpen] = useState4(false);
const [inputValue, setInputValue] = useState4("");
const [dropdownStyle, setDropdownStyle] = useState4({});
const inputRef = useRef2(null);
const calendarRef = useRef2(null);
const containerRef = useRef2(null);
const formatValue = (val) => {
if (!val) return "";
if (val instanceof Date) {
return format5(val, inputFormat);
}
if (Array.isArray(val)) {
return val.map((d) => format5(d, inputFormat)).join(", ");
}
const start = val.start ? format5(val.start, inputFormat) : "";
const end = val.end ? format5(val.end, inputFormat) : "";
return start && end ? `${start} - ${end}` : start || end;
};
useEffect4(() => {
setInputValue(formatValue(value ?? defaultValue ?? null));
}, [value, defaultValue, inputFormat]);
const handleInputChange = (e) => {
if (readOnly) return;
setInputValue(e.target.value);
};
const handleInputClick = () => {
if (disabled || readOnly) return;
if (!isOpen) {
setIsOpen(true);
onOpen?.();
}
};
const handleCalendarChange = (newValue) => {
onChange?.(newValue);
setInputValue(formatValue(newValue));
if (closeOnSelect && selectionMode === "single" && !showTimePicker) {
setIsOpen(false);
onClose?.();
}
};
const handleClear = (e) => {
e.stopPropagation();
onChange?.(null);
setInputValue("");
onClear?.();
};
useEffect4(() => {
if (isOpen && inputRef.current) {
const inputRect = inputRef.current.getBoundingClientRect();
const viewportHeight = window.innerHeight;
let top = inputRect.bottom + dropdownOffset;
let position = "bottom";
if (dropdownPosition === "auto") {
const spaceBelow = viewportHeight - inputRect.bottom;
const estimatedCalendarHeight = 400;
if (spaceBelow < estimatedCalendarHeight && inputRect.top > estimatedCalendarHeight) {
position = "top";
}
} else if (dropdownPosition === "top") {
position = "top";
}
setDropdownStyle({
position: "fixed",
top: position === "top" ? "auto" : top,
bottom: position === "top" ? viewportHeight - inputRect.top + dropdownOffset : "auto",
left: inputRect.left,
zIndex: 1e3,
minWidth: inputRect.width
});
}
}, [isOpen, dropdownPosition, dropdownOffset]);
useEffect4(() => {
const handleClickOutside = (event) => {
if (closeOnOutsideClick && isOpen && containerRef.current && !containerRef.current.contains(event.target)) {
setIsOpen(false);
onClose?.();
}
};
if (isOpen) {
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}
}, [isOpen, closeOnOutsideClick, onClose]);
useEffect4(() => {
const handleEscape = (event) => {
if (event.key === "Escape" && isOpen) {
setIsOpen(false);
onClose?.();
}
};
if (isOpen) {
document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
}
}, [isOpen, onClose]);
useEffect4(() => {
let timeoutId = null;
const handleScroll = () => {
if (isOpen && inputRef.current) {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
const inputRect = inputRef.current.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
if (inputRect.bottom < 0 || inputRect.top > viewportHeight || inputRect.right < 0 || inputRect.left > viewportWidth) {
setIsOpen(false);
onClose?.();
return;
}
let top = inputRect.bottom + dropdownOffset;
let position = "bottom";
if (dropdownPosition === "auto") {
const spaceBelow = viewportHeight - inputRect.bottom;
const estimatedCalendarHeight = 400;
if (spaceBelow < estimatedCalendarHeight && inputRect.top > estimatedCalendarHeight) {
position = "top";
}
} else if (dropdownPosition === "top") {
position = "top";
}
setDropdownStyle({
position: "fixed",
top: position === "top" ? "auto" : top,
bottom: position === "top" ? viewportHeight - inputRect.top + dropdownOffset : "auto",
left: inputRect.left,
zIndex: 1e3,
minWidth: inputRect.width
});
}, 16);
}
};
if (isOpen) {
window.addEventListener("scroll", handleScroll, true);
window.addEventListener("resize", handleScroll);
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
window.removeEventListener("scroll", handleScroll, true);
window.removeEventListener("resize", handleScroll);
};
}
}, [isOpen, dropdownPosition, dropdownOffset]);
return /* @__PURE__ */ jsxs6("div", { ref: containerRef, className: "rcp-calendar-input", children: [
showInput && /* @__PURE__ */ jsxs6("div", { className: "rcp-calendar-input__wrapper", children: [
/* @__PURE__ */ jsx6(
"input",
{
ref: inputRef,
type: "text",
value: inputValue,
onChange: handleInputChange,
onClick: handleInputClick,
placeholder,
disabled,
readOnly,
name,
id,
autoComplete,
required,
className: clsx6(
"rcp-calendar-input__field",
disabled && "rcp-calendar-input__field--disabled",
readOnly && "rcp-calendar-input__field--readonly",
isOpen && "rcp-calendar-input__field--open",
inputClassName
)
}
),
clearable && inputValue && !disabled && !readOnly && /* @__PURE__ */ jsx6(
"button",
{
type: "button",
onClick: handleClear,
className: "rcp-calendar-input__clear",
"aria-label": "Clear date",
children: "\xD7"
}
),
/* @__PURE__ */ jsx6(
"button",
{
type: "button",
onClick: handleInputClick,
disabled,
className: clsx6(
"rcp-calendar-input__toggle",
disabled && "rcp-calendar-input__toggle--disabled",
isOpen && "rcp-calendar-input__toggle--open"
),
"aria-label": "Open calendar",
children: "\u{1F4C5}"
}
)
] }),
isOpen && /* @__PURE__ */ jsxs6(
"div",
{
ref: calendarRef,
className: clsx6(
"rcp-calendar-input__dropdown",
calendarClassName
),
style: dropdownStyle,
children: [
/* @__PURE__ */ jsx6(
Calendar,
{
value: value || defaultValue,
onChange: handleCalendarChange,
selectionMode,
showTimePicker,
...calendarProps
}
),
showTimePicker && /* @__PURE__ */ jsx6("div", { style: {
padding: "12px 16px",
borderTop: "1px solid var(--rcp-border)",
backgroundColor: "var(--rcp-hover)",
display: "flex",
justifyContent: "flex-end",
gap: "8px"
}, children: /* @__PURE__ */ jsx6(
"button",
{
type: "button",
onClick: () => {
setIsOpen(false);
onClose?.();
},
style: {
padding: "8px 16px",
backgroundColor: "var(--rcp-primary)",
color: "white",
border: "none",
borderRadius: "4px",
cursor: "pointer",
fontSize: "14px",
fontWeight: "500"
},
children: "Done"
}
) })
]
}
)
] });
};
// src/index.ts
import "./calendar-5YM4I3IC.css";
export {
Calendar,
CalendarDay,
CalendarHeader,
CalendarInput,
DEFAULT_LOCALE,
TimePicker,
YearMonthSelector,
combineDateTime,
formatDate,
formatDateForDisplay,
getCalendarDays,
getMonthNames,
getViewTitle,
getWeekDays,
getWeekNumber,
getYearRange,
isDateDisabled,
isDateInRange,
isDateSelected,
navigateDate,
navigateToMonth,
navigateToYear,
navigateToYearMonth,
parseDateTime
};