feat: phone number validation

This commit is contained in:
2025-07-19 10:04:00 +04:00
parent 964641a58d
commit 84cc04ea67
7 changed files with 148 additions and 69 deletions

View File

@ -7,67 +7,63 @@ import {
type InputBaseProps,
type PolymorphicComponentProps,
} from "@mantine/core";
import { useUncontrolled } from "@mantine/hooks";
import CountrySelect from "@/components/PhoneInput/components/CountrySelect";
import getFormat from "@/components/PhoneInput/utils/getFormat";
import { Country } from "@/components/PhoneInput/types";
import getInitialDataFromValue from "@/components/PhoneInput/utils/getInitialDataFromValue";
import getPhoneMask from "@/components/PhoneInput/utils/getPhoneMask";
export type Props = {
type AdditionalProps = {
onChange: (value: string | null) => void;
setPhoneMask: (mask: string) => void;
initialCountryCode?: string;
defaultValue?: string;
} & Omit<
};
type InputProps = Omit<
PolymorphicComponentProps<typeof IMaskInput, InputBaseProps>,
"onChange" | "defaultValue"
> & { onChange: (value: string | null) => void };
>;
export type Props = AdditionalProps & InputProps;
const PhoneInput = ({
initialCountryCode = "RU",
value: _value,
onChange: _onChange,
defaultValue,
setPhoneMask: _setPhoneMask,
...props
}: Props) => {
const [value, onChange] = useUncontrolled({
value: _value,
defaultValue,
onChange: _onChange,
});
const initialData = useRef(
getInitialDataFromValue(value, initialCountryCode)
);
const [country, setCountry] = useState(initialData.current.country);
const [format, setFormat] = useState(initialData.current.format);
const [localValue, setLocalValue] = useState(
initialData.current.localValue
const [mask, setMask] = useState<string>("");
const initialData = useRef(getInitialDataFromValue(initialCountryCode));
const [country, setCountry] = useState<Country>(
initialData.current.country
);
const [value, setValue] = useState<string>("");
const inputRef = useRef<HTMLInputElement>(null);
const [dropdownWidth, setDropdownWidth] = useState<number>(300);
const lastNotifiedValue = useRef<string | null>(value ?? "");
useEffect(() => {
const value = localValue.trim();
if (value !== lastNotifiedValue.current) {
lastNotifiedValue.current = value;
onChange(value);
}
}, [country.code, localValue]);
const onChange = (numberWithoutCode: string) => {
setValue(numberWithoutCode);
_onChange(`+${country.callingCode} ${numberWithoutCode}`);
};
const setPhoneMask = (phoneMask: string, country: Country) => {
setMask(phoneMask);
_setPhoneMask(`+${country.callingCode} ${phoneMask}`);
};
useEffect(() => {
if (
typeof value !== "undefined" &&
value !== lastNotifiedValue.current
) {
const initialData = getInitialDataFromValue(
value,
initialCountryCode
);
lastNotifiedValue.current = value;
setCountry(initialData.country);
setFormat(initialData.format);
setLocalValue(initialData.localValue);
setPhoneMask(initialData.current.format, country);
}, [initialData.current.format]);
useEffect(() => {
const localValue = value.trim();
if (localValue !== lastNotifiedValue.current) {
lastNotifiedValue.current = localValue;
onChange(localValue);
}
}, [value]);
}, [country.code, value]);
const { readOnly, disabled } = props;
const leftSectionWidth = 90;
@ -88,8 +84,8 @@ const PhoneInput = ({
country={country}
setCountry={country => {
setCountry(country);
setFormat(getFormat(country.code));
setLocalValue("");
setPhoneMask(getPhoneMask(country.code), country);
setValue("");
if (inputRef.current) {
inputRef.current.focus();
}
@ -110,9 +106,9 @@ const PhoneInput = ({
},
}}
inputMode={"numeric"}
mask={format.mask}
value={localValue}
onAccept={value => setLocalValue(value)}
mask={mask}
value={value}
onAccept={value => setValue(value)}
/>
);
};