react-native-country-select 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +7 -7
- package/README.md +319 -296
- package/lib/assets/images/preview.png +0 -0
- package/lib/components/CountryItem/index.tsx +59 -41
- package/lib/components/CountrySelect/index.tsx +319 -159
- package/lib/components/index.ts +2 -2
- package/lib/components/styles.ts +98 -83
- package/lib/index.d.ts +23 -21
- package/lib/index.tsx +18 -18
- package/lib/interface/country.ts +111 -111
- package/lib/interface/countryCca2.ts +251 -251
- package/lib/interface/countryItemProps.ts +12 -10
- package/lib/interface/countrySelectLanguages.ts +34 -34
- package/lib/interface/countrySelectProps.ts +31 -11
- package/lib/interface/countrySelectStyles.ts +28 -0
- package/lib/interface/index.ts +1 -2
- package/lib/interface/itemList.ts +4 -0
- package/lib/interface/sectionTitle.ts +4 -0
- package/lib/utils/getTranslation.ts +74 -1
- package/package.json +42 -42
@@ -1,41 +1,59 @@
|
|
1
|
-
import React, {memo} from 'react';
|
2
|
-
import {View, Text, TouchableOpacity} from 'react-native';
|
3
|
-
|
4
|
-
import {createStyles} from '../styles';
|
5
|
-
import {ICountryItemProps, ICountrySelectLanguages} from '../../interface';
|
6
|
-
|
7
|
-
const DEFAULT_LANGUAGE: ICountrySelectLanguages = 'eng';
|
8
|
-
|
9
|
-
export const CountryItem = memo<ICountryItemProps>(
|
10
|
-
({
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
1
|
+
import React, {memo} from 'react';
|
2
|
+
import {View, Text, TouchableOpacity} from 'react-native';
|
3
|
+
|
4
|
+
import {createStyles} from '../styles';
|
5
|
+
import {ICountryItemProps, ICountrySelectLanguages} from '../../interface';
|
6
|
+
|
7
|
+
const DEFAULT_LANGUAGE: ICountrySelectLanguages = 'eng';
|
8
|
+
|
9
|
+
export const CountryItem = memo<ICountryItemProps>(
|
10
|
+
({
|
11
|
+
item,
|
12
|
+
onSelect,
|
13
|
+
onClose,
|
14
|
+
theme = 'light',
|
15
|
+
language,
|
16
|
+
countrySelectStyle,
|
17
|
+
}) => {
|
18
|
+
const styles = createStyles(theme);
|
19
|
+
|
20
|
+
return (
|
21
|
+
<TouchableOpacity
|
22
|
+
testID="countrySelectItem"
|
23
|
+
accessibilityRole="button"
|
24
|
+
accessibilityLabel="Country Select Item"
|
25
|
+
accessibilityHint="Click to select a country"
|
26
|
+
style={[styles.countryItem, countrySelectStyle?.popup?.countryItem]}
|
27
|
+
onPress={() => {
|
28
|
+
onSelect(item);
|
29
|
+
onClose();
|
30
|
+
}}>
|
31
|
+
<Text
|
32
|
+
testID="countrySelectItemFlag"
|
33
|
+
style={[styles.flag, countrySelectStyle?.popup?.flag]}>
|
34
|
+
{item.flag}
|
35
|
+
</Text>
|
36
|
+
<View
|
37
|
+
style={[styles.countryInfo, countrySelectStyle?.popup?.countryInfo]}>
|
38
|
+
<Text
|
39
|
+
testID="countrySelectItemCallingCode"
|
40
|
+
style={[
|
41
|
+
styles.callingCode,
|
42
|
+
countrySelectStyle?.popup?.callingCode,
|
43
|
+
]}>
|
44
|
+
{item.idd.root}
|
45
|
+
</Text>
|
46
|
+
<Text
|
47
|
+
testID="countrySelectItemName"
|
48
|
+
style={[
|
49
|
+
styles.countryName,
|
50
|
+
countrySelectStyle?.popup?.countryName,
|
51
|
+
]}>
|
52
|
+
{item?.translations[language]?.common ||
|
53
|
+
item?.translations[DEFAULT_LANGUAGE]?.common}
|
54
|
+
</Text>
|
55
|
+
</View>
|
56
|
+
</TouchableOpacity>
|
57
|
+
);
|
58
|
+
},
|
59
|
+
);
|
@@ -1,159 +1,319 @@
|
|
1
|
-
/* eslint-disable react-native/no-inline-styles */
|
2
|
-
/* eslint-disable react-hooks/exhaustive-deps */
|
3
|
-
import React, {useCallback, useMemo, useState} from 'react';
|
4
|
-
import {
|
5
|
-
View,
|
6
|
-
TextInput,
|
7
|
-
FlatList,
|
8
|
-
Pressable,
|
9
|
-
ListRenderItem,
|
10
|
-
Modal,
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
import {
|
16
|
-
|
17
|
-
import {
|
18
|
-
import
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
const
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
};
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
1
|
+
/* eslint-disable react-native/no-inline-styles */
|
2
|
+
/* eslint-disable react-hooks/exhaustive-deps */
|
3
|
+
import React, {useCallback, useMemo, useState} from 'react';
|
4
|
+
import {
|
5
|
+
View,
|
6
|
+
TextInput,
|
7
|
+
FlatList,
|
8
|
+
Pressable,
|
9
|
+
ListRenderItem,
|
10
|
+
Modal,
|
11
|
+
Text,
|
12
|
+
TouchableOpacity,
|
13
|
+
} from 'react-native';
|
14
|
+
|
15
|
+
import {CountryItem} from '../CountryItem';
|
16
|
+
|
17
|
+
import {createStyles} from '../styles';
|
18
|
+
import countries from '../../constants/countries.json';
|
19
|
+
import {translations} from '../../utils/getTranslation';
|
20
|
+
import {
|
21
|
+
ICountry,
|
22
|
+
ICountrySelectProps,
|
23
|
+
ICountrySelectLanguages,
|
24
|
+
IListItem,
|
25
|
+
} from '../../interface';
|
26
|
+
|
27
|
+
const DEFAULT_LANGUAGE: ICountrySelectLanguages = 'eng';
|
28
|
+
|
29
|
+
export const CountrySelect: React.FC<ICountrySelectProps> = ({
|
30
|
+
visible,
|
31
|
+
onClose,
|
32
|
+
onSelect,
|
33
|
+
theme = 'light',
|
34
|
+
isFullScreen = false,
|
35
|
+
countrySelectStyle,
|
36
|
+
popularCountries = [],
|
37
|
+
visibleCountries = [],
|
38
|
+
hiddenCountries = [],
|
39
|
+
language = DEFAULT_LANGUAGE,
|
40
|
+
showSearchInput = true,
|
41
|
+
searchPlaceholder,
|
42
|
+
disabledBackdropPress,
|
43
|
+
removedBackdrop,
|
44
|
+
onBackdropPress,
|
45
|
+
sectionTitleComponent,
|
46
|
+
countryItemComponent,
|
47
|
+
popularCountriesTitle,
|
48
|
+
allCountriesTitle,
|
49
|
+
showsVerticalScrollIndicator = false,
|
50
|
+
...props
|
51
|
+
}) => {
|
52
|
+
const styles = createStyles(theme);
|
53
|
+
|
54
|
+
const [searchQuery, setSearchQuery] = useState('');
|
55
|
+
|
56
|
+
// Obtain the country name in the selected language
|
57
|
+
const getCountryNameInLanguage = (country: ICountry): string => {
|
58
|
+
return (
|
59
|
+
country.translations[language]?.common ||
|
60
|
+
country.translations[DEFAULT_LANGUAGE]?.common ||
|
61
|
+
country.name.common ||
|
62
|
+
''
|
63
|
+
);
|
64
|
+
};
|
65
|
+
|
66
|
+
// Normalize country name and remove accents
|
67
|
+
const normalizeCountryName = (str: string) =>
|
68
|
+
str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
69
|
+
|
70
|
+
const sortCountriesAlphabetically = (
|
71
|
+
countriesList: ICountry[],
|
72
|
+
): ICountry[] => {
|
73
|
+
return [...countriesList].sort((a, b) => {
|
74
|
+
const nameA = normalizeCountryName(
|
75
|
+
getCountryNameInLanguage(a).toLowerCase(),
|
76
|
+
);
|
77
|
+
const nameB = normalizeCountryName(
|
78
|
+
getCountryNameInLanguage(b).toLowerCase(),
|
79
|
+
);
|
80
|
+
return nameA.localeCompare(nameB);
|
81
|
+
});
|
82
|
+
};
|
83
|
+
|
84
|
+
const getCountries = useMemo(() => {
|
85
|
+
const query = searchQuery.toLowerCase();
|
86
|
+
|
87
|
+
if (query.length > 0) {
|
88
|
+
let countriesData = countries as unknown as ICountry[];
|
89
|
+
|
90
|
+
if (visibleCountries.length > 0) {
|
91
|
+
countriesData = (countries as unknown as ICountry[]).filter(country =>
|
92
|
+
visibleCountries.includes(country.cca2),
|
93
|
+
);
|
94
|
+
}
|
95
|
+
|
96
|
+
if (hiddenCountries.length > 0) {
|
97
|
+
countriesData = (countries as unknown as ICountry[]).filter(
|
98
|
+
country => !hiddenCountries.includes(country.cca2),
|
99
|
+
);
|
100
|
+
}
|
101
|
+
|
102
|
+
const filteredCountries = countriesData.filter(country => {
|
103
|
+
const countryName = getCountryNameInLanguage(country);
|
104
|
+
const normalizedCountryName = normalizeCountryName(
|
105
|
+
countryName.toLowerCase(),
|
106
|
+
);
|
107
|
+
const normalizedQuery = normalizeCountryName(query);
|
108
|
+
const callingCode = country.idd.root.toLowerCase();
|
109
|
+
const flag = country.flag.toLowerCase();
|
110
|
+
const countryCode = country.cca2.toLowerCase();
|
111
|
+
|
112
|
+
return (
|
113
|
+
normalizedCountryName.includes(normalizedQuery) ||
|
114
|
+
countryName.toLowerCase().includes(query) ||
|
115
|
+
callingCode.includes(query) ||
|
116
|
+
flag.includes(query) ||
|
117
|
+
countryCode.includes(query)
|
118
|
+
);
|
119
|
+
});
|
120
|
+
|
121
|
+
return sortCountriesAlphabetically(filteredCountries);
|
122
|
+
}
|
123
|
+
|
124
|
+
let allCountries = countries as unknown as ICountry[];
|
125
|
+
|
126
|
+
if (visibleCountries.length > 0) {
|
127
|
+
allCountries = (countries as unknown as ICountry[]).filter(country =>
|
128
|
+
visibleCountries.includes(country.cca2),
|
129
|
+
);
|
130
|
+
}
|
131
|
+
|
132
|
+
if (hiddenCountries.length > 0) {
|
133
|
+
allCountries = (countries as unknown as ICountry[]).filter(
|
134
|
+
country => !hiddenCountries.includes(country.cca2),
|
135
|
+
);
|
136
|
+
}
|
137
|
+
|
138
|
+
const popularCountriesData = sortCountriesAlphabetically(
|
139
|
+
allCountries.filter(country => popularCountries.includes(country.cca2)),
|
140
|
+
);
|
141
|
+
|
142
|
+
const otherCountriesData = sortCountriesAlphabetically(
|
143
|
+
allCountries.filter(country => !popularCountries.includes(country.cca2)),
|
144
|
+
);
|
145
|
+
|
146
|
+
const result: IListItem[] = [];
|
147
|
+
|
148
|
+
if (popularCountriesData.length > 0) {
|
149
|
+
result.push({
|
150
|
+
isSection: true as const,
|
151
|
+
title:
|
152
|
+
translations.popularCountriesTitle[
|
153
|
+
language as ICountrySelectLanguages
|
154
|
+
],
|
155
|
+
});
|
156
|
+
result.push(...popularCountriesData);
|
157
|
+
result.push({
|
158
|
+
isSection: true as const,
|
159
|
+
title:
|
160
|
+
translations.allCountriesTitle[language as ICountrySelectLanguages],
|
161
|
+
});
|
162
|
+
}
|
163
|
+
|
164
|
+
result.push(...otherCountriesData);
|
165
|
+
|
166
|
+
return result;
|
167
|
+
}, [
|
168
|
+
searchQuery,
|
169
|
+
popularCountries,
|
170
|
+
language,
|
171
|
+
visibleCountries,
|
172
|
+
hiddenCountries,
|
173
|
+
]);
|
174
|
+
|
175
|
+
const keyExtractor = useCallback(
|
176
|
+
(item: IListItem) => ('isSection' in item ? item.title : item.cca2),
|
177
|
+
[],
|
178
|
+
);
|
179
|
+
|
180
|
+
const renderItem: ListRenderItem<IListItem> = useCallback(
|
181
|
+
({item, index}) => {
|
182
|
+
if ('isSection' in item) {
|
183
|
+
if (sectionTitleComponent) {
|
184
|
+
return sectionTitleComponent(item);
|
185
|
+
}
|
186
|
+
return (
|
187
|
+
<Text
|
188
|
+
testID="countrySelectSectionTitle"
|
189
|
+
accessibilityRole="header"
|
190
|
+
style={[
|
191
|
+
styles.sectionTitle,
|
192
|
+
countrySelectStyle?.popup?.sectionTitle,
|
193
|
+
]}>
|
194
|
+
{popularCountriesTitle && index === 0
|
195
|
+
? popularCountriesTitle
|
196
|
+
: allCountriesTitle && index > 0
|
197
|
+
? allCountriesTitle
|
198
|
+
: item.title}
|
199
|
+
</Text>
|
200
|
+
);
|
201
|
+
}
|
202
|
+
|
203
|
+
if (countryItemComponent) {
|
204
|
+
return countryItemComponent(item as ICountry);
|
205
|
+
}
|
206
|
+
|
207
|
+
return (
|
208
|
+
<CountryItem
|
209
|
+
item={item as ICountry}
|
210
|
+
onSelect={onSelect}
|
211
|
+
onClose={onClose}
|
212
|
+
theme={theme}
|
213
|
+
language={language}
|
214
|
+
countrySelectStyle={countrySelectStyle}
|
215
|
+
/>
|
216
|
+
);
|
217
|
+
},
|
218
|
+
[
|
219
|
+
onSelect,
|
220
|
+
onClose,
|
221
|
+
styles,
|
222
|
+
language,
|
223
|
+
countryItemComponent,
|
224
|
+
sectionTitleComponent,
|
225
|
+
],
|
226
|
+
);
|
227
|
+
|
228
|
+
return (
|
229
|
+
<Modal
|
230
|
+
visible={visible}
|
231
|
+
transparent
|
232
|
+
animationType="fade"
|
233
|
+
onRequestClose={onClose}
|
234
|
+
statusBarTranslucent
|
235
|
+
{...props}>
|
236
|
+
<Pressable
|
237
|
+
style={[
|
238
|
+
styles.backdrop,
|
239
|
+
{alignItems: 'center', justifyContent: 'center'},
|
240
|
+
countrySelectStyle?.popup?.backdrop,
|
241
|
+
removedBackdrop && {backgroundColor: 'transparent'},
|
242
|
+
]}
|
243
|
+
disabled={disabledBackdropPress || removedBackdrop}
|
244
|
+
onPress={onBackdropPress || onClose}>
|
245
|
+
<Pressable
|
246
|
+
style={[
|
247
|
+
styles.popupContainer,
|
248
|
+
countrySelectStyle?.popup?.popupContainer,
|
249
|
+
isFullScreen && {flex: 1, width: '100%', height: '100%'},
|
250
|
+
]}>
|
251
|
+
<View
|
252
|
+
style={[
|
253
|
+
styles.popupContent,
|
254
|
+
countrySelectStyle?.popup?.popupContent,
|
255
|
+
]}>
|
256
|
+
<View
|
257
|
+
style={[
|
258
|
+
styles.searchContainer,
|
259
|
+
countrySelectStyle?.popup?.searchContainer,
|
260
|
+
]}>
|
261
|
+
{isFullScreen && (
|
262
|
+
<TouchableOpacity
|
263
|
+
style={[
|
264
|
+
styles.closeButton,
|
265
|
+
countrySelectStyle?.popup?.closeButton,
|
266
|
+
]}
|
267
|
+
activeOpacity={0.6}
|
268
|
+
onPress={onClose}>
|
269
|
+
<Text
|
270
|
+
style={[
|
271
|
+
styles.closeButtonText,
|
272
|
+
countrySelectStyle?.popup?.closeButtonText,
|
273
|
+
]}>
|
274
|
+
{'\u00D7'}
|
275
|
+
</Text>
|
276
|
+
</TouchableOpacity>
|
277
|
+
)}
|
278
|
+
{showSearchInput && (
|
279
|
+
<TextInput
|
280
|
+
testID="countrySelectSearchInput"
|
281
|
+
accessibilityRole="text"
|
282
|
+
accessibilityLabel="Country Select Search Input"
|
283
|
+
accessibilityHint="Type to search for a country"
|
284
|
+
style={[
|
285
|
+
styles.searchInput,
|
286
|
+
countrySelectStyle?.popup?.searchInput,
|
287
|
+
]}
|
288
|
+
placeholder={
|
289
|
+
searchPlaceholder ||
|
290
|
+
translations.searchPlaceholder[
|
291
|
+
language as ICountrySelectLanguages
|
292
|
+
]
|
293
|
+
}
|
294
|
+
placeholderTextColor={styles.searchInputPlaceholder.color}
|
295
|
+
value={searchQuery}
|
296
|
+
onChangeText={setSearchQuery}
|
297
|
+
/>
|
298
|
+
)}
|
299
|
+
</View>
|
300
|
+
|
301
|
+
<FlatList
|
302
|
+
testID="countrySelectList"
|
303
|
+
accessibilityRole="list"
|
304
|
+
accessibilityLabel="Country Select List"
|
305
|
+
accessibilityHint="List of countries"
|
306
|
+
data={getCountries}
|
307
|
+
keyExtractor={keyExtractor}
|
308
|
+
renderItem={renderItem}
|
309
|
+
keyboardShouldPersistTaps="handled"
|
310
|
+
showsVerticalScrollIndicator={
|
311
|
+
showsVerticalScrollIndicator || false
|
312
|
+
}
|
313
|
+
/>
|
314
|
+
</View>
|
315
|
+
</Pressable>
|
316
|
+
</Pressable>
|
317
|
+
</Modal>
|
318
|
+
);
|
319
|
+
};
|
package/lib/components/index.ts
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
export {CountrySelect} from './CountrySelect';
|
2
|
-
export {CountryItem} from './CountryItem';
|
1
|
+
export {CountrySelect} from './CountrySelect';
|
2
|
+
export {CountryItem} from './CountryItem';
|