Phone Input

An implementation of a Phone Input component for React, built on top of Shadcn UI input component.

Read the Docs

Installation

npm install @hookform/resolvers/zod react-hook-form react-phone-number-input
phone-input.tsx
1
import { CheckIcon, ChevronsUpDown } from 'lucide-react';
2
import * as React from 'react';
3
import * as RPNInput from 'react-phone-number-input';
4
import flags from 'react-phone-number-input/flags';
5
import { Button } from '@/components/website/ui/button';
6
import {
7
Command,
8
CommandEmpty,
9
CommandGroup,
10
CommandInput,
11
CommandItem,
12
CommandList,
13
} from '@/components/website/ui/command';
14
import {
15
Popover,
16
PopoverContent,
17
PopoverTrigger,
18
} from '@/components/website/ui/popover';
19
20
import { cn } from '@/lib/utils';
21
import { ScrollArea } from '@/components/website/ui/scroll-area';
22
23
type PhoneInputProps = Omit<
24
React.InputHTMLAttributes<HTMLInputElement>,
25
'onChange' | 'value'
26
> &
27
Omit<RPNInput.Props<typeof RPNInput.default>, 'onChange'> & {
28
onChange?: (value: RPNInput.Value) => void;
29
};
30
const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> =
31
React.forwardRef<React.ElementRef<typeof RPNInput.default>, PhoneInputProps>(
32
({ className, onChange, ...props }, ref) => {
33
return (
34
<RPNInput.default
35
ref={ref}
36
className={cn('flex', className)}
37
flagComponent={FlagComponent}
38
countrySelectComponent={CountrySelect}
39
inputComponent={InputComponent}
40
/**
41
* Handles the onChange event.
42
*
43
* react-phone-number-input might trigger the onChange event as undefined
44
* when a valid phone number is not entered. To prevent this,
45
* the value is coerced to an empty string.
46
*
47
* @param {E164Number | undefined} value - The entered value
48
*/
49
// @ts-ignore
50
onChange={(value) => onChange?.(value || '')}
51
{...props}
52
/>
53
);
54
}
55
);
56
PhoneInput.displayName = 'PhoneInput';
57
58
const InputComponent = React.forwardRef<HTMLInputElement, any>(
59
({ className, ...props }, ref) => (
60
<input
61
className={cn(
62
'rounded-e-lg rounded-s-none px-2 bg-background outline-none ',
63
className
64
)}
65
{...props}
66
ref={ref}
67
/>
68
)
69
);
70
InputComponent.displayName = 'InputComponent';
71
72
type CountrySelectOption = { label: string; value: RPNInput.Country };
73
74
type CountrySelectProps = {
75
disabled?: boolean;
76
value: RPNInput.Country;
77
onChange: (value: RPNInput.Country) => void;
78
options: CountrySelectOption[];
79
};
80
81
const CountrySelect = ({
82
disabled,
83
value,
84
onChange,
85
options,
86
}: CountrySelectProps) => {
87
const handleSelect = React.useCallback(
88
(country: RPNInput.Country) => {
89
onChange(country);
90
},
91
[onChange]
92
);
93
94
return (
95
<Popover>
96
<PopoverTrigger asChild>
97
<Button
98
type='button'
99
variant={'outline'}
100
className={cn('flex gap-1 rounded-e-none rounded-s-lg px-3')}
101
disabled={disabled}
102
>
103
<FlagComponent country={value} countryName={value} />
104
<ChevronsUpDown
105
className={cn(
106
'-mr-2 h-4 w-4 opacity-50',
107
disabled ? 'hidden' : 'opacity-100'
108
)}
109
/>
110
</Button>
111
</PopoverTrigger>
112
<PopoverContent className='w-[300px] p-0'>
113
<Command>
114
<CommandList>
115
<ScrollArea className='h-72'>
116
<CommandInput placeholder='Search country...' />
117
<CommandEmpty>No country found.</CommandEmpty>
118
<CommandGroup>
119
{options
120
.filter((x) => x.value)
121
.map((option) => (
122
<CommandItem
123
className='gap-2'
124
key={option.value}
125
onSelect={() => handleSelect(option.value)}
126
>
127
<FlagComponent
128
country={option.value}
129
countryName={option.label}
130
/>
131
<span className='flex-1 text-sm'>{option.label}</span>
132
{option.value && (
133
<span className='text-foreground/50 text-sm'>
134
{`+${RPNInput.getCountryCallingCode(option.value)}`}
135
</span>
136
)}
137
<CheckIcon
138
className={cn(
139
'ml-auto h-4 w-4',
140
option.value === value ? 'opacity-100' : 'opacity-0'
141
)}
142
/>
143
</CommandItem>
144
))}
145
</CommandGroup>
146
</ScrollArea>
147
</CommandList>
148
</Command>
149
</PopoverContent>
150
</Popover>
151
);
152
};
153
154
const FlagComponent = ({ country, countryName }: RPNInput.FlagProps) => {
155
const Flag = flags[country];
156
157
return (
158
<span className='bg-foreground/20 flex h-4 w-6 overflow-hidden rounded-sm'>
159
{Flag && <Flag title={countryName} />}
160
</span>
161
);
162
};
163
FlagComponent.displayName = 'FlagComponent';
164
165
export { PhoneInput };