'use client' import { useState, useEffect, useRef, type CSSProperties, type ReactNode } from 'react' type GeoData = Record> interface CiudadSelectFieldProps { name: string label: ReactNode value: string required: boolean departamento?: string | null onChange: (value: string) => void error?: string } export function CiudadSelectField({ name, label, value, required, departamento, onChange, error, }: CiudadSelectFieldProps) { const [geoData, setGeoData] = useState(null) const [open, setOpen] = useState(false) const [activeIdx, setActiveIdx] = useState(-1) const [dropdownStyle, setDropdownStyle] = useState({}) const inputRef = useRef(null) // Stable ref to always call the latest onChange without it in effect deps const onChangeRef = useRef(onChange) onChangeRef.current = onChange const prevDeptoRef = useRef(departamento) useEffect(() => { fetch('/data/geo_paraguay_full.json') .then((r) => r.json()) .then(setGeoData) .catch(() => {}) }, []) // Clear city when departamento changes so no orphan value remains useEffect(() => { if (prevDeptoRef.current !== departamento) { onChangeRef.current('') setOpen(false) } prevDeptoRef.current = departamento }, [departamento]) // Close fixed dropdown on any scroll (input moves, list stays) useEffect(() => { if (!open) return const close = () => setOpen(false) window.addEventListener('scroll', close, { passive: true, capture: true }) return () => window.removeEventListener('scroll', close, { capture: true }) }, [open]) function computeDropdownStyle() { if (!inputRef.current) return const rect = inputRef.current.getBoundingClientRect() setDropdownStyle({ top: rect.bottom + 4, left: rect.left, width: rect.width }) } const ciudades: string[] = (() => { if (!geoData) return [] if (departamento) return Object.keys(geoData[departamento] ?? {}) return Object.values(geoData).flatMap((d) => Object.keys(d)) })() const query = value.trim().toLowerCase() const suggestions = query.length >= 2 ? ciudades.filter((c) => c.toLowerCase().includes(query)).slice(0, 10) : [] function handleInput(v: string) { onChange(v) computeDropdownStyle() setOpen(true) setActiveIdx(-1) } function select(ciudad: string) { onChange(ciudad) setOpen(false) setActiveIdx(-1) } function handleKeyDown(e: React.KeyboardEvent) { if (!open || suggestions.length === 0) return if (e.key === 'ArrowDown') { e.preventDefault() setActiveIdx((i) => Math.min(i + 1, suggestions.length - 1)) } else if (e.key === 'ArrowUp') { e.preventDefault() setActiveIdx((i) => Math.max(i - 1, -1)) } else if (e.key === 'Enter' && activeIdx >= 0) { e.preventDefault() select(suggestions[activeIdx]) } else if (e.key === 'Escape') { setOpen(false) } } const listId = `${name}-suggestions` return (
0 ? listId : undefined} aria-activedescendant={activeIdx >= 0 ? `${name}-opt-${activeIdx}` : undefined} autoComplete="off" className="text-short-input" ref={inputRef} onChange={(e) => handleInput(e.target.value)} onKeyDown={handleKeyDown} onFocus={() => { if (suggestions.length > 0) { computeDropdownStyle(); setOpen(true) } }} onBlur={() => setTimeout(() => setOpen(false), 150)} /> {open && suggestions.length > 0 && (
    {suggestions.map((ciudad, i) => (
  • select(ciudad)} onTouchEnd={(e) => { e.preventDefault(); select(ciudad) }} > {ciudad}
  • ))}
)}
{error && ( )}
) }