1
+ "use client" ;
2
+
1
3
import {
2
4
ChangeEvent ,
3
5
forwardRef ,
@@ -10,7 +12,9 @@ import {
10
12
useState
11
13
} from "react" ;
12
14
import useFormInstance from "antd/es/form/hooks/useFormInstance" ;
15
+ import { ConfigContext } from "antd/es/config-provider" ;
13
16
import { FormContext } from "antd/es/form/context" ;
17
+ import { useWatch } from "antd/es/form/Form" ;
14
18
import Select from "antd/es/select" ;
15
19
import Input from "antd/es/input" ;
16
20
@@ -31,18 +35,19 @@ import {
31
35
import { injectMergedStyles } from "./styles" ;
32
36
import { PhoneInputProps , PhoneNumber } from "./types" ;
33
37
34
- injectMergedStyles ( ) ;
35
-
36
38
const PhoneInput = forwardRef ( ( {
37
39
value : initialValue = "" ,
38
40
country = getDefaultISO2Code ( ) ,
41
+ disabled = false ,
39
42
enableSearch = false ,
40
43
disableDropdown = false ,
44
+ disableParentheses = false ,
41
45
onlyCountries = [ ] ,
42
46
excludeCountries = [ ] ,
43
47
preferredCountries = [ ] ,
44
48
searchNotFound = "No country found" ,
45
49
searchPlaceholder = "Search country" ,
50
+ dropdownRender = ( node ) => node ,
46
51
onMount : handleMount = ( ) => null ,
47
52
onInput : handleInput = ( ) => null ,
48
53
onChange : handleChange = ( ) => null ,
@@ -51,13 +56,18 @@ const PhoneInput = forwardRef(({
51
56
} : PhoneInputProps , forwardedRef : any ) => {
52
57
const formInstance = useFormInstance ( ) ;
53
58
const formContext = useContext ( FormContext ) ;
59
+ const { getPrefixCls} = useContext ( ConfigContext ) ;
54
60
const inputRef = useRef < any > ( null ) ;
61
+ const searchRef = useRef < any > ( null ) ;
55
62
const selectedRef = useRef < boolean > ( false ) ;
56
63
const initiatedRef = useRef < boolean > ( false ) ;
57
64
const [ query , setQuery ] = useState < string > ( "" ) ;
58
65
const [ minWidth , setMinWidth ] = useState < number > ( 0 ) ;
59
66
const [ countryCode , setCountryCode ] = useState < string > ( country ) ;
60
67
68
+ const prefixCls = getPrefixCls ( ) ;
69
+ injectMergedStyles ( prefixCls ) ;
70
+
61
71
const {
62
72
value,
63
73
pattern,
@@ -72,6 +82,7 @@ const PhoneInput = forwardRef(({
72
82
onlyCountries,
73
83
excludeCountries,
74
84
preferredCountries,
85
+ disableParentheses,
75
86
} ) ;
76
87
77
88
const {
@@ -85,18 +96,22 @@ const PhoneInput = forwardRef(({
85
96
return ( { ...metadata } ) ?. [ 0 ] + ( { ...metadata } ) ?. [ 2 ] ;
86
97
} , [ countriesList , countryCode , value ] )
87
98
88
- const setFieldValue = useCallback ( ( value : PhoneNumber ) => {
89
- if ( formInstance ) {
90
- let namePath = [ ] ;
91
- let formName = ( formContext as any ) ?. name || "" ;
92
- let fieldName = ( antInputProps as any ) ?. id || "" ;
93
- if ( formName ) {
94
- namePath . push ( formName ) ;
95
- fieldName = fieldName . slice ( formName . length + 1 ) ;
96
- }
97
- formInstance . setFieldValue ( namePath . concat ( fieldName . split ( "_" ) ) , value ) ;
99
+ const namePath = useMemo ( ( ) => {
100
+ let path = [ ] ;
101
+ let formName = ( formContext as any ) ?. name || "" ;
102
+ let fieldName = ( antInputProps as any ) ?. id || "" ;
103
+ if ( formName ) {
104
+ path . push ( formName ) ;
105
+ fieldName = fieldName . slice ( formName . length + 1 ) ;
98
106
}
99
- } , [ antInputProps , formContext , formInstance ] )
107
+ return path . concat ( fieldName . split ( "_" ) ) ;
108
+ } , [ antInputProps , formContext ] )
109
+
110
+ const phoneValue = useWatch ( namePath , formInstance ) ;
111
+
112
+ const setFieldValue = useCallback ( ( value : PhoneNumber ) => {
113
+ if ( formInstance ) formInstance . setFieldValue ( namePath , value ) ;
114
+ } , [ formInstance , namePath ] )
100
115
101
116
const onKeyDown = useCallback ( ( event : KeyboardEvent < HTMLInputElement > ) => {
102
117
onKeyDownMaskHandler ( event ) ;
@@ -122,13 +137,29 @@ const PhoneInput = forwardRef(({
122
137
handleMount ( value ) ;
123
138
} , [ handleMount , setFieldValue ] )
124
139
140
+ const onDropdownVisibleChange = useCallback ( ( open : boolean ) => {
141
+ if ( open && enableSearch ) setTimeout ( ( ) => searchRef . current . focus ( ) , 100 ) ;
142
+ } , [ enableSearch ] )
143
+
125
144
const ref = useCallback ( ( node : any ) => {
126
145
[ forwardedRef , inputRef ] . forEach ( ( ref ) => {
127
146
if ( typeof ref === "function" ) ref ( node ) ;
128
147
else if ( ref != null ) ref . current = node ;
129
148
} )
130
149
} , [ forwardedRef ] )
131
150
151
+ useEffect ( ( ) => {
152
+ const rawValue = getRawValue ( phoneValue ) ;
153
+ const metadata = getMetadata ( rawValue ) ;
154
+ // Skip if value has not been updated by `setFieldValue`.
155
+ if ( ! metadata ?. [ 3 ] || rawValue === getRawValue ( value ) ) return ;
156
+ const formattedNumber = getFormattedNumber ( rawValue , metadata ?. [ 3 ] as string ) ;
157
+ const phoneMetadata = parsePhoneNumber ( formattedNumber ) ;
158
+ setFieldValue ( { ...phoneMetadata , valid : ( strict : boolean ) => checkValidity ( phoneMetadata , strict ) } ) ;
159
+ setCountryCode ( metadata ?. [ 0 ] as string ) ;
160
+ setValue ( formattedNumber ) ;
161
+ } , [ phoneValue , value , setFieldValue , setValue ] )
162
+
132
163
useEffect ( ( ) => {
133
164
if ( initiatedRef . current ) return ;
134
165
initiatedRef . current = true ;
@@ -147,28 +178,33 @@ const PhoneInput = forwardRef(({
147
178
< Select
148
179
suffixIcon = { null }
149
180
value = { selectValue }
181
+ disabled = { disabled }
150
182
open = { disableDropdown ? false : undefined }
151
183
onSelect = { ( selectedOption , { key} ) => {
152
184
const [ _ , mask ] = key . split ( "_" ) ;
153
- if ( selectValue === selectedOption ) return ;
154
185
const selectedCountryCode = selectedOption . slice ( 0 , 2 ) ;
155
186
const formattedNumber = displayFormat ( cleanInput ( mask , mask ) . join ( "" ) ) ;
156
187
const phoneMetadata = parsePhoneNumber ( formattedNumber , countriesList , selectedCountryCode ) ;
157
188
setFieldValue ( { ...phoneMetadata , valid : ( strict : boolean ) => checkValidity ( phoneMetadata , strict ) } ) ;
158
189
setCountryCode ( selectedCountryCode ) ;
159
190
setValue ( formattedNumber ) ;
191
+ setQuery ( "" ) ;
160
192
selectedRef . current = true ;
161
193
const nativeInputValueSetter = ( Object . getOwnPropertyDescriptor ( HTMLInputElement . prototype , "value" ) as any ) . set ;
162
194
nativeInputValueSetter . call ( inputRef . current . input , formattedNumber ) ;
163
195
inputRef . current . input . dispatchEvent ( new Event ( "change" , { bubbles : true } ) ) ;
196
+ inputRef . current . input . focus ( ) ;
164
197
} }
165
198
optionLabelProp = "label"
166
199
dropdownStyle = { { minWidth} }
167
200
notFoundContent = { searchNotFound }
201
+ onDropdownVisibleChange = { onDropdownVisibleChange }
168
202
dropdownRender = { ( menu ) => (
169
- < div className = "ant -phone-input-search-wrapper" >
203
+ < div className = { ` ${ prefixCls } -phone-input-search-wrapper` } >
170
204
{ enableSearch && (
171
205
< Input
206
+ value = { query }
207
+ ref = { searchRef }
172
208
placeholder = { searchPlaceholder }
173
209
onInput = { ( { target} : any ) => setQuery ( target . value ) }
174
210
/>
@@ -177,22 +213,25 @@ const PhoneInput = forwardRef(({
177
213
</ div >
178
214
) }
179
215
>
180
- { countriesList . map ( ( [ iso , name , dial , mask ] ) => (
181
- < Select . Option
182
- value = { iso + dial }
183
- key = { `${ iso } _${ mask } ` }
184
- label = { < div className = { `flag ${ iso } ` } /> }
185
- children = { < div className = "ant-phone-input-select-item" >
186
- < div className = { `flag ${ iso } ` } />
187
- { name } { displayFormat ( mask ) }
188
- </ div > }
189
- />
190
- ) ) }
216
+ { countriesList . map ( ( [ iso , name , dial , pattern ] ) => {
217
+ const mask = disableParentheses ? pattern . replace ( / [ ( ) ] / g, "" ) : pattern ;
218
+ return (
219
+ < Select . Option
220
+ value = { iso + dial }
221
+ key = { `${ iso } _${ mask } ` }
222
+ label = { < div className = { `flag ${ iso } ` } /> }
223
+ children = { < div className = { `${ prefixCls } -phone-input-select-item` } >
224
+ < div className = { `flag ${ iso } ` } />
225
+ { name } { displayFormat ( mask ) }
226
+ </ div > }
227
+ />
228
+ )
229
+ } ) }
191
230
</ Select >
192
- ) , [ selectValue , disableDropdown , minWidth , searchNotFound , countriesList , setFieldValue , setValue , enableSearch , searchPlaceholder ] )
231
+ ) , [ selectValue , query , disabled , disableParentheses , disableDropdown , onDropdownVisibleChange , minWidth , searchNotFound , countriesList , setFieldValue , setValue , prefixCls , enableSearch , searchPlaceholder ] )
193
232
194
233
return (
195
- < div className = "ant -phone-input-wrapper"
234
+ < div className = { ` ${ prefixCls } -phone-input-wrapper` }
196
235
ref = { node => setMinWidth ( node ?. offsetWidth || 0 ) } >
197
236
< Input
198
237
ref = { ref }
@@ -201,7 +240,8 @@ const PhoneInput = forwardRef(({
201
240
onInput = { onInput }
202
241
onChange = { onChange }
203
242
onKeyDown = { onKeyDown }
204
- addonBefore = { countriesSelect }
243
+ addonBefore = { dropdownRender ( countriesSelect ) }
244
+ disabled = { disabled }
205
245
{ ...antInputProps }
206
246
/>
207
247
</ div >
0 commit comments