@@ -4,72 +4,22 @@ import {FormContext} from "antd/es/form/context";
44import Select from "antd/es/select" ;
55import Input from "antd/es/input" ;
66
7+ import {
8+ checkValidity ,
9+ cleanInput ,
10+ displayFormat ,
11+ getCountry ,
12+ getDefaultISO2Code ,
13+ getMetadata ,
14+ getRawValue ,
15+ parsePhoneNumber ,
16+ usePhone ,
17+ } from "react-phone-hooks" ;
18+
19+ import { injectMergedStyles } from "./styles" ;
720import { PhoneInputProps , PhoneNumber } from "./types" ;
821
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" ;
13-
14- styleInject ( "styles.css" ) ;
15-
16- const slots = new Set ( "." ) ;
17-
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- }
22+ injectMergedStyles ( ) ;
7323
7424const PhoneInput = ( {
7525 value : initialValue = "" ,
@@ -87,67 +37,34 @@ const PhoneInput = ({
8737 onKeyDown : handleKeyDown = ( ) => null ,
8838 ...antInputProps
8939 } : 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-
9440 const formInstance = useFormInstance ( ) ;
9541 const formContext = useContext ( FormContext ) ;
9642 const backRef = useRef < boolean > ( false ) ;
9743 const initiatedRef = useRef < boolean > ( false ) ;
9844 const [ query , setQuery ] = useState < string > ( "" ) ;
99- const [ value , setValue ] = useState < string > ( defaultValueState ) ;
10045 const [ minWidth , setMinWidth ] = useState < number > ( 0 ) ;
10146 const [ countryCode , setCountryCode ] = useState < string > ( country ) ;
10247
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 ] )
48+ const {
49+ clean,
50+ value,
51+ format,
52+ metadata,
53+ setValue,
54+ countriesList,
55+ } = usePhone ( {
56+ query,
57+ country,
58+ countryCode,
59+ initialValue,
60+ onlyCountries,
61+ excludeCountries,
62+ preferredCountries,
63+ } ) ;
14764
14865 const selectValue = useMemo ( ( ) => {
14966 let metadata = getMetadata ( getRawValue ( value ) , countriesList ) ;
150- metadata = metadata || countries . find ( ( [ iso ] ) => iso === countryCode ) ;
67+ metadata = metadata || getCountry ( countryCode as any ) ;
15168 return ( { ...metadata } ) ?. [ 0 ] + ( { ...metadata } ) ?. [ 2 ] ;
15269 } , [ countriesList , countryCode , value ] )
15370
@@ -164,17 +81,6 @@ const PhoneInput = ({
16481 }
16582 } , [ antInputProps , formContext , formInstance ] )
16683
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-
17884 const onKeyDown = useCallback ( ( event : KeyboardEvent < HTMLInputElement > ) => {
17985 backRef . current = event . key === "Backspace" ;
18086 handleKeyDown ( event ) ;
@@ -206,16 +112,17 @@ const PhoneInput = ({
206112 const formattedNumber = displayFormat ( clean ( initialValue ) . join ( "" ) ) ;
207113 const phoneMetadata = parsePhoneNumber ( formattedNumber , countriesList ) ;
208114 onMount ( { ...phoneMetadata , valid : ( strict : boolean ) => checkValidity ( phoneMetadata , strict ) } ) ;
209- setCountryCode ( phoneMetadata . isoCode as keyof typeof validations ) ;
115+ setCountryCode ( phoneMetadata . isoCode as any ) ;
210116 setValue ( formattedNumber ) ;
211- } , [ clean , countriesList , metadata , onMount , value ] )
117+ } , [ clean , countriesList , metadata , onMount , setValue , value ] )
212118
213119 const countriesSelect = useMemo ( ( ) => (
214120 < Select
215121 suffixIcon = { null }
216122 value = { selectValue }
217123 open = { disableDropdown ? false : undefined }
218- onSelect = { ( selectedOption , { key : mask } ) => {
124+ onSelect = { ( selectedOption , { key} ) => {
125+ const [ _ , mask ] = key . split ( "_" ) ;
219126 if ( selectValue === selectedOption ) return ;
220127 const selectedCountryCode = selectedOption . slice ( 0 , 2 ) ;
221128 const formattedNumber = displayFormat ( cleanInput ( mask , mask ) . join ( "" ) ) ;
@@ -241,8 +148,8 @@ const PhoneInput = ({
241148 >
242149 { countriesList . map ( ( [ iso , name , dial , mask ] ) => (
243150 < Select . Option
244- key = { iso + mask }
245151 value = { iso + dial }
152+ key = { `${ iso } _${ mask } ` }
246153 label = { < div className = { `flag ${ iso } ` } /> }
247154 children = { < div className = "ant-phone-input-select-item" >
248155 < div className = { `flag ${ iso } ` } />
@@ -251,7 +158,7 @@ const PhoneInput = ({
251158 />
252159 ) ) }
253160 </ Select >
254- ) , [ selectValue , disableDropdown , minWidth , searchNotFound , countriesList , setFieldValue , enableSearch , searchPlaceholder ] )
161+ ) , [ selectValue , disableDropdown , minWidth , searchNotFound , countriesList , setFieldValue , setValue , enableSearch , searchPlaceholder ] )
255162
256163 return (
257164 < div className = "ant-phone-input-wrapper"
0 commit comments