feat(forms): add PhoneInput and unify module form validation UX

This commit is contained in:
2026-06-23 16:28:28 +05:30
parent 27cf5c9d3f
commit 066374bebc
30 changed files with 1415 additions and 229 deletions

View File

@@ -0,0 +1,87 @@
'use client';
import { useEffect, useState, type RefObject } from 'react';
import {
parsePhoneNumberFromString,
type CountryCode,
} from 'libphonenumber-js';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import { buildPhoneValue, getNationalValue } from './countries';
import { CountryCombobox } from './CountryCombobox';
interface PhoneInputProps {
value?: string;
onChange: (value: string) => void;
onBlur?: () => void;
disabled?: boolean;
'aria-invalid'?: boolean;
placeholder?: string;
defaultCountry?: CountryCode;
portalContainer?: RefObject<HTMLDivElement | null>;
}
export function PhoneInput({
value = '',
onChange,
onBlur,
disabled = false,
'aria-invalid': ariaInvalid,
placeholder = 'Enter phone number',
defaultCountry = 'IN',
portalContainer,
}: PhoneInputProps) {
const parsedCountry = parsePhoneNumberFromString(value)?.country;
const [country, setCountry] = useState<CountryCode>(
parsedCountry ?? defaultCountry,
);
useEffect(() => {
const parsed = parsePhoneNumberFromString(value);
if (parsed?.country) {
setCountry(parsed.country);
}
}, [value]);
const nationalValue = getNationalValue(value, country);
function handleCountryChange(nextCountry: CountryCode) {
setCountry(nextCountry);
onChange(buildPhoneValue(nextCountry, nationalValue));
}
function handleNationalChange(nextNationalValue: string) {
onChange(buildPhoneValue(country, nextNationalValue));
}
return (
<div
className={cn(
'flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50 dark:bg-input/30',
ariaInvalid &&
'border-destructive focus-within:border-destructive focus-within:ring-destructive/20',
disabled && 'cursor-not-allowed opacity-50',
)}
>
<CountryCombobox
value={country}
onValueChange={handleCountryChange}
disabled={disabled}
portalContainer={portalContainer}
/>
<Input
type="tel"
value={nationalValue}
onChange={(event) => handleNationalChange(event.target.value)}
onBlur={onBlur}
disabled={disabled}
aria-invalid={ariaInvalid}
placeholder={placeholder}
className="h-9 flex-1 rounded-none border-0 bg-transparent px-3 py-1 shadow-none focus-visible:ring-0"
/>
</div>
);
}