"use client"; import { Select } from "@/components/FormElements/select"; import { cn } from "@/lib/utils"; import { normalizeToE164, PHONE_UI_MAX_LENGTH, splitE164ForEditing, } from "@/lib/phone/e164"; import { DEFAULT_PHONE_COUNTRY, PHONE_COUNTRIES, } from "@/constants/phone-countries"; import { AsYouType, type CountryCode } from "libphonenumber-js"; import { useCallback, useEffect, useId, useRef, useState, type ChangeEvent, } from "react"; import type { ComponentProps } from "react"; import type { FieldValues } from "react-hook-form"; type CountrySelectOnChange = NonNullable< ComponentProps>["onChange"] >; export type PhoneNumberInputProps = { label: string; /** Stored value in E.164 (e.g. `+14155552671`). */ value?: string; onChange: (e164: string) => void; onBlur?: () => void; error?: string; required?: boolean; disabled?: boolean; dataCy?: string; defaultCountry?: CountryCode; className?: string; }; export function PhoneNumberInput({ label, value = "", onChange, onBlur, error, required = false, disabled = false, dataCy, defaultCountry = DEFAULT_PHONE_COUNTRY, className, }: PhoneNumberInputProps) { const inputId = useId(); const lastEmitted = useRef(value); const [country, setCountry] = useState(() => splitE164ForEditing(value, defaultCountry).country, ); const [national, setNational] = useState(() => splitE164ForEditing(value, defaultCountry).national, ); const syncFromE164 = useCallback( (e164: string | undefined) => { const next = splitE164ForEditing(e164, defaultCountry); setCountry(next.country); setNational(next.national); }, [defaultCountry], ); useEffect(() => { if (value === lastEmitted.current) return; lastEmitted.current = value; syncFromE164(value); }, [value, syncFromE164]); const emitE164 = useCallback( (nationalInput: string, countryCode: CountryCode) => { const e164 = normalizeToE164(nationalInput, countryCode); const next = e164 ?? ""; lastEmitted.current = next; onChange(next); }, [onChange], ); const handleNationalChange = (e: ChangeEvent) => { const raw = e.target.value.slice(0, PHONE_UI_MAX_LENGTH); const formatted = new AsYouType(country).input(raw); setNational(formatted); emitE164(formatted, country); }; const handleCountryChange = (e: ChangeEvent) => { const nextCountry = e.target.value as CountryCode; setCountry(nextCountry); const formatted = new AsYouType(nextCountry).input(national); setNational(formatted); emitE164(formatted, nextCountry); }; const handleBlur = () => { const e164 = normalizeToE164(national, country); if (e164) { const display = splitE164ForEditing(e164, country); setNational(display.national); lastEmitted.current = e164; onChange(e164); } else if (!required) { lastEmitted.current = ""; onChange(""); } onBlur?.(); }; return (
name={"phoneCountry" as never} label="Country code" items={PHONE_COUNTRIES} searchable searchPlaceholder="Search country or code…" value={country} onChange={handleCountryChange as CountrySelectOnChange} disabled={disabled} className={cn( "w-full shrink-0 space-y-0 sm:w-[min(100%,13.5rem)]", "[&_label]:sr-only", )} dataCy={dataCy ? `${dataCy}-country` : undefined} />
{error ? ( ) : null}
); }