/* eslint-disable @typescript-eslint/indent */ import { useMemo, useState, useEffect, ChangeEvent, useRef, KeyboardEvent } from 'react'; import { Box, MenuItem, Typography, TextField, Dialog, DialogContent, IconButton, Select, Slide, Stack, } from '@mui/material'; import { Clear as ClearIcon } from '@mui/icons-material'; import { useFormContext } from 'react-hook-form'; import { FlagEmoji, defaultCountries, parseCountry } from 'react-international-phone'; import type { SlideProps, SxProps } from '@mui/material'; import type { CountryIso2 } from 'react-international-phone'; import { useMobile } from '../hooks/mobile'; function useFormContextSafe() { try { return useFormContext(); } catch { return null; } } function Transition({ ref, ...props }: { ref: React.RefObject } & SlideProps) { return ; } export type CountrySelectProps = { value: CountryIso2; onChange: (value: CountryIso2) => void; name?: string; sx?: SxProps; showDialCode?: boolean; label?: string; allowClear?: boolean; bordered?: boolean; disabled?: boolean; }; export default function CountrySelect({ ref = undefined, value, onChange, name = '', sx = {}, showDialCode = false, label = '', bordered = false, allowClear = false, disabled = false, }: CountrySelectProps & { ref?: React.RefObject; }) { const formContext = useFormContextSafe(); const [open, setOpen] = useState(false); const [searchText, setSearchText] = useState(''); const inputRef = useRef(null); const menuRef = useRef(null); const listRef = useRef(null); const [focusedIndex, setFocusedIndex] = useState(-1); const itemHeightRef = useRef(40); const { isMobile } = useMobile(); const measuredRef = useRef(false); // Handle window resize useEffect(() => { if (!open) return () => {}; const handleResize = () => { measuredRef.current = false; }; window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); const scrollToTop = () => { if (listRef.current) { listRef.current.scrollTop = 0; } }; const measureItemHeight = () => { if (!listRef.current || !open) return; const items = listRef.current.querySelectorAll('.MuiMenuItem-root'); if (items.length > 0) { const firstItem = items[0] as HTMLElement; if (firstItem.offsetHeight > 0) { itemHeightRef.current = firstItem.offsetHeight; } } }; const controlScrollPosition = (index: number) => { if (!listRef.current) return; // Always measure height when dropdown is open for accuracy if (open && !measuredRef.current) { measureItemHeight(); measuredRef.current = true; } const listHeight = listRef.current.clientHeight; const targetPosition = index * itemHeightRef.current; if (index < 2) { listRef.current.scrollTop = 0; } else if (index > filteredCountries.length - 3) { listRef.current.scrollTop = listRef.current.scrollHeight - listHeight; } else { const scrollPosition = targetPosition - listHeight / 2 + itemHeightRef.current / 2; listRef.current.scrollTop = Math.max(0, scrollPosition); } }; useEffect(() => { let timeout: NodeJS.Timeout | null = null; if (open) { timeout = setTimeout(() => { scrollToTop(); if (!isMobile && inputRef.current) { inputRef.current.focus(); } }, 100); } else { setSearchText(''); setFocusedIndex(-1); } return () => { if (timeout) { clearTimeout(timeout); } }; }, [open, isMobile]); const filteredCountries = useMemo(() => { if (!searchText) return defaultCountries; return defaultCountries.filter((c) => { const parsed = parseCountry(c); return ( parsed.name.toLowerCase().includes(searchText.toLowerCase()) || parsed.iso2.toLowerCase().includes(searchText.toLowerCase()) || `+${parsed.dialCode}`.includes(searchText) ); }); }, [searchText]); useEffect(() => { scrollToTop(); setFocusedIndex(-1); }, [searchText]); useEffect(() => { let timeout: NodeJS.Timeout | null = null; if (focusedIndex >= 0) { timeout = setTimeout(() => { controlScrollPosition(focusedIndex); }, 10); } return () => { if (timeout) { clearTimeout(timeout); } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [focusedIndex, filteredCountries.length]); const countryDetail = useMemo(() => { const item = defaultCountries.find((v) => v[1] === value); return value && item ? parseCountry(item) : { name: '' }; }, [value]); const handleCountryClick = (code: CountryIso2) => { onChange(code); if (formContext && name) { formContext.setValue(name, code); } setOpen(false); }; const handleClear = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); onChange('' as CountryIso2); if (formContext && name) { formContext.setValue(name, ''); } }; const handleSearchChange = (e: ChangeEvent) => { e.stopPropagation(); setSearchText(e.target.value); }; const handleKeyDown = (e: KeyboardEvent) => { e.stopPropagation(); if (e.key === 'Escape') { setOpen(false); return; } const handleNavigation = (direction: 'next' | 'prev') => { e.preventDefault(); setFocusedIndex((prev) => { if (direction === 'next') { if (prev === -1) return 0; return prev >= filteredCountries.length - 1 ? 0 : prev + 1; } if (prev === -1) return filteredCountries.length - 1; return prev <= 0 ? filteredCountries.length - 1 : prev - 1; }); }; if (e.key === 'Tab') { handleNavigation(e.shiftKey ? 'prev' : 'next'); return; } if (e.key === 'ArrowDown') { handleNavigation('next'); return; } if (e.key === 'ArrowUp') { handleNavigation('prev'); return; } if (e.key === 'Enter' && focusedIndex >= 0 && focusedIndex < filteredCountries.length) { e.preventDefault(); const country = parseCountry(filteredCountries[focusedIndex]); handleCountryClick(country.iso2); } }; const countryListContent = ( <> { e.stopPropagation(); }}> e.stopPropagation()} size="small" variant="outlined" disabled={disabled} /> {filteredCountries.length > 0 ? ( filteredCountries.map((c: any, index) => { const parsed = parseCountry(c); const isFocused = index === focusedIndex; return ( handleCountryClick(parsed.iso2)} sx={{ display: 'flex', alignItems: 'center', '&.Mui-selected': { backgroundColor: 'primary.lighter', }, '&:hover': { backgroundColor: 'grey.100', }, ...(isFocused ? { backgroundColor: 'grey.100', outline: 'none', } : {}), }}> {parsed.name} {showDialCode && ( {`+${parsed.dialCode}`} )} ); }) ) : ( No countries found )} ); if (!isMobile) { return ( ); } return ( { if (disabled) return; setOpen(true); }} sx={{ display: 'flex', alignItems: 'center', flexWrap: 'nowrap', gap: 0.5, cursor: 'pointer', padding: '8px', paddingRight: '24px', position: 'relative', ...(!bordered ? { border: 'none', } : {}), '-webkit-tap-highlight-color': 'transparent', userSelect: 'none', background: 'none', '&:hover, &:focus, &:active': { backgroundColor: 'transparent', outline: 'none', }, ...(disabled ? { backgroundColor: 'grey.100', cursor: 'not-allowed', color: 'text.disabled', '&:hover, &:focus, &:active': { backgroundColor: 'grey.100', color: 'text.disabled', }, } : {}), touchAction: 'manipulation', }}> {value ? ( <> {countryDetail?.name} {allowClear && ( { event.stopPropagation(); event.preventDefault(); }} onTouchStart={(event) => { event.stopPropagation(); }} sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer', p: 0.5, mr: 1, borderRadius: '50%', border: 'none', background: 'transparent', '&:hover': { backgroundColor: 'action.hover', }, }}> )} ) : ( {label || 'Select country...'} )} setOpen(false)} fullWidth maxWidth="xs" slotProps={{ paper: { sx: { position: 'absolute', bottom: 0, m: 0, p: 0, borderBottomLeftRadius: 0, borderBottomRightRadius: 0, width: '100%', }, }, }} slots={{ transition: Transition, }}> Select Country setOpen(false)} sx={{ '-webkit-tap-highlight-color': 'transparent' }}> ✕ {countryListContent} ); }