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