frontend/components/PhoneInput/index.tsx (view raw)
1import React, {useState} from 'react';
2import {PhoneNumberUtil} from 'google-libphonenumber';
3import {
4 InputAdornment,
5 MenuItem,
6 Select,
7 TextField,
8 TextFieldProps,
9 Typography,
10} from '@mui/material';
11import {
12 CountryIso2,
13 defaultCountries,
14 FlagImage,
15 parseCountry,
16 usePhoneInput,
17} from 'react-international-phone';
18import 'react-international-phone/style.css';
19
20interface Props {
21 value: string;
22 required?: boolean;
23 onChange: ({
24 phone,
25 country,
26 }: {
27 phone: string;
28 country: CountryIso2 | '';
29 error: boolean;
30 }) => void;
31 label: string;
32}
33
34const phoneUtil = PhoneNumberUtil.getInstance();
35const isPhoneValid = (phone: string) => {
36 try {
37 return phoneUtil.isValidNumber(phoneUtil.parseAndKeepRawInput(phone));
38 } catch (error) {
39 return false;
40 }
41};
42
43const PhoneInput = ({
44 value,
45 onChange,
46 label,
47 required,
48 ...textFieldProps
49}: Omit<TextFieldProps, 'onChange'> & Props) => {
50 const [phone, setPhone] = useState(value);
51
52 const browserLocales = navigator.language.split('-');
53 const defaultCountry =
54 browserLocales[browserLocales.length - 1].toLowerCase();
55
56 const {inputValue, handlePhoneValueChange, inputRef, country, setCountry} =
57 usePhoneInput({
58 defaultCountry: defaultCountry || defaultCountries[0][1],
59 value: phone,
60 countries: defaultCountries,
61 onChange: ({phone, country}) => {
62 setPhone(phone);
63 if (isPhoneValid(phone))
64 onChange({phone, country: country.iso2, error: false});
65 else onChange({phone: '', country: '', error: true});
66 },
67 });
68
69 return (
70 <TextField
71 fullWidth
72 required={required}
73 error={inputValue && (!phone || value !== phone)}
74 {...textFieldProps}
75 label={label}
76 value={inputValue}
77 onChange={handlePhoneValueChange}
78 type="tel"
79 inputRef={inputRef}
80 InputProps={{
81 startAdornment: (
82 <InputAdornment
83 position="start"
84 style={{marginRight: '2px', marginLeft: '-8px'}}
85 >
86 <Select
87 MenuProps={{
88 style: {
89 height: '300px',
90 width: '360px',
91 top: '10px',
92 left: '-34px',
93 },
94 transformOrigin: {
95 vertical: 'top',
96 horizontal: 'left',
97 },
98 }}
99 sx={{
100 width: 'max-content',
101 // Remove default outline (display only on focus)
102 fieldset: {
103 display: 'none',
104 },
105 '&.Mui-focused:has(div[aria-expanded="false"])': {
106 fieldset: {
107 display: 'block',
108 },
109 },
110 // Update default spacing
111 '.MuiSelect-select': {
112 padding: '8px',
113 paddingRight: '24px !important',
114 },
115 svg: {
116 right: 0,
117 },
118 }}
119 value={country.iso2}
120 onChange={e => setCountry(e.target.value)}
121 renderValue={value => (
122 <FlagImage iso2={value} style={{display: 'flex'}} />
123 )}
124 >
125 {defaultCountries.map(c => {
126 const country = parseCountry(c);
127 return (
128 <MenuItem key={country.iso2} value={country.iso2}>
129 <FlagImage
130 iso2={country.iso2}
131 style={{marginRight: '8px'}}
132 />
133 <Typography marginRight="8px">{country.name}</Typography>
134 <Typography color="gray">+{country.dialCode}</Typography>
135 </MenuItem>
136 );
137 })}
138 </Select>
139 </InputAdornment>
140 ),
141 }}
142 />
143 );
144};
145
146export default PhoneInput;