import { useEffect, useId, useRef, useState, type InputHTMLAttributes, type KeyboardEvent as ReactKeyboardEvent } from "react"; import { cn } from "../../lib/cn"; import { FloatingPortal } from "./floating-portal"; import { buildCalendarDays, CALENDAR_WEEKDAYS, compareDates, emitInputChange, fieldClassName, fieldNote, formatDateLabel, formatDateValue, MONTH_LABELS, parseDateValue, PICKER_ITEM_RADIUS, PICKER_PANEL_RADIUS, resolvePopupPlacement, sameDay, shiftMonth, startOfMonth, type FieldMeta, type PopupPlacement, } from "./shared"; export type DatePickerProps = InputHTMLAttributes & FieldMeta; export function DatePicker({ className, defaultValue, disabled, error, hint, id, label, max, min, name, onChange, placeholder, required, value, }: DatePickerProps) { const generatedId = useId(); const fieldId = id ?? generatedId; const hintId = hint ? `${fieldId}-hint` : undefined; const errorId = error ? `${fieldId}-error` : undefined; const panelRef = useRef(null); const buttonRef = useRef(null); const calendarRef = useRef(null); const calendarOverlayRef = useRef(null); const controlledValue = typeof value === "string" ? value : undefined; const [internalValue, setInternalValue] = useState( typeof defaultValue === "string" ? defaultValue : "", ); const selectedValue = controlledValue ?? internalValue; const selectedDate = parseDateValue(selectedValue); const minDate = parseDateValue(min); const maxDate = parseDateValue(max); const [open, setOpen] = useState(false); const [, setPlacement] = useState({ horizontal: "left", vertical: "bottom", }); const [pickerView, setPickerView] = useState<"days" | "months" | "years">("days"); const [visibleMonth, setVisibleMonth] = useState(() => startOfMonth(selectedDate ?? new Date()), ); const [activeDate, setActiveDate] = useState(selectedDate ?? new Date()); const yearWindowStart = Math.floor(visibleMonth.getFullYear() / 12) * 12; const yearOptions = Array.from({ length: 12 }, (_, index) => yearWindowStart + index); useEffect(() => { if (selectedDate) { setVisibleMonth(startOfMonth(selectedDate)); setActiveDate(selectedDate); } }, [selectedValue]); useEffect(() => { if (open) { calendarRef.current?.focus(); requestAnimationFrame(() => { if (!buttonRef.current || !calendarOverlayRef.current) { return; } setPlacement( resolvePopupPlacement( buttonRef.current.getBoundingClientRect(), calendarOverlayRef.current.getBoundingClientRect(), ), ); }); } }, [open]); useEffect(() => { function handlePointerDown(event: PointerEvent) { if (!panelRef.current?.contains(event.target as Node)) { setOpen(false); } } function handleEscape(event: KeyboardEvent) { if (event.key === "Escape") { setOpen(false); } } window.addEventListener("pointerdown", handlePointerDown); window.addEventListener("keydown", handleEscape); return () => { window.removeEventListener("pointerdown", handlePointerDown); window.removeEventListener("keydown", handleEscape); }; }, []); function isDisabledDate(date: Date) { if (minDate && compareDates(date, minDate) < 0) { return true; } if (maxDate && compareDates(date, maxDate) > 0) { return true; } return false; } function emitChange(nextValue: string) { if (controlledValue === undefined) { setInternalValue(nextValue); } emitInputChange(onChange, nextValue, name); } function handleSelect(date: Date) { if (isDisabledDate(date)) { return; } const nextValue = formatDateValue(date); emitChange(nextValue); setActiveDate(date); setVisibleMonth(startOfMonth(date)); setPickerView("days"); setOpen(false); buttonRef.current?.focus(); } function handleClear() { emitChange(""); setActiveDate(new Date()); setOpen(false); buttonRef.current?.focus(); } function moveActiveDate(step: number) { const base = activeDate ?? selectedDate ?? new Date(); const nextDate = new Date(base); nextDate.setDate(base.getDate() + step); setActiveDate(nextDate); setVisibleMonth(startOfMonth(nextDate)); } function handleKeyDown(event: ReactKeyboardEvent) { if (!open && (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Enter" || event.key === " ")) { event.preventDefault(); setOpen(true); setActiveDate(selectedDate ?? new Date()); setPickerView("days"); return; } if (!open) { return; } if (event.key === "ArrowRight") { event.preventDefault(); if (pickerView === "months") { setVisibleMonth((current) => shiftMonth(current, 1)); return; } if (pickerView === "years") { setVisibleMonth((current) => shiftMonth(current, 12)); return; } moveActiveDate(1); return; } if (event.key === "ArrowLeft") { event.preventDefault(); if (pickerView === "months") { setVisibleMonth((current) => shiftMonth(current, -1)); return; } if (pickerView === "years") { setVisibleMonth((current) => shiftMonth(current, -12)); return; } moveActiveDate(-1); return; } if (event.key === "ArrowDown") { event.preventDefault(); if (pickerView === "months") { setVisibleMonth((current) => shiftMonth(current, 3)); return; } if (pickerView === "years") { setVisibleMonth((current) => shiftMonth(current, 12 * 3)); return; } moveActiveDate(7); return; } if (event.key === "ArrowUp") { event.preventDefault(); if (pickerView === "months") { setVisibleMonth((current) => shiftMonth(current, -3)); return; } if (pickerView === "years") { setVisibleMonth((current) => shiftMonth(current, -(12 * 3))); return; } moveActiveDate(-7); return; } if (event.key === "PageDown") { event.preventDefault(); if (pickerView === "years") { setVisibleMonth((current) => shiftMonth(current, 12 * 12)); } else { setVisibleMonth((current) => shiftMonth(current, event.shiftKey ? 12 : 1)); } return; } if (event.key === "PageUp") { event.preventDefault(); if (pickerView === "years") { setVisibleMonth((current) => shiftMonth(current, -12 * 12)); } else { setVisibleMonth((current) => shiftMonth(current, event.shiftKey ? -12 : -1)); } return; } if (event.key === "Home") { event.preventDefault(); if (pickerView === "months") { setVisibleMonth((current) => new Date(current.getFullYear(), 0, 1)); return; } if (pickerView === "years") { setVisibleMonth((current) => new Date(yearWindowStart, current.getMonth(), 1)); return; } const base = activeDate ?? selectedDate ?? new Date(); moveActiveDate(-((base.getDay() + 6) % 7)); return; } if (event.key === "End") { event.preventDefault(); if (pickerView === "months") { setVisibleMonth((current) => new Date(current.getFullYear(), 11, 1)); return; } if (pickerView === "years") { setVisibleMonth((current) => new Date(yearWindowStart + 11, current.getMonth(), 1)); return; } const base = activeDate ?? selectedDate ?? new Date(); moveActiveDate(6 - ((base.getDay() + 6) % 7)); return; } if (event.key === "Enter" || event.key === " ") { event.preventDefault(); if (pickerView === "months") { setPickerView("days"); return; } if (pickerView === "years") { setPickerView("months"); return; } if (activeDate) { handleSelect(activeDate); } } } const calendarDays = buildCalendarDays(visibleMonth); return ( ); }