import { useEffect, useMemo, useRef, useState } from "react"; import type { KeyboardEvent } from "react"; import type { Locale, LocaleOption } from "@/hooks/useI18n"; type Props = { locale: Locale; onChange: (code: Locale) => void; options: LocaleOption[]; label: string; searchPlaceholder: string; }; export function LocaleSelector({ locale, onChange, options, label, searchPlaceholder }: Props) { const dropdownRef = useRef(null); const searchRef = useRef(null); const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); const [focusedIndex, setFocusedIndex] = useState(-1); const selected = useMemo( () => options.find((opt) => opt.code === locale) || options[0], [locale, options] ); const filtered = useMemo(() => { const term = search.trim().toLowerCase(); return term ? options.filter((opt) => opt.name.toLowerCase().includes(term)) : options; }, [options, search]); useEffect(() => { if (!open) return; const clickHandler = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setOpen(false); setSearch(""); setFocusedIndex(-1); } }; document.addEventListener("click", clickHandler); setTimeout(() => searchRef.current?.focus(), 0); return () => document.removeEventListener("click", clickHandler); }, [open]); const toggle = () => setOpen((prev) => !prev); const close = () => { setOpen(false); setSearch(""); setFocusedIndex(-1); }; const select = (opt: LocaleOption) => { onChange(opt.code); close(); }; const onKeyDownList = (e: KeyboardEvent) => { if (!open) return; const list = filtered; if (!list.length) return; if (e.key === "ArrowDown") { e.preventDefault(); setFocusedIndex((prev) => { const next = (prev + 1) % list.length; scrollIntoView(next); return next; }); } else if (e.key === "ArrowUp") { e.preventDefault(); setFocusedIndex((prev) => { const next = (prev - 1 + list.length) % list.length; scrollIntoView(next); return next; }); } else if (e.key === "Enter" && focusedIndex >= 0) { e.preventDefault(); select(list[focusedIndex]); } else if (e.key === "Escape") { e.preventDefault(); close(); } }; const scrollIntoView = (index: number) => { requestAnimationFrame(() => { const items = dropdownRef.current?.querySelectorAll(".locale-panel-item"); const el = items?.[index] as HTMLElement | undefined; el?.scrollIntoView({ block: "nearest" }); }); }; const handleItemKeyDown = (e: KeyboardEvent, opt: LocaleOption) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); select(opt); } }; return (

{label}

{open && (
setSearch(e.target.value)} type="text" placeholder={searchPlaceholder} />
    {filtered.map((opt, index) => (
  • select(opt)} onKeyDown={(e) => handleItemKeyDown(e, opt)} onMouseEnter={() => setFocusedIndex(index)} > {opt.emoji} {opt.name}
  • ))} {filtered.length === 0 &&
  • No languages found.
  • }
)}
); }