react-native-country-select 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ISC License
2
+
3
+ Copyright 2025-present Willian Barbosa Lima do Nascimento
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,296 @@
1
+ <h1 align="center">React Native Country Select</h1>
2
+
3
+ <p>
4
+ 🌍 A lightweight and customizable country picker for React Native with modern UI, flags, search engine, and i18n support. Includes TypeScript types, offline support and no dependencies.
5
+ </p>
6
+
7
+ <br>
8
+
9
+ <div align="center">
10
+ <a href="https://www.npmjs.com/package/react-native-country-select">
11
+ <img src="https://img.shields.io/npm/v/react-native-country-select.svg?style=flat-square">
12
+ </a>
13
+ <a href="https://www.npmjs.com/package/react-native-country-select">
14
+ <img src="https://img.shields.io/npm/dt/react-native-country-select.svg?style=flat-square&color=success">
15
+ </a>
16
+ <a href="https://github.com/AstrOOnauta/react-native-country-select">
17
+ <img src="https://img.shields.io/github/stars/AstrOOnauta/react-native-country-select?style=flat-square&color=success"/>
18
+ </a>
19
+ <a href="https://github.com/AstrOOnauta/react-native-country-select/issues">
20
+ <img src="https://img.shields.io/github/issues/AstrOOnauta/react-native-country-select?style=flat-square&color=blue"/>
21
+ </a>
22
+ <a href="https://github.com/AstrOOnauta/react-native-country-select/pulls">
23
+ <img src="https://img.shields.io/github/issues-pr/AstrOOnauta/react-native-country-select?style=flat-square&color=blue"/>
24
+ </a>
25
+ <a href="LICENSE.md">
26
+ <img src="https://img.shields.io/:license-isc-yellow.svg?style=flat-square"/>
27
+ </a>
28
+ </div>
29
+
30
+ <br>
31
+
32
+ <div align="center">
33
+ <a href="https://www.buymeacoffee.com/astroonauta" target="_blank">
34
+ <img src="https://survivingmexico.files.wordpress.com/2018/07/button-gif.gif" alt="Buy Me A Coffee" style="height: auto !important;width: 60% !important;">
35
+ </a>
36
+ </div>
37
+
38
+ <br>
39
+
40
+ ## Features
41
+
42
+ - 📱 Cross-platform: works with iOS, Android and Expo;
43
+ - 🎨 Lib with custom and modern UI;
44
+ - 👨‍💻 Functional and class component support;
45
+ - 🈶 32 languages supported;
46
+ - ♿ Accessibility.
47
+
48
+ <br>
49
+
50
+ ## Try it out
51
+
52
+ - [Demo](https://snack.expo.dev/@astroonauta/react-native-country-select)
53
+
54
+ <br>
55
+
56
+ ## Installation
57
+
58
+ ```sh
59
+ npm install react-native-country-select
60
+ # or
61
+ yarn add react-native-country-select
62
+ ```
63
+
64
+ <br>
65
+
66
+ ## Basic Usage
67
+
68
+ - Class Component
69
+
70
+ ```jsx
71
+ import React, {Component} from 'react';
72
+ import {Text, TouchableOpacity, View} from 'react-native';
73
+ import CountrySelect from 'react-native-country-select';
74
+
75
+ export default class App extends Component {
76
+ countryRef = null;
77
+
78
+ constructor(props) {
79
+ super(props);
80
+ this.state = {
81
+ showPicker: false,
82
+ country: null,
83
+ };
84
+ }
85
+
86
+ handleCountrySelect = country => {
87
+ this.setState({country});
88
+ };
89
+
90
+ render() {
91
+ return (
92
+ <View style={{flex: 1}}>
93
+ <TouchableOpacity onPress={() => this.setState({showPicker: true})}>
94
+ <Text>Select Country</Text>
95
+ </TouchableOpacity>
96
+ <Text>
97
+ Country:{' '}
98
+ {`${this.state.selectedCountry?.name?.common} (${this.state.selectedCountry?.cca2})`}
99
+ </Text>
100
+
101
+ <CountrySelect
102
+ visible={this.state.showPicker}
103
+ onClose={() => this.setState({showPicker: false})}
104
+ onSelect={this.handleCountrySelect}
105
+ />
106
+ </View>
107
+ );
108
+ }
109
+ }
110
+ ```
111
+
112
+ <br>
113
+
114
+ - Function Component
115
+
116
+ ```jsx
117
+ import React, {useState} from 'react';
118
+ import {Text, TouchableOpacity, View} from 'react-native';
119
+
120
+ import CountrySelect from 'react-native-country-select';
121
+
122
+ export default function App() {
123
+ const [showPicker, setShowPicker] = useState(false);
124
+ const [selectedCountry, setSelectedCountry] = useState(null);
125
+
126
+ const handleCountrySelect = country => {
127
+ setSelectedCountry(country);
128
+ };
129
+
130
+ return (
131
+ <View
132
+ style={{
133
+ flex: 1,
134
+ }}>
135
+ <TouchableOpacity onPress={() => setShowPicker(true)}>
136
+ <Text>Select Country</Text>
137
+ </TouchableOpacity>
138
+ <Text>
139
+ Country: {`${selectedCountry?.name?.common} (${selectedCountry?.cca2})`}
140
+ </Text>
141
+
142
+ <CountrySelect
143
+ visible={showPicker}
144
+ onClose={() => setShowPicker(false)}
145
+ onSelect={handleCountrySelect}
146
+ />
147
+ </View>
148
+ );
149
+ }
150
+ ```
151
+
152
+ <br>
153
+
154
+ - Typescript
155
+
156
+ ```tsx
157
+ import React, {useState} from 'react';
158
+ import {Text, TouchableOpacity, View} from 'react-native';
159
+
160
+ import CountrySelect, {ICountry} from 'react-native-country-select';
161
+
162
+ export default function App() {
163
+ const [showPicker, setShowPicker] = useState<boolean>(false);
164
+ const [selectedCountry, setSelectedCountry] = useState<ICountry | null>(null);
165
+
166
+ const handleCountrySelect = (country: ICountry) => {
167
+ setSelectedCountry(country);
168
+ };
169
+
170
+ return (
171
+ <View
172
+ style={{
173
+ flex: 1,
174
+ }}>
175
+ <TouchableOpacity onPress={() => setShowPicker(true)}>
176
+ <Text>Select Country</Text>
177
+ </TouchableOpacity>
178
+ <Text>
179
+ Country: {`${selectedCountry?.name?.common} (${selectedCountry?.cca2})`}
180
+ </Text>
181
+
182
+ <CountrySelect
183
+ visible={showPicker}
184
+ onClose={() => setShowPicker(false)}
185
+ onSelect={handleCountrySelect}
186
+ />
187
+ </View>
188
+ );
189
+ }
190
+ ```
191
+
192
+ <br>
193
+
194
+ ## CountrySelect Props
195
+
196
+ | Prop | Type | Required | Default | Description |
197
+ | -------- | --------------------------- | -------- | ------- | ---------------------------------------------------------- |
198
+ | visible | boolean | Yes | false | Controls the visibility of the country picker modal |
199
+ | onClose | () => void | Yes | - | Callback function called when the modal is closed |
200
+ | onSelect | (country: ICountry) => void | Yes | - | Callback function called when a country is selected |
201
+ | theme | 'light' \| 'dark' | No | 'light' | Theme for the country picker |
202
+ | language | ICountrySelectLanguages | No | 'eng' | Language for country names (see supported languages below) |
203
+
204
+ <br>
205
+
206
+ ### Supported Languages
207
+
208
+ The `language` prop supports the following values:
209
+
210
+ | Code | Language |
211
+ | ---------- | ------------------- |
212
+ | `ara` | Arabic |
213
+ | `bel` | Belarusian |
214
+ | `bre` | Breton |
215
+ | `bul` | Bulgarian |
216
+ | `ces` | Czech |
217
+ | `deu` | German |
218
+ | `ell` | Greek |
219
+ | `eng` | English |
220
+ | `est` | Estonian |
221
+ | `fin` | Finnish |
222
+ | `fra` | French |
223
+ | `heb` | Hebrew |
224
+ | `hrv` | Croatian |
225
+ | `hun` | Hungarian |
226
+ | `ita` | Italian |
227
+ | `jpn` | Japanese |
228
+ | `kor` | Korean |
229
+ | `nld` | Dutch |
230
+ | `per` | Persian |
231
+ | `pol` | Polish |
232
+ | `por` | Portuguese |
233
+ | `ron` | Romanian |
234
+ | `rus` | Russian |
235
+ | `slk` | Slovak |
236
+ | `spa` | Spanish |
237
+ | `srp` | Serbian |
238
+ | `swe` | Swedish |
239
+ | `tur` | Turkish |
240
+ | `ukr` | Ukrainian |
241
+ | `urd` | Urdu |
242
+ | `zho` | Chinese |
243
+ | `zho-Hans` | Simplified Chinese |
244
+ | `zho-Hant` | Traditional Chinese |
245
+
246
+ <br>
247
+
248
+ ## Testing
249
+
250
+ When utilizing this package, you may need to target the CountrySelect component in your automated tests. To facilitate this, we provide a testID props for the CountrySelect component. The testID can be integrated with popular testing libraries such as @testing-library/react-native or Maestro. This enables you to efficiently locate and interact with CountrySelect elements within your tests, ensuring a robust and reliable testing experience.
251
+
252
+ ```js
253
+ const countrySelect = getByTestId('countrySelectSearchInput');
254
+ const countrySelectList = getByTestId('countrySelectList');
255
+ const countrySelectItem = getByTestId('countrySelectItem');
256
+ ```
257
+
258
+ <br>
259
+
260
+ ## Accessibility
261
+
262
+ Ensure your app is inclusive and usable by everyone by leveraging built-in React Native accessibility features. The accessibility props are covered by this package.
263
+
264
+ <br>
265
+
266
+ ## Contributing
267
+
268
+ Thank you for considering contributing to **react-native-country-select**!
269
+
270
+ - Fork or clone this repository
271
+
272
+ ```bash
273
+ $ git clone https://github.com/AstrOOnauta/react-native-country-select.git
274
+ ```
275
+
276
+ - Repair, Update and Enjoy 🛠️🚧⚙️
277
+
278
+ - Create a new PR to this repository
279
+
280
+ <br>
281
+
282
+ ## Credits
283
+
284
+ @mledoze for the [countries data](https://github.com/mledoze/countries)
285
+
286
+ ## License
287
+
288
+ [ISC](LICENSE.md)
289
+
290
+ <br>
291
+
292
+ <div align = "center">
293
+ <br>
294
+ Thanks for stopping by! 😁
295
+ <br>
296
+ </div>
@@ -0,0 +1,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
+ ({item, onSelect, onClose, theme = 'light', language}) => {
11
+ const styles = createStyles(theme);
12
+
13
+ return (
14
+ <TouchableOpacity
15
+ testID="countrySelectItem"
16
+ accessibilityRole="button"
17
+ accessibilityLabel="Country Select Item"
18
+ accessibilityHint="Click to select a country"
19
+ style={styles.countryItem}
20
+ onPress={() => {
21
+ onSelect(item);
22
+ onClose();
23
+ }}>
24
+ <Text testID="countrySelectItemFlag" style={styles.flag}>
25
+ {item.flag}
26
+ </Text>
27
+ <View style={styles.countryInfo}>
28
+ <Text
29
+ testID="countrySelectItemCallingCode"
30
+ style={styles.callingCode}>
31
+ {item.idd.root}
32
+ </Text>
33
+ <Text testID="countrySelectItemName" style={styles.countryName}>
34
+ {item?.translations[language]?.common ||
35
+ item?.translations[DEFAULT_LANGUAGE]?.common}
36
+ </Text>
37
+ </View>
38
+ </TouchableOpacity>
39
+ );
40
+ },
41
+ );
@@ -0,0 +1,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
+ } from 'react-native';
12
+
13
+ import {CountryItem} from '../CountryItem';
14
+
15
+ import {createStyles} from '../styles';
16
+ import countries from '../../constants/countries.json';
17
+ import {translations} from '../../utils/getTranslation';
18
+ import {
19
+ ICountry,
20
+ ICountrySelectProps,
21
+ ICountrySelectLanguages,
22
+ } from '../../interface';
23
+
24
+ const DEFAULT_LANGUAGE: ICountrySelectLanguages = 'eng';
25
+
26
+ export const CountrySelect: React.FC<ICountrySelectProps> = ({
27
+ visible,
28
+ onClose,
29
+ onSelect,
30
+ theme = 'light',
31
+ language = DEFAULT_LANGUAGE,
32
+ ...props
33
+ }) => {
34
+ const styles = createStyles(theme);
35
+
36
+ const [searchQuery, setSearchQuery] = useState('');
37
+
38
+ // Obtain the country name in the selected language
39
+ const getCountryNameInLanguage = (country: ICountry): string => {
40
+ return (
41
+ country.translations[language]?.common ||
42
+ country.translations[DEFAULT_LANGUAGE]?.common ||
43
+ country.name.common ||
44
+ ''
45
+ );
46
+ };
47
+
48
+ // Normalize country name and remove accents
49
+ const normalizeCountryName = (str: string) =>
50
+ str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
51
+
52
+ const sortCountriesAlphabetically = (
53
+ countriesList: ICountry[],
54
+ ): ICountry[] => {
55
+ return [...countriesList].sort((a, b) => {
56
+ const nameA = normalizeCountryName(
57
+ getCountryNameInLanguage(a).toLowerCase(),
58
+ );
59
+ const nameB = normalizeCountryName(
60
+ getCountryNameInLanguage(b).toLowerCase(),
61
+ );
62
+ return nameA.localeCompare(nameB);
63
+ });
64
+ };
65
+
66
+ const getCountries = useMemo(() => {
67
+ const query = searchQuery.toLowerCase();
68
+
69
+ if (query.length > 0) {
70
+ let countriesData = countries as unknown as ICountry[];
71
+
72
+ const filteredCountries = countriesData.filter(country => {
73
+ const countryName = getCountryNameInLanguage(country);
74
+ const callingCode = country.idd.root.toLowerCase();
75
+ const flag = country.flag.toLowerCase();
76
+ return (
77
+ countryName.includes(query) ||
78
+ callingCode.includes(query) ||
79
+ flag.includes(query)
80
+ );
81
+ });
82
+
83
+ return sortCountriesAlphabetically(filteredCountries);
84
+ }
85
+
86
+ let allCountries = countries as unknown as ICountry[];
87
+
88
+ const result: ICountry[] = allCountries;
89
+
90
+ return result;
91
+ }, [searchQuery]);
92
+
93
+ const keyExtractor = useCallback((item: ICountry) => item.cca2, []);
94
+
95
+ const renderItem: ListRenderItem<ICountry> = useCallback(
96
+ ({item}) => {
97
+ return (
98
+ <CountryItem
99
+ item={item as ICountry}
100
+ onSelect={onSelect}
101
+ onClose={onClose}
102
+ theme={theme}
103
+ language={language}
104
+ />
105
+ );
106
+ },
107
+ [onSelect, onClose, styles, language],
108
+ );
109
+
110
+ return (
111
+ <Modal
112
+ visible={visible}
113
+ transparent
114
+ animationType="fade"
115
+ onRequestClose={onClose}
116
+ statusBarTranslucent
117
+ {...props}>
118
+ <Pressable
119
+ style={[
120
+ styles.backdrop,
121
+ {alignItems: 'center', justifyContent: 'center'},
122
+ ]}
123
+ onPress={onClose}>
124
+ <Pressable style={styles.popupContainer}>
125
+ <View style={styles.popupContent}>
126
+ <View style={styles.searchContainer}>
127
+ <TextInput
128
+ testID="countrySelectSearchInput"
129
+ accessibilityRole="text"
130
+ accessibilityLabel="Country Select Search Input"
131
+ accessibilityHint="Type to search for a country"
132
+ style={styles.searchInput}
133
+ placeholder={
134
+ translations.searchPlaceholder[
135
+ language as ICountrySelectLanguages
136
+ ]
137
+ }
138
+ placeholderTextColor={styles.searchInputPlaceholder.color}
139
+ value={searchQuery}
140
+ onChangeText={setSearchQuery}
141
+ />
142
+ </View>
143
+
144
+ <FlatList
145
+ testID="countrySelectList"
146
+ accessibilityRole="list"
147
+ accessibilityLabel="Country Select List"
148
+ accessibilityHint="List of countries"
149
+ data={getCountries}
150
+ keyExtractor={keyExtractor}
151
+ renderItem={renderItem}
152
+ keyboardShouldPersistTaps="handled"
153
+ />
154
+ </View>
155
+ </Pressable>
156
+ </Pressable>
157
+ </Modal>
158
+ );
159
+ };
@@ -0,0 +1,2 @@
1
+ export {CountrySelect} from './CountrySelect';
2
+ export {CountryItem} from './CountryItem';
@@ -0,0 +1,83 @@
1
+ import {StyleSheet} from 'react-native';
2
+
3
+ export const createStyles = (theme: 'light' | 'dark') =>
4
+ StyleSheet.create({
5
+ backdrop: {
6
+ flex: 1,
7
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
8
+ },
9
+ popupContainer: {
10
+ width: '90%',
11
+ maxWidth: 600,
12
+ height: '60%',
13
+ alignItems: 'center',
14
+ justifyContent: 'center',
15
+ alignSelf: 'center',
16
+ },
17
+ popupContent: {
18
+ flex: 1,
19
+ width: '100%',
20
+ backgroundColor: theme === 'dark' ? '#202020' : '#FFFFFF',
21
+ borderRadius: 20,
22
+ alignSelf: 'center',
23
+ padding: 16,
24
+ },
25
+ searchContainer: {
26
+ marginBottom: 16,
27
+ paddingTop: 16,
28
+ flexDirection: 'row',
29
+ },
30
+ searchInput: {
31
+ flex: 1,
32
+ borderRadius: 8,
33
+ paddingHorizontal: 16,
34
+ fontSize: 16,
35
+ borderColor: theme === 'dark' ? '#F3F3F3' : '#303030',
36
+ borderWidth: 1,
37
+ color: theme === 'dark' ? '#FFFFFF' : '#000000',
38
+ },
39
+ searchInputPlaceholder: {
40
+ color: theme === 'dark' ? '#FFFFFF80' : '#00000080',
41
+ },
42
+ list: {
43
+ flex: 1,
44
+ },
45
+ countryItem: {
46
+ flex: 1,
47
+ flexDirection: 'row',
48
+ alignItems: 'center',
49
+ padding: 12,
50
+ marginBottom: 8,
51
+ backgroundColor: theme === 'dark' ? '#404040' : '#F3F3F3',
52
+ borderColor: theme === 'dark' ? '#F3F3F3' : '#303030',
53
+ borderWidth: 1,
54
+ borderRadius: 8,
55
+ },
56
+ flag: {
57
+ flex: 0.1,
58
+ fontSize: 16,
59
+ color: theme === 'dark' ? '#FFFFFF' : '#000000',
60
+ },
61
+ countryInfo: {
62
+ flex: 0.9,
63
+ flexDirection: 'row',
64
+ alignItems: 'center',
65
+ },
66
+ callingCode: {
67
+ flex: 0.2,
68
+ fontSize: 14,
69
+ color: theme === 'dark' ? '#FFFFFF80' : '#00000080',
70
+ },
71
+ countryName: {
72
+ flex: 0.8,
73
+ fontSize: 16,
74
+ fontWeight: '500',
75
+ color: theme === 'dark' ? '#FFFFFF' : '#000000',
76
+ },
77
+ sectionTitle: {
78
+ fontSize: 16,
79
+ fontWeight: '600',
80
+ paddingVertical: 8,
81
+ color: theme === 'dark' ? '#FFFFFF' : '#000000',
82
+ },
83
+ });