"use client" import { AsYouType, CountryCode, getCountries, getCountryCallingCode, parsePhoneNumberFromString } from 'libphonenumber-js'; import { ChevronDown, Search } from 'lucide-react'; import * as React from 'react'; import { useAppT } from '@djangocfg/i18n'; import { Button } from '../../forms/button'; import { Input } from '../../forms/input'; import { cn } from '../../../lib'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../../navigation/dropdown-menu'; import { Flag } from '../../specialized/flag'; // Get country name from country code using browser's built-in Intl.DisplayNames const getCountryName = (countryCode: CountryCode): string => { try { const displayNames = new Intl.DisplayNames(['en'], { type: 'region' }) return displayNames.of(countryCode) || countryCode } catch { // Fallback for unsupported country codes return countryCode } } // Generate all countries from libphonenumber-js const getAllCountries = () => { return getCountries().map(countryCode => ({ code: countryCode, name: getCountryName(countryCode), dialCode: `+${getCountryCallingCode(countryCode)}` })).sort((a, b) => a.name.localeCompare(b.name)) } const COUNTRIES = getAllCountries() export interface PhoneInputProps { value?: string onChange?: (value: string | undefined) => void defaultCountry?: CountryCode className?: string placeholder?: string disabled?: boolean } const PhoneInput = React.forwardRef( ({ className, value = '', onChange, defaultCountry = 'US', placeholder, disabled = false, ...props }, ref) => { const t = useAppT() const [selectedCountry, setSelectedCountry] = React.useState(defaultCountry) const [inputValue, setInputValue] = React.useState('') const [isDropdownOpen, setIsDropdownOpen] = React.useState(false) const [searchQuery, setSearchQuery] = React.useState('') const [highlightedIndex, setHighlightedIndex] = React.useState(-1) // Prepare translations const translations = React.useMemo(() => ({ searchCountries: t('ui.phone.searchCountries'), noCountries: t('ui.phone.noCountries'), }), [t]) // Find country data const currentCountry = COUNTRIES.find(c => c.code === selectedCountry) || COUNTRIES[0]! // Filter countries based on search query const filteredCountries = React.useMemo(() => { if (!searchQuery.trim()) return COUNTRIES const query = searchQuery.toLowerCase() return COUNTRIES.filter(country => country.name.toLowerCase().includes(query) || country.dialCode.includes(query) || country.code.toLowerCase().includes(query) ) }, [searchQuery]) // Initialize input value from props React.useEffect(() => { if (value) { try { const phoneNumber = parsePhoneNumberFromString(value) if (phoneNumber) { setSelectedCountry(phoneNumber.country || defaultCountry) setInputValue(phoneNumber.nationalNumber) } else { setInputValue(value) } } catch { setInputValue(value) } } }, [value, defaultCountry]) // Reset highlighted index when filtered countries change React.useEffect(() => { setHighlightedIndex(-1) }, [filteredCountries]) // Reset search when dropdown closes React.useEffect(() => { if (!isDropdownOpen) { setSearchQuery('') setHighlightedIndex(-1) } }, [isDropdownOpen]) // Handle country selection const handleCountrySelect = (country: typeof COUNTRIES[0]) => { setSelectedCountry(country.code) setIsDropdownOpen(false) setSearchQuery('') setHighlightedIndex(-1) // Format existing number for new country if (inputValue) { const formatter = new AsYouType(country.code) const formatted = formatter.input(inputValue) setInputValue(formatted) // Get E.164 format for onChange const phoneNumber = formatter.getNumber() onChange?.(phoneNumber?.number) } } // Handle keyboard navigation const handleKeyDown = (e: React.KeyboardEvent) => { if (!isDropdownOpen) return switch (e.key) { case 'ArrowDown': e.preventDefault() setHighlightedIndex(prev => prev < filteredCountries.length - 1 ? prev + 1 : 0 ) break case 'ArrowUp': e.preventDefault() setHighlightedIndex(prev => prev > 0 ? prev - 1 : filteredCountries.length - 1 ) break case 'Enter': e.preventDefault() if (highlightedIndex >= 0 && highlightedIndex < filteredCountries.length) { handleCountrySelect(filteredCountries[highlightedIndex]!) } break case 'Escape': e.preventDefault() setIsDropdownOpen(false) break } } // Handle input change const handleInputChange = (e: React.ChangeEvent) => { const input = e.target.value // Use AsYouType formatter for real-time formatting const formatter = new AsYouType(selectedCountry) const formatted = formatter.input(input) setInputValue(formatted) // Get the parsed phone number for validation and E.164 format const phoneNumber = formatter.getNumber() onChange?.(phoneNumber?.number) } // Handle paste events to extract phone numbers const handlePaste = (e: React.ClipboardEvent) => { const pastedText = e.clipboardData.getData('text') try { // Try to parse as international number first const phoneNumber = parsePhoneNumberFromString(pastedText) if (phoneNumber) { e.preventDefault() setSelectedCountry(phoneNumber.country || selectedCountry) setInputValue(phoneNumber.nationalNumber) onChange?.(phoneNumber.number) return } } catch { // Let default paste behavior handle it } } return (
{/* Country Dropdown */} {/* Search Input */}
setSearchQuery(e.target.value)} className="pl-8 h-8" autoFocus />
{/* Countries List */}
{filteredCountries.length === 0 ? (
{translations.noCountries}
) : ( filteredCountries.map((country, index) => ( handleCountrySelect(country)} className={cn( "flex items-center gap-3 px-3 py-2 cursor-pointer", index === highlightedIndex && "bg-accent" )} > {country.name} {country.dialCode} )) )}
{/* Phone Input */}
) } ) PhoneInput.displayName = "PhoneInput" export { PhoneInput }