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