all repos — caroster @ main

[Octree] Group carpool to your event https://caroster.io

frontend/components/PhoneInput/index.tsx (view raw)

  1import React, {useState} from 'react';
  2import {PhoneNumberUtil} from 'google-libphonenumber';
  3import {
  4  InputAdornment,
  5  MenuItem,
  6  MenuProps,
  7  Select,
  8  TextField,
  9  TextFieldProps,
 10  Typography,
 11} from '@mui/material';
 12import {
 13  CountryIso2,
 14  defaultCountries,
 15  FlagImage,
 16  parseCountry,
 17  usePhoneInput,
 18} from 'react-international-phone';
 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        let formatedPhone = phone?.replace(/^\+0*/, '+');
 55        if (country.iso2 === 'fr')
 56          formatedPhone = formatedPhone?.replace(/^\+330/, '+33');
 57        setPhone(formatedPhone);
 58        if (isPhoneValid(formatedPhone))
 59          onChange({phone: formatedPhone, country: country.iso2, error: false});
 60        else onChange({phone: '', country: '', error: true});
 61      },
 62    });
 63
 64  return (
 65    <TextField
 66      fullWidth
 67      required={required}
 68      error={inputValue && (!phone || value !== phone)}
 69      {...textFieldProps}
 70      label={label}
 71      value={inputValue}
 72      onChange={handlePhoneValueChange}
 73      type="tel"
 74      inputRef={inputRef}
 75      slotProps={{
 76        input: {
 77          startAdornment: (
 78            <InputAdornment position="start" sx={{mr: 0.5, ml: -2}}>
 79              <Select
 80                MenuProps={menuProps}
 81                sx={selectSx}
 82                value={country.iso2}
 83                onChange={e => setCountry(e.target.value)}
 84                renderValue={value => (
 85                  <FlagImage iso2={value} style={{display: 'flex'}} />
 86                )}
 87              >
 88                {defaultCountries.map(c => {
 89                  const country = parseCountry(c);
 90                  return (
 91                    <MenuItem key={country.iso2} value={country.iso2}>
 92                      <FlagImage
 93                        iso2={country.iso2}
 94                        style={{marginRight: '8px'}}
 95                      />
 96                      <Typography marginRight="8px">{country.name}</Typography>
 97                      <Typography color="gray">+{country.dialCode}</Typography>
 98                    </MenuItem>
 99                  );
100                })}
101              </Select>
102            </InputAdornment>
103          ),
104        },
105      }}
106    />
107  );
108};
109
110const phoneUtil = PhoneNumberUtil.getInstance();
111const isPhoneValid = (phone: string) => {
112  try {
113    return phoneUtil.isValidNumber(phoneUtil.parseAndKeepRawInput(phone));
114  } catch (error) {
115    return false;
116  }
117};
118
119const selectSx = {
120  width: 'max-content',
121  // Remove default outline (display only on focus)
122  fieldset: {
123    display: 'none',
124  },
125  '&.Mui-focused:has(div[aria-expanded="false"])': {
126    fieldset: {
127      display: 'block',
128    },
129  },
130  // Update default spacing
131  '.MuiSelect-select': {
132    padding: '8px',
133    paddingRight: '24px !important',
134  },
135  svg: {
136    right: 0,
137  },
138};
139
140const menuProps: Partial<MenuProps> = {
141  style: {
142    height: '300px',
143    width: '360px',
144    top: '10px',
145    left: '-34px',
146  },
147  transformOrigin: {
148    vertical: 'top',
149    horizontal: 'left',
150  },
151};
152
153export default PhoneInput;