{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "date-picker", "type": "registry:ui", "title": "Date Picker", "description": "A date picker component that can be used to select dates.", "dependencies": [ "date-fns", "react-day-picker" ], "registryDependencies": [ "@tra-kit/button", "@tra-kit/calendar", "@tra-kit/label", "@tra-kit/popover" ], "files": [ { "path": "registry/tra-kit/components/date-picker.tsx", "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\r\n/* eslint-disable react/jsx-no-useless-fragment */\r\n/* eslint-disable jsx-a11y/click-events-have-key-events */\r\n/* eslint-disable jsx-a11y/no-static-element-interactions */\r\n/* eslint-disable no-nested-ternary */\r\nimport { format } from 'date-fns';\r\nimport { CalendarIcon, XCircleIcon } from '@phosphor-icons/react';\r\n\r\nimport { cn } from '@/lib/utils';\r\nimport Button from './button';\r\nimport Calendar from './calendar';\r\nimport { Popover, PopoverContent, PopoverTrigger } from './popover';\r\nimport { type DateRange, type Matcher } from 'react-day-picker';\r\nimport { useEffect, useRef, useState } from 'react';\r\n// import { useIsMobile } from '@/hooks/use-mobile';\r\nimport { tr, type Locale } from 'date-fns/locale';\r\nimport Label from './label';\r\n\r\ntype Segment = 'day' | 'month' | 'year';\r\n\r\n/** locale'e göre segment placeholder etiketleri döndürür */\r\nconst getSegmentLabels = (locale: Locale) => {\r\n const code = (locale as any).code ?? '';\r\n if (code === 'tr') return { day: 'GG', month: 'AA', year: 'YYYY' };\r\n return { day: 'DD', month: 'MM', year: 'YYYY' };\r\n};\r\n\r\ninterface IDatePicker {\r\n id?: string;\r\n label?: string;\r\n labelClassName?: string;\r\n showRequiredIcon?: boolean;\r\n tooltip?: string | string[];\r\n mode?: 'single' | 'range' | 'multiple';\r\n locale?: Locale;\r\n value?: Date | DateRange | undefined;\r\n onChange?: (e: any) => void;\r\n disabled?: boolean;\r\n error?: boolean;\r\n showCompleteButton?: boolean;\r\n showClearButton?: boolean;\r\n minDate?: Date | undefined;\r\n maxDate?: Date | undefined;\r\n onBlur?: () => void;\r\n}\r\n\r\nconst DatePicker = ({\r\n id,\r\n label,\r\n labelClassName,\r\n showRequiredIcon,\r\n tooltip,\r\n mode = 'single',\r\n locale = tr,\r\n value,\r\n onChange,\r\n disabled,\r\n showCompleteButton,\r\n showClearButton,\r\n error,\r\n minDate,\r\n maxDate,\r\n onBlur,\r\n}: IDatePicker) => {\r\n const triggerRef = useRef(null);\r\n const [triggerWidth, setTriggerWidth] = useState();\r\n const [open, setOpen] = useState(false);\r\n\r\n const [day, _setDay] = useState('');\r\n const [month, _setMonth] = useState('');\r\n const [year, _setYear] = useState('');\r\n const [focused, setFocused] = useState(null);\r\n const dayRef = useRef(null);\r\n const monthRef = useRef(null);\r\n const yearRef = useRef(null);\r\n\r\n // Mirror state in refs so onBlur reads the latest value even when\r\n // a state update and a focus transition happen in the same synchronous call.\r\n const dayValRef = useRef('');\r\n const monthValRef = useRef('');\r\n const yearValRef = useRef('');\r\n\r\n const setDay = (v: string | ((p: string) => string)) => {\r\n const next = typeof v === 'function' ? v(dayValRef.current) : v;\r\n dayValRef.current = next;\r\n _setDay(next);\r\n };\r\n const setMonth = (v: string | ((p: string) => string)) => {\r\n const next = typeof v === 'function' ? v(monthValRef.current) : v;\r\n monthValRef.current = next;\r\n _setMonth(next);\r\n };\r\n const setYear = (v: string | ((p: string) => string)) => {\r\n const next = typeof v === 'function' ? v(yearValRef.current) : v;\r\n yearValRef.current = next;\r\n _setYear(next);\r\n };\r\n\r\n const containerRef = useRef(null);\r\n\r\n const handleContainerBlur = (e: React.FocusEvent) => {\r\n const relatedTarget = e.relatedTarget as Node | null;\r\n // Focus hâlâ bileşen içinde (segment'ler arası veya takvim ikonuna)\r\n if (containerRef.current?.contains(relatedTarget)) return;\r\n // Popover açıkken takvime tıklandı (portal → container dışında)\r\n if (open) return;\r\n onBlur?.();\r\n };\r\n\r\n // const isMobile = useIsMobile();\r\n\r\n // Sync external value -> segments\r\n useEffect(() => {\r\n if (mode === 'single' && value instanceof Date && !Number.isNaN(value.getTime())) {\r\n const newDay = format(value, 'dd');\r\n const newMonth = format(value, 'MM');\r\n const newYear = format(value, 'yyyy');\r\n if (newDay !== day || newMonth !== month || newYear !== year) {\r\n setDay(newDay);\r\n setMonth(newMonth);\r\n setYear(newYear);\r\n }\r\n }\r\n }, [value]);\r\n\r\n useEffect(() => {\r\n function updateWidth() {\r\n if (triggerRef.current) {\r\n setTriggerWidth(triggerRef.current.offsetWidth);\r\n }\r\n }\r\n updateWidth();\r\n window.addEventListener('resize', updateWidth);\r\n return () => window.removeEventListener('resize', updateWidth);\r\n }, []);\r\n\r\n const focusSegment = (seg: Segment) => {\r\n if (seg === 'day') dayRef.current?.focus();\r\n else if (seg === 'month') monthRef.current?.focus();\r\n else yearRef.current?.focus();\r\n };\r\n const nextSegment = (seg: Segment) => {\r\n if (seg === 'day') focusSegment('month');\r\n else if (seg === 'month') focusSegment('year');\r\n };\r\n const prevSegment = (seg: Segment) => {\r\n if (seg === 'month') focusSegment('day');\r\n else if (seg === 'year') focusSegment('month');\r\n };\r\n\r\n const handleKeyDown = (e: any, seg: Segment) => {\r\n const { key } = e;\r\n if (key === 'ArrowRight') {\r\n e.preventDefault();\r\n nextSegment(seg);\r\n return;\r\n }\r\n if (key === 'ArrowLeft') {\r\n e.preventDefault();\r\n prevSegment(seg);\r\n return;\r\n }\r\n if (key === 'Tab') return;\r\n if (key === 'Backspace') {\r\n e.preventDefault();\r\n if (seg === 'day') {\r\n if (day === '') prevSegment(seg);\r\n else setDay((d) => d.slice(0, -1));\r\n } else if (seg === 'month') {\r\n if (month === '') prevSegment(seg);\r\n else setMonth((m) => m.slice(0, -1));\r\n } else if (year === '') prevSegment(seg);\r\n else setYear((y) => y.slice(0, -1));\r\n return;\r\n }\r\n if (!/^\\d$/.test(key)) {\r\n e.preventDefault();\r\n return;\r\n }\r\n e.preventDefault();\r\n\r\n if (seg === 'day') {\r\n if (day.length === 2) {\r\n setDay(key);\r\n } else if (day.length === 0) {\r\n if (parseInt(key, 10) > 3) {\r\n setDay(`0${key}`);\r\n nextSegment('day');\r\n } else setDay(key);\r\n } else {\r\n const val = parseInt(day + key, 10);\r\n setDay(String(Math.min(Math.max(val, 1), 31)).padStart(2, '0'));\r\n nextSegment('day');\r\n }\r\n } else if (seg === 'month') {\r\n if (month.length === 2) {\r\n setMonth(key);\r\n } else if (month.length === 0) {\r\n if (parseInt(key, 10) > 1) {\r\n setMonth(`0${key}`);\r\n nextSegment('month');\r\n } else setMonth(key);\r\n } else {\r\n const val = parseInt(month + key, 10);\r\n setMonth(String(Math.min(Math.max(val, 1), 12)).padStart(2, '0'));\r\n nextSegment('month');\r\n }\r\n } else if (year.length < 4) {\r\n const next = yearValRef.current + key;\r\n if (next.length === 4) {\r\n const clamped = Math.min(Math.max(parseInt(next, 10), 1900), 2100);\r\n setYear(String(clamped));\r\n } else {\r\n setYear(next);\r\n }\r\n } else {\r\n setYear(key);\r\n }\r\n };\r\n\r\n const getSegmentDate = (): Date | undefined => {\r\n if (day.length !== 2 || month.length !== 2 || year.length !== 4) return undefined;\r\n const dd = parseInt(day, 10);\r\n const mm = parseInt(month, 10);\r\n const yyyy = parseInt(year, 10);\r\n const d = new Date(yyyy, mm - 1, dd);\r\n if (d.getFullYear() === yyyy && d.getMonth() === mm - 1 && d.getDate() === dd) return d;\r\n return undefined;\r\n };\r\n\r\n // Notify parent when segments form a valid date\r\n useEffect(() => {\r\n if (mode !== 'single') return;\r\n const d = getSegmentDate();\r\n if (d && onChange) {\r\n if (!(value instanceof Date) || value.getTime() !== d.getTime()) {\r\n onChange(d);\r\n }\r\n }\r\n }, [day, month, year]);\r\n\r\n const handleCalendarSelect = (selectedDate: Date | undefined) => {\r\n if (!selectedDate) return;\r\n setDay(format(selectedDate, 'dd'));\r\n setMonth(format(selectedDate, 'MM'));\r\n setYear(format(selectedDate, 'yyyy'));\r\n onChange?.(selectedDate);\r\n if (!showCompleteButton) setOpen(false);\r\n };\r\n\r\n const segmentDate = getSegmentDate();\r\n const isSegmentedMode = mode === 'single';\r\n const segmentLabels = getSegmentLabels(locale);\r\n\r\n const dayDisplay = day !== '' ? day : focused === 'day' ? segmentLabels.day : '';\r\n const monthDisplay = month !== '' ? month : focused === 'month' ? segmentLabels.month : '';\r\n const yearDisplay = year !== '' ? year : focused === 'year' ? segmentLabels.year : '';\r\n\r\n return (\r\n \r\n {label && (\r\n \r\n {label}\r\n \r\n )}\r\n {\r\n setOpen(isOpen);\r\n if (!isOpen) {\r\n // Popover kapandıktan sonra focus bileşen dışına çıktıysa blur'u tetikle\r\n setTimeout(() => {\r\n if (!containerRef.current?.contains(document.activeElement)) {\r\n onBlur?.();\r\n }\r\n }, 0);\r\n }\r\n }}\r\n >\r\n {isSegmentedMode ? (\r\n /* ── Segmented Input Trigger ── */\r\n \r\n !focused && !disabled && focusSegment('day')}\r\n >\r\n setFocused('day')}\r\n onBlur={() => {\r\n if (dayValRef.current === '0') setDay('01');\r\n else if (dayValRef.current.length === 1) setDay((d) => d.padStart(2, '0'));\r\n setFocused(null);\r\n }}\r\n onKeyDown={(e) => handleKeyDown(e, 'day')}\r\n />\r\n /\r\n setFocused('month')}\r\n onBlur={() => {\r\n if (monthValRef.current === '0') setMonth('01');\r\n else if (monthValRef.current.length === 1) setMonth((m) => m.padStart(2, '0'));\r\n setFocused(null);\r\n }}\r\n onKeyDown={(e) => handleKeyDown(e, 'month')}\r\n />\r\n /\r\n setFocused('year')}\r\n onBlur={() => setFocused(null)}\r\n onKeyDown={(e) => handleKeyDown(e, 'year')}\r\n />\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ) : (\r\n /* ── Button Trigger (range / showMonthYearPicker) ── */\r\n \r\n \r\n {mode === 'range' && (\r\n <>\r\n {value && 'from' in value && value.from ? (\r\n value.to ? (\r\n <>\r\n {format(value.from, 'd MMMM yyyy', { locale })} -{' '}\r\n {format(value.to, 'd MMMM yyyy', { locale })}\r\n \r\n ) : (\r\n format(value.from, 'd MMMM yyyy', { locale })\r\n )\r\n ) : (\r\n Bir tarih seçiniz\r\n )}\r\n \r\n )}\r\n \r\n \r\n \r\n )}\r\n\r\n \r\n {mode === 'range' ? (\r\n {\r\n if (onChange) onChange(e);\r\n }}\r\n // numberOfMonths={isMobile ? 1 : 2}\r\n disabled={\r\n minDate || maxDate\r\n ? ([\r\n minDate ? { before: minDate } : undefined,\r\n maxDate ? { after: maxDate } : undefined,\r\n ].filter(Boolean) as Matcher[])\r\n : undefined\r\n }\r\n />\r\n ) : (\r\n {\r\n if (isSegmentedMode) {\r\n handleCalendarSelect(e);\r\n return;\r\n }\r\n if (onChange) onChange(e);\r\n }}\r\n onMonthChange={(e) => {\r\n if (onChange) onChange(e);\r\n }}\r\n numberOfMonths={1}\r\n disabled={\r\n minDate || maxDate\r\n ? ([\r\n minDate ? { before: minDate } : undefined,\r\n maxDate ? { after: maxDate } : undefined,\r\n ].filter(Boolean) as Matcher[])\r\n : undefined\r\n }\r\n />\r\n )}\r\n {showCompleteButton && (\r\n
\r\n setOpen(false)}\r\n >\r\n Seçimi Tamamla\r\n \r\n
\r\n )}\r\n {showClearButton && value && (\r\n
\r\n {\r\n if (isSegmentedMode) {\r\n setDay('');\r\n setMonth('');\r\n setYear('');\r\n }\r\n onChange?.(mode === 'single' ? undefined : { from: undefined, to: undefined });\r\n }}\r\n >\r\n \r\n Temizle\r\n \r\n
\r\n )}\r\n \r\n \r\n \r\n );\r\n};\r\n\r\nexport default DatePicker;\r\n", "type": "registry:ui" } ] }