import { useEffect, useMemo, useRef, useState } from "react"; import { buildMonthCells, toYmd, WEEKDAY_LABELS_SHORT_RU } from "../lib"; const MONTH_NAMES_RU = [ "Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь", ]; interface DatePickerFieldProps { value: string; name: string; required?: boolean; onChange: (value: string) => void; } function parseYmd(value: string): { year: number; monthIndex: number; day: number } | null { if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { return null; } const year = Number(value.slice(0, 4)); const monthIndex = Number(value.slice(5, 7)) - 1; const day = Number(value.slice(8, 10)); if (!Number.isInteger(year) || !Number.isInteger(monthIndex) || !Number.isInteger(day)) { return null; } if (monthIndex < 0 || monthIndex > 11) { return null; } return { year, monthIndex, day }; } function getInitialVisibleMonth(value: string): { year: number; monthIndex: number } { const parsed = parseYmd(value); if (parsed) { return { year: parsed.year, monthIndex: parsed.monthIndex }; } const now = new Date(); return { year: now.getFullYear(), monthIndex: now.getMonth() }; } export function DatePickerField(props: DatePickerFieldProps): JSX.Element { const { value, name, required, onChange } = props; const [isOpen, setIsOpen] = useState(false); const [visibleMonth, setVisibleMonth] = useState(() => getInitialVisibleMonth(value)); const rootRef = useRef(null); useEffect(() => { const parsed = parseYmd(value); if (!parsed) { return; } setVisibleMonth({ year: parsed.year, monthIndex: parsed.monthIndex }); }, [value]); useEffect(() => { if (!isOpen) { return; } function handlePointerDown(event: MouseEvent): void { if (rootRef.current?.contains(event.target as Node)) { return; } setIsOpen(false); } function handleKeyDown(event: KeyboardEvent): void { if (event.key === "Escape") { setIsOpen(false); } } document.addEventListener("mousedown", handlePointerDown); document.addEventListener("keydown", handleKeyDown); return () => { document.removeEventListener("mousedown", handlePointerDown); document.removeEventListener("keydown", handleKeyDown); }; }, [isOpen]); const selected = parseYmd(value); const todayYmd = toYmd(new Date().getFullYear(), new Date().getMonth(), new Date().getDate()); const cells = useMemo( () => buildMonthCells(visibleMonth.year, visibleMonth.monthIndex), [visibleMonth], ); const monthTitle = `${MONTH_NAMES_RU[visibleMonth.monthIndex]} ${visibleMonth.year}`; function shiftMonth(delta: number): void { setVisibleMonth((prev) => { const next = new Date(Date.UTC(prev.year, prev.monthIndex + delta, 1)); return { year: next.getUTCFullYear(), monthIndex: next.getUTCMonth() }; }); } return (
{ onChange(event.target.value); }} onFocus={() => setIsOpen(true)} placeholder="2026-05-03" autoComplete="off" required={required} />
{isOpen ? (

{monthTitle}

{WEEKDAY_LABELS_SHORT_RU.map((weekday) => ( {weekday} ))}
{cells.map((day, idx) => { if (day === null) { return ; } const ymd = toYmd(visibleMonth.year, visibleMonth.monthIndex, day); const isSelected = selected?.year === visibleMonth.year && selected.monthIndex === visibleMonth.monthIndex && selected.day === day; return ( ); })}
) : null}
); }