feat(forms): add PhoneInput and unify module form validation UX
This commit is contained in:
87
src/components/form/phone-input/PhoneInput.tsx
Normal file
87
src/components/form/phone-input/PhoneInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user