Skip to content

Commit 63bd7f3

Browse files
committed
Segregate the initial core of the phone components
1 parent ed89437 commit 63bd7f3

File tree

19 files changed

+1027
-6102
lines changed

19 files changed

+1027
-6102
lines changed

development/src/ant-phone/index.tsx

Lines changed: 38 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -4,72 +4,26 @@ import {FormContext} from "antd/es/form/context";
44
import Select from "antd/es/select";
55
import Input from "antd/es/input";
66

7-
import {PhoneInputProps, PhoneNumber} from "./types";
7+
import {PhoneInputProps} from "./types";
8+
import {PhoneNumber} from "../phone-hooks/types";
89

9-
import styleInject from "./styles";
10-
import timezones from "./metadata/timezones.json";
11-
import countries from "./metadata/countries.json";
12-
import validations from "./metadata/validations.json";
10+
import {mergedStyles} from "./styles";
11+
import {injectStyles} from "../phone-hooks/styles";
1312

14-
styleInject("styles.css");
13+
import {
14+
checkValidity,
15+
cleanInput,
16+
displayFormat,
17+
getDefaultISO2Code,
18+
getMetadata,
19+
getRawValue,
20+
parsePhoneNumber,
21+
usePhone,
22+
} from "../phone-hooks";
1523

16-
const slots = new Set(".");
24+
import countries from "../phone-hooks/metadata/countries.json";
1725

18-
const getMetadata = (rawValue: string, countriesList: typeof countries = countries, country: any = null) => {
19-
country = country == null && rawValue.startsWith("44") ? "gb" : country;
20-
if (country != null) {
21-
countriesList = countriesList.filter((c) => c[0] === country);
22-
countriesList = countriesList.sort((a, b) => b[2].length - a[2].length);
23-
}
24-
return countriesList.find((c) => rawValue.startsWith(c[2]));
25-
}
26-
27-
const getRawValue = (value: PhoneNumber | string) => {
28-
if (typeof value === "string") return value.replaceAll(/\D/g, "");
29-
return [value?.countryCode, value?.areaCode, value?.phoneNumber].filter(Boolean).join("");
30-
}
31-
32-
const displayFormat = (value: string) => {
33-
return value.replace(/[.\s\D]+$/, "").replace(/(\(\d+)$/, "$1)");
34-
}
35-
36-
const cleanInput = (input: any, pattern: string) => {
37-
input = input.match(/\d/g) || [];
38-
return Array.from(pattern, c => input[0] === c || slots.has(c) ? input.shift() || c : c);
39-
}
40-
41-
const checkValidity = (metadata: PhoneNumber, strict: boolean = false) => {
42-
/** Checks if both the area code and phone number match the validation pattern */
43-
const pattern = (validations as any)[metadata.isoCode as keyof typeof validations][Number(strict)];
44-
return new RegExp(pattern).test([metadata.areaCode, metadata.phoneNumber].filter(Boolean).join(""));
45-
}
46-
47-
const getDefaultISO2Code = () => {
48-
/** Returns the default ISO2 code, based on the user's timezone */
49-
return (timezones[Intl.DateTimeFormat().resolvedOptions().timeZone as keyof typeof timezones] || "") || "us";
50-
}
51-
52-
const parsePhoneNumber = (formattedNumber: string, countriesList: typeof countries = countries, country: any = null): PhoneNumber => {
53-
const value = getRawValue(formattedNumber);
54-
const isoCode = getMetadata(value, countriesList, country)?.[0] || getDefaultISO2Code();
55-
const countryCodePattern = /\+\d+/;
56-
const areaCodePattern = /\((\d+)\)/;
57-
58-
/** Parses the matching partials of the phone number by predefined regex patterns */
59-
const countryCodeMatch = formattedNumber ? (formattedNumber.match(countryCodePattern) || []) : [];
60-
const areaCodeMatch = formattedNumber ? (formattedNumber.match(areaCodePattern) || []) : [];
61-
62-
/** Converts the parsed values of the country and area codes to integers if values present */
63-
const countryCode = countryCodeMatch.length > 0 ? parseInt(countryCodeMatch[0]) : null;
64-
const areaCode = areaCodeMatch.length > 1 ? areaCodeMatch[1] : null;
65-
66-
/** Parses the phone number by removing the country and area codes from the formatted value */
67-
const phoneNumberPattern = new RegExp(`^${countryCode}${(areaCode || "")}(\\d+)`);
68-
const phoneNumberMatch = value ? (value.match(phoneNumberPattern) || []) : [];
69-
const phoneNumber = phoneNumberMatch.length > 1 ? phoneNumberMatch[1] : null;
70-
71-
return {countryCode, areaCode, phoneNumber, isoCode};
72-
}
26+
injectStyles(mergedStyles());
7327

7428
const PhoneInput = ({
7529
value: initialValue = "",
@@ -87,63 +41,30 @@ const PhoneInput = ({
8741
onKeyDown: handleKeyDown = () => null,
8842
...antInputProps
8943
}: PhoneInputProps) => {
90-
const defaultValue = getRawValue(initialValue);
91-
const defaultMetadata = getMetadata(defaultValue) || countries.find(([iso]) => iso === country);
92-
const defaultValueState = defaultValue || countries.find(([iso]) => iso === defaultMetadata?.[0])?.[2] as string;
93-
9444
const formInstance = useFormInstance();
9545
const formContext = useContext(FormContext);
9646
const backRef = useRef<boolean>(false);
9747
const initiatedRef = useRef<boolean>(false);
9848
const [query, setQuery] = useState<string>("");
99-
const [value, setValue] = useState<string>(defaultValueState);
10049
const [minWidth, setMinWidth] = useState<number>(0);
10150
const [countryCode, setCountryCode] = useState<string>(country);
10251

103-
const countriesOnly = useMemo(() => {
104-
const allowList = onlyCountries.length > 0 ? onlyCountries : countries.map(([iso]) => iso);
105-
return countries.map(([iso]) => iso).filter((iso) => {
106-
return allowList.includes(iso) && !excludeCountries.includes(iso);
107-
});
108-
}, [onlyCountries, excludeCountries])
109-
110-
const countriesList = useMemo(() => {
111-
const filteredCountries = countries.filter(([iso, name, _1, dial]) => {
112-
return countriesOnly.includes(iso) && (
113-
name.toLowerCase().startsWith(query.toLowerCase()) || dial.includes(query)
114-
);
115-
});
116-
return [
117-
...filteredCountries.filter(([iso]) => preferredCountries.includes(iso)),
118-
...filteredCountries.filter(([iso]) => !preferredCountries.includes(iso)),
119-
];
120-
}, [countriesOnly, preferredCountries, query])
121-
122-
const metadata = useMemo(() => {
123-
const calculatedMetadata = getMetadata(getRawValue(value), countriesList, countryCode);
124-
if (countriesList.find(([iso]) => iso === calculatedMetadata?.[0] || iso === defaultMetadata?.[0])) {
125-
return calculatedMetadata || defaultMetadata;
126-
}
127-
return countriesList[0];
128-
}, [countriesList, countryCode, defaultMetadata, value])
129-
130-
const pattern = useMemo(() => {
131-
return metadata?.[3] || defaultMetadata?.[3] || "";
132-
}, [defaultMetadata, metadata])
133-
134-
const clean = useCallback((input: any) => {
135-
return cleanInput(input, pattern.replaceAll(/\d/g, "."));
136-
}, [pattern])
137-
138-
const first = useMemo(() => {
139-
return [...pattern].findIndex(c => slots.has(c));
140-
}, [pattern])
141-
142-
const prev = useMemo((j = 0) => {
143-
return Array.from(pattern.replaceAll(/\d/g, "."), (c, i) => {
144-
return slots.has(c) ? j = i + 1 : j;
145-
});
146-
}, [pattern])
52+
const {
53+
clean,
54+
value,
55+
format,
56+
metadata,
57+
setValue,
58+
countriesList,
59+
} = usePhone({
60+
query,
61+
country,
62+
countryCode,
63+
initialValue,
64+
onlyCountries,
65+
excludeCountries,
66+
preferredCountries,
67+
});
14768

14869
const selectValue = useMemo(() => {
14970
let metadata = getMetadata(getRawValue(value), countriesList);
@@ -164,17 +85,6 @@ const PhoneInput = ({
16485
}
16586
}, [antInputProps, formContext, formInstance])
16687

167-
const format = useCallback(({target}: ChangeEvent<HTMLInputElement>) => {
168-
const [i, j] = [target.selectionStart, target.selectionEnd].map((i: any) => {
169-
i = clean(target.value.slice(0, i)).findIndex(c => slots.has(c));
170-
return i < 0 ? prev[prev.length - 1] : backRef.current ? prev[i - 1] || first : i;
171-
});
172-
target.value = displayFormat(clean(target.value).join(""));
173-
target.setSelectionRange(i, j);
174-
backRef.current = false;
175-
setValue(target.value);
176-
}, [clean, first, prev])
177-
17888
const onKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) => {
17989
backRef.current = event.key === "Backspace";
18090
handleKeyDown(event);
@@ -206,16 +116,17 @@ const PhoneInput = ({
206116
const formattedNumber = displayFormat(clean(initialValue).join(""));
207117
const phoneMetadata = parsePhoneNumber(formattedNumber, countriesList);
208118
onMount({...phoneMetadata, valid: (strict: boolean) => checkValidity(phoneMetadata, strict)});
209-
setCountryCode(phoneMetadata.isoCode as keyof typeof validations);
119+
setCountryCode(phoneMetadata.isoCode as any);
210120
setValue(formattedNumber);
211-
}, [clean, countriesList, metadata, onMount, value])
121+
}, [clean, countriesList, metadata, onMount, setValue, value])
212122

213123
const countriesSelect = useMemo(() => (
214124
<Select
215125
suffixIcon={null}
216126
value={selectValue}
217127
open={disableDropdown ? false : undefined}
218-
onSelect={(selectedOption, {key: mask}) => {
128+
onSelect={(selectedOption, {key}) => {
129+
const [_, mask] = key.split("_");
219130
if (selectValue === selectedOption) return;
220131
const selectedCountryCode = selectedOption.slice(0, 2);
221132
const formattedNumber = displayFormat(cleanInput(mask, mask).join(""));
@@ -241,8 +152,8 @@ const PhoneInput = ({
241152
>
242153
{countriesList.map(([iso, name, dial, mask]) => (
243154
<Select.Option
244-
key={iso + mask}
245155
value={iso + dial}
156+
key={`${iso}_${mask}`}
246157
label={<div className={`flag ${iso}`}/>}
247158
children={<div className="ant-phone-input-select-item">
248159
<div className={`flag ${iso}`}/>
@@ -251,7 +162,7 @@ const PhoneInput = ({
251162
/>
252163
))}
253164
</Select>
254-
), [selectValue, disableDropdown, minWidth, searchNotFound, countriesList, setFieldValue, enableSearch, searchPlaceholder])
165+
), [selectValue, disableDropdown, minWidth, searchNotFound, countriesList, setFieldValue, setValue, enableSearch, searchPlaceholder])
255166

256167
return (
257168
<div className="ant-phone-input-wrapper"

0 commit comments

Comments
 (0)