import { useEffect, useId, useMemo, useRef, useState, type ReactNode, } from "react"; import { cx } from "./utils"; import { Icon } from "./Icon"; import { Button } from "./Button"; const MONTHS = [ "janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre", ]; const WEEKDAYS = ["lun", "mar", "mer", "jeu", "ven", "sam", "dim"]; const fmt = (d: Date | null) => d ? d.toLocaleDateString("fr-FR", { day: "2-digit", month: "2-digit", year: "numeric", }) : ""; const sameDay = (a: Date, b: Date) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); function buildMonthGrid(year: number, month: number) { const first = new Date(year, month, 1); // En France lundi = jour 0 const startDay = (first.getDay() + 6) % 7; const daysInMonth = new Date(year, month + 1, 0).getDate(); const daysInPrev = new Date(year, month, 0).getDate(); const cells: { date: Date; outside: boolean }[] = []; // Préfixe (mois précédent) for (let i = startDay - 1; i >= 0; i--) { cells.push({ date: new Date(year, month - 1, daysInPrev - i), outside: true }); } for (let d = 1; d <= daysInMonth; d++) { cells.push({ date: new Date(year, month, d), outside: false }); } // Suffixe (mois suivant) while (cells.length < 42) { const idx = cells.length - (startDay + daysInMonth); cells.push({ date: new Date(year, month + 1, idx + 1), outside: true }); } return cells; } export type DatePickerProps = { label?: ReactNode; hint?: ReactNode; error?: ReactNode; value?: Date | null; onChange?: (date: Date | null) => void; required?: boolean; disabled?: boolean; /** Désactive certaines dates */ isDisabled?: (date: Date) => boolean; placeholder?: string; className?: string; }; export function DatePicker({ label, hint, error, value, onChange, required, disabled, isDisabled, placeholder = "JJ/MM/AAAA", className, }: DatePickerProps) { const id = useId(); const [open, setOpen] = useState(false); const [view, setView] = useState(() => value ?? new Date()); const ref = useRef(null); useEffect(() => { if (value) setView(value); }, [value]); useEffect(() => { if (!open) return; const onClick = (e: MouseEvent) => { if (!ref.current?.contains(e.target as Node)) setOpen(false); }; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); }; document.addEventListener("mousedown", onClick); document.addEventListener("keydown", onKey); return () => { document.removeEventListener("mousedown", onClick); document.removeEventListener("keydown", onKey); }; }, [open]); const cells = useMemo( () => buildMonthGrid(view.getFullYear(), view.getMonth()), [view], ); const today = new Date(); return (
{label && ( )}
!disabled && setOpen((v) => !v)} onFocus={() => !disabled && setOpen(true)} aria-haspopup="dialog" aria-expanded={open} />
{error ? ( {error} ) : hint ? ( {hint} ) : null} {open && (
{MONTHS[view.getMonth()]} {view.getFullYear()}
{WEEKDAYS.map((w) => (
{w}
))}
{cells.map(({ date, outside }, i) => { const selected = value && sameDay(date, value); const todayMatch = sameDay(date, today); const dis = isDisabled?.(date) ?? false; return ( ); })}
)}
); }