import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' import { defaultSingleStrings, defaultRangeStrings } from 'shared-types/datepicker' import { valueToArray } from 'shared-utils/value-utils' import { isIOS } from 'shared-utils/device-utils' import { newDate, filterSelectableDates } from 'shared-utils/date-utils' import { getDatepickerInputType, getDatepickerInputClasses, getDatepickerButtonClasses, getRangeLabelClasses, processDateSelection, handleDatepickerKeydown, validateRangeOrder, } from 'shared-utils/datepicker-utils' import type { IPktDatepicker, IDatepickerState } from './types' export function useDatepickerState(props: IPktDatepicker, ref: React.ForwardedRef): IDatepickerState { const { id, label, value, defaultValue, dateformat = 'dd.MM.yyyy', multiple = false, maxlength, range = false, showRangeLabels = false, weeknumbers = false, withcontrols = false, excludedates, excludeweekdays, currentmonth, today, calendarOpen: calendarOpenProp, timezone, fullwidth = false, hasFieldset = false, inline = false, helptext, helptextDropdown, helptextDropdownButton, hasError = false, errorMessage, optionalTag = false, optionalText, requiredTag = false, requiredText, tagText, useWrapper = true, strings: stringsProp, disabled = false, readOnly = false, required = false, name, min, max, placeholder, onChange, onValueChange, className, ...restProps } = props // Value management const normalizeValue = useCallback((val: string | string[] | undefined): string[] => { if (val === undefined) return [] return valueToArray(val) }, []) const isControlled = value !== undefined const initialDefault = typeof defaultValue === 'number' ? undefined : (defaultValue as string | string[] | undefined) const [internalValue, setInternalValue] = useState(() => normalizeValue(value ?? initialDefault)) const values = isControlled ? normalizeValue(value) : internalValue // Store initial default values for form reset const initialValuesRef = useRef(internalValue) useEffect(() => { if (isControlled) { setInternalValue(normalizeValue(value)) } }, [value, isControlled, normalizeValue]) // Calendar state const [calendarOpen, setCalendarOpen] = useState(calendarOpenProp ?? false) useEffect(() => { if (calendarOpenProp !== undefined) { setCalendarOpen(calendarOpenProp) } }, [calendarOpenProp]) // Refs const inputRef = useRef(null) const inputRefTo = useRef(null) const changeInputRef = useRef(null) const btnRef = useRef(null) const wrapperRef = useRef(null) // Derived state const inputType = useMemo(() => getDatepickerInputType(), []) const isIOSDevice = useMemo(() => isIOS(), []) const strings = useMemo( () => stringsProp ?? (range ? defaultRangeStrings : defaultSingleStrings), [stringsProp, range], ) const minStr = typeof min === 'string' ? min : min !== undefined ? String(min) : undefined const maxStr = typeof max === 'string' ? max : max !== undefined ? String(max) : undefined const inputClasses = useMemo(() => { const classMap = getDatepickerInputClasses(fullwidth, showRangeLabels, multiple, range, readOnly, inputType) return Object.entries(classMap) .filter(([, v]) => v) .map(([k]) => k) .join(' ') }, [fullwidth, showRangeLabels, multiple, range, readOnly, inputType]) const buttonClasses = useMemo(() => { const classMap = getDatepickerButtonClasses() return Object.entries(classMap) .filter(([, v]) => v) .map(([k]) => k) .join(' ') }, []) const rangeLabelClasses = useMemo(() => { const classMap = getRangeLabelClasses(showRangeLabels) return Object.entries(classMap) .filter(([, v]) => v) .map(([k]) => k) .join(' ') }, [showRangeLabels]) const datepickerInputsClasses = useMemo( () => ['pkt-datepicker__inputs', range && 'pkt-input__range-inputs'].filter(Boolean).join(' '), [range, showRangeLabels], ) const effectiveCurrentMonth = useMemo(() => { if (currentmonth) return currentmonth if (maxStr) return maxStr.slice(0, 7) return undefined }, [currentmonth, maxStr]) const isInputDisabled = disabled || (multiple && maxlength != null && values.length >= maxlength) const hasCounter = multiple && maxlength != null const inputId = `${id}-input` const formValue = values.join(',') // Sync hidden input value for form submission (input is uncontrolled) useEffect(() => { if (changeInputRef.current) { changeInputRef.current.value = formValue } }, [formValue]) // Timezone useEffect(() => { if (timezone && typeof window !== 'undefined' && timezone !== window.pktTz) { window.pktTz = timezone } }, [timezone]) // Value update helpers const updateValues = useCallback( (newValues: string[], notify = true) => { if (!isControlled) { setInternalValue(newValues) } if (notify) onValueChange?.(newValues) }, [isControlled, onValueChange], ) const nativeInputValueSetter = useMemo( () => Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set, [], ) const dispatchChange = useCallback( (newValues: string[]) => { const input = changeInputRef.current if (!input || !nativeInputValueSetter) return nativeInputValueSetter.call(input, newValues.join(',')) input.dispatchEvent(new Event('input', { bubbles: true })) }, [nativeInputValueSetter], ) // Calendar toggle const toggleCalendar = useCallback(() => { if (disabled) return setCalendarOpen((prev) => !prev) }, [disabled]) const hideCalendar = useCallback(() => { setCalendarOpen(false) }, []) // Outside click / Escape useEffect(() => { if (!calendarOpen) return const handleClick = (e: MouseEvent) => { const target = e.target as Node if ( wrapperRef.current && !wrapperRef.current.contains(target) && !(target as Element).closest?.('.pkt-calendar-popup') ) { hideCalendar() } } const handleKeydown = (e: KeyboardEvent) => { if (e.key === 'Escape') { hideCalendar() inputRef.current?.focus() } } document.addEventListener('click', handleClick, true) document.addEventListener('keydown', handleKeydown) return () => { document.removeEventListener('click', handleClick, true) document.removeEventListener('keydown', handleKeydown) } }, [calendarOpen, hideCalendar]) // iOS auto-open on focus const handleFocus = useCallback(() => { if (isIOSDevice && !calendarOpen) { setCalendarOpen(true) } }, [isIOSDevice, calendarOpen]) // Calendar date selection const handleDateSelected = useCallback( (selected: string[]) => { const processed = processDateSelection(selected, multiple, range) const newValues = valueToArray(processed) updateValues(newValues) dispatchChange(newValues) if (!multiple && !range) { hideCalendar() inputRef.current?.focus() } }, [multiple, range, updateValues, dispatchChange, hideCalendar], ) const handleCalendarClose = useCallback(() => { hideCalendar() inputRef.current?.focus() }, [hideCalendar]) // Single mode input change const handleSingleInputChange = useCallback( (e: React.ChangeEvent) => { const newValue = e.target.value const newValues = newValue ? [newValue] : [] updateValues(newValues) dispatchChange(newValues) }, [updateValues, dispatchChange], ) // Range mode input change const handleRangeFromChange = useCallback( (e: React.ChangeEvent) => { const fromVal = e.target.value const toVal = values[1] ?? '' const newValues = [fromVal, toVal].filter(Boolean) updateValues(newValues) dispatchChange(newValues) }, [values, updateValues, dispatchChange], ) const handleRangeToChange = useCallback( (e: React.ChangeEvent) => { const fromVal = values[0] ?? '' const toVal = e.target.value const newValues = [fromVal, toVal].filter(Boolean) updateValues(newValues) dispatchChange(newValues) }, [values, updateValues, dispatchChange], ) const handleRangeBlur = useCallback(() => { if (values.length === 2 && !validateRangeOrder(values)) { updateValues([values[0]]) dispatchChange([values[0]]) } }, [values, updateValues, dispatchChange]) // Multiple mode handlers const addDateToMultiple = useCallback( (dateStr: string) => { const date = newDate(dateStr) const minAsDate = minStr ? newDate(minStr) : null const maxAsDate = maxStr ? newDate(maxStr) : null if (date && !isNaN(date.getTime()) && (!minAsDate || date >= minAsDate) && (!maxAsDate || date <= maxAsDate)) { const filtered = filterSelectableDates([...values, dateStr], minStr, maxStr, excludedates, excludeweekdays) const unique = [...new Set(filtered)] updateValues(unique) dispatchChange(unique) } }, [values, minStr, maxStr, excludedates, excludeweekdays, updateValues, dispatchChange], ) const handleMultipleBlur = useCallback( (e: React.FocusEvent) => { const target = e.target if (!target.value) return addDateToMultiple(target.value.split(',')[0]) target.value = '' }, [addDateToMultiple], ) const handleMultipleComma = useCallback( (e: KeyboardEvent) => { const target = e.target as HTMLInputElement if (!target.value) return addDateToMultiple(target.value.split(',')[0]) target.value = '' }, [addDateToMultiple], ) const handleTagRemoved = useCallback( (date: string) => { const newValues = values.filter((d) => d !== date) updateValues(newValues) dispatchChange(newValues) }, [values, updateValues, dispatchChange], ) // Keyboard navigation const submitFormOrBlur = useCallback((inputEl: HTMLInputElement | null) => { const form = inputEl?.closest('form') if (form) { form.requestSubmit() } else { inputEl?.blur() } }, []) const handleSingleKeydown = useCallback( (e: React.KeyboardEvent) => { handleDatepickerKeydown( e.nativeEvent, () => toggleCalendar(), () => submitFormOrBlur(inputRef.current), undefined, () => inputRef.current?.blur(), ) }, [toggleCalendar, submitFormOrBlur], ) const handleRangeFromKeydown = useCallback( (e: React.KeyboardEvent) => { handleDatepickerKeydown( e.nativeEvent, () => toggleCalendar(), () => submitFormOrBlur(inputRef.current), () => inputRefTo.current?.focus(), () => inputRef.current?.blur(), ) }, [toggleCalendar, submitFormOrBlur], ) const handleRangeToKeydown = useCallback( (e: React.KeyboardEvent) => { handleDatepickerKeydown( e.nativeEvent, () => toggleCalendar(), () => submitFormOrBlur(inputRefTo.current), undefined, () => inputRefTo.current?.blur(), ) }, [toggleCalendar, submitFormOrBlur], ) const handleMultipleKeydown = useCallback( (e: React.KeyboardEvent) => { handleDatepickerKeydown( e.nativeEvent, () => toggleCalendar(), () => submitFormOrBlur(inputRef.current), undefined, () => inputRef.current?.blur(), handleMultipleComma, ) }, [toggleCalendar, submitFormOrBlur, handleMultipleComma], ) // Form reset: listen for the parent
's reset event and restore initial values useEffect(() => { const form = wrapperRef.current?.closest('form') if (!form) return const handleReset = () => { window.setTimeout(() => { setInternalValue(initialValuesRef.current) setCalendarOpen(false) if (changeInputRef.current) { changeInputRef.current.value = initialValuesRef.current.join(',') } }, 0) } form.addEventListener('reset', handleReset) return () => form.removeEventListener('reset', handleReset) }, []) // Expose value getter/setter on ref for RHF register() compatibility. // RHF reads ref.value via getFieldValue(field._f) — we read directly from the // hidden input DOM element so the value is always fresh (not stale from a // batched state update). RHF calls ref.value = x on mount and on reset. useImperativeHandle( ref, () => ({ get value() { return changeInputRef.current?.value ?? '' }, set value(newVal: string) { const newValues = newVal ? newVal.split(',').filter(Boolean) : [] setInternalValue(newValues) if (changeInputRef.current) { changeInputRef.current.value = newVal ?? '' } }, focus() { inputRef.current?.focus() }, blur() { inputRef.current?.blur() }, }) as unknown as HTMLDivElement, [], ) return { id, inputId, label, values, calendarOpen, dateformat, multiple, maxlength, range, showRangeLabels, weeknumbers, withcontrols, excludedates, excludeweekdays, disabled, readOnly, required, name, placeholder, fullwidth, hasFieldset, inline, helptext, helptextDropdown, helptextDropdownButton, hasError, errorMessage, optionalTag, optionalText, requiredTag, requiredText, tagText, useWrapper, className, minStr, maxStr, effectiveCurrentMonth, today, inputType, isIOSDevice, strings, inputClasses, buttonClasses, rangeLabelClasses, datepickerInputsClasses, isInputDisabled, hasCounter, inputRef, inputRefTo, changeInputRef, btnRef, wrapperRef, onChange, toggleCalendar, hideCalendar, handleDateSelected, handleCalendarClose, handleFocus, handleSingleInputChange, handleRangeFromChange, handleRangeToChange, handleRangeBlur, handleMultipleBlur, handleTagRemoved, handleSingleKeydown, handleRangeFromKeydown, handleRangeToKeydown, handleMultipleKeydown, restProps, } }