react-native-country-select 0.1.2 โ†’ 0.2.0

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/README.md CHANGED
@@ -1,3 +1,11 @@
1
+ <br>
2
+
3
+ <div align = "center">
4
+ <img src="lib/assets/images/preview.png" alt="React Native International Phone Number Input Lib preview">
5
+ </div>
6
+
7
+ <br>
8
+
1
9
  <h1 align="center">React Native Country Select</h1>
2
10
 
3
11
  <p>
@@ -39,11 +47,13 @@
39
47
 
40
48
  ## Features
41
49
 
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.
50
+ - ๐Ÿ“ฑ **Cross-Platform** โ€“ Works seamlessly on **iOS, Android and Web**;
51
+ - ๐Ÿงฉ **Flexible Integration** โ€“ Supports both **React Native CLI & Expo**;
52
+ - ๐ŸŽจ **Modern UI** - Custom component with sleek design;
53
+ - ๐Ÿ‘จโ€๐Ÿ’ป **Component Versatility** - Works with **functional & class components**;
54
+ - ๐Ÿˆถ **internationalization** - Supports **32 languages**;
55
+ - ๐Ÿงช **Test Ready** โ€“ Smooth testing integration;
56
+ - โ™ฟ **Accessibility** โ€“ Accessibility standards to screen readers.
47
57
 
48
58
  <br>
49
59
 
@@ -63,6 +73,51 @@ yarn add react-native-country-select
63
73
 
64
74
  <br>
65
75
 
76
+ ## Additional config to WEB
77
+
78
+ - ### Using React Native CLI:
79
+
80
+ Create a `react-native.config.js` file at the root of your react-native project with:
81
+
82
+ ```bash
83
+ module.exports = {
84
+ project: {
85
+ ios: {},
86
+ android: {},
87
+ },
88
+ assets: [
89
+ './node_modules/react-native-country-select/lib/assets/fonts',
90
+ ],
91
+ };
92
+ ```
93
+
94
+ Then link the font to your native projects with:
95
+
96
+ ```bash
97
+ npx react-native-asset
98
+ ```
99
+
100
+ - ### Using Expo:
101
+
102
+ 1. Install [expo-fonts](https://docs.expo.dev/versions/latest/sdk/font/): `npx expo install expo-font`;
103
+ 2. Initialize the `expo-font`:
104
+
105
+ ```bash
106
+ import { useFonts } from 'expo-font';
107
+
108
+ ...
109
+
110
+ useFonts({
111
+ 'TwemojiMozilla': require('./node_modules/react-native-country-select/lib/assets/fonts/TwemojiMozilla.woff2'),
112
+ });
113
+
114
+ ...
115
+ ```
116
+
117
+ > Observation: you need to recompile your project after adding new fonts.
118
+
119
+ <br>
120
+
66
121
  ## Basic Usage
67
122
 
68
123
  - Class Component
@@ -193,24 +248,34 @@ export default function App() {
193
248
 
194
249
  ## CountrySelect Props
195
250
 
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
- | popularCountries | string[] | No | [] | Array of country codes to show in popular section |
202
- | visibleCountries | ICountryCca2[] | No | [] | Array of country codes to show (whitelist) |
203
- | hiddenCountries | ICountryCca2[] | No | [] | Array of country codes to hide (blacklist) |
204
- | theme | 'light' \| 'dark' | No | 'light' | Theme for the country picker |
205
- | language | ICountrySelectLanguages | No | 'eng' | Language for country names (see supported languages below) |
206
- | disabledBackdropPress | boolean | No | false | Whether to disable backdrop press to close |
207
- | removedBackdrop | boolean | No | false | Whether to remove the backdrop completely |
208
- | onBackdropPress | () => void | No | - | Custom callback for backdrop press |
209
- | countryItemComponent | (item: ICountry) => ReactElement | No | - | Custom component for country items |
210
- | sectionTitleComponent | (item: ISectionTitle) => ReactElement | No | - | Custom component for section titles |
211
- | popularCountriesTitle | string | No | 'Popular Countries' | Popular Countries section title |
212
- | allCountriesTitle | string | No | 'All Countries' | All Countries section title |
213
- | showsVerticalScrollIndicator | boolean | No | false | displays a horizontal scroll indicator |
251
+ | Prop | Type | Required | Default | Description |
252
+ | ---------------------------- | ----------------------------------------------------------------------- | -------- | ------------------- | ---------------------------------------------------------- |
253
+ | visible | boolean | Yes | false | Controls the visibility of the country picker modal |
254
+ | onClose | () => void | Yes | - | Callback function called when the modal is closed |
255
+ | onSelect | (country: [ICountry](lib/interfaces/country.ts)) => void | Yes | - | Callback function called when a country is selected |
256
+ | modalType | 'bottomSheet' \| 'popup' | No | 'bottomSheet' | Type of modal to display |
257
+ | countrySelectStyle | [ICountrySelectStyle](lib/interfaces/countrySelectStyles.ts) | No | - | Custom styles for the country picker |
258
+ | isFullScreen | boolean | No | false | Whether the modal should be full screen |
259
+ | popularCountries | string[] | No | [] | Array of country codes to show in popular section |
260
+ | visibleCountries | [ICountryCca2[]](lib/interfaces/countryCca2.ts) | No | [] | Array of country codes to show (whitelist) |
261
+ | hiddenCountries | [ICountryCca2[]](lib/interfaces/countryCca2.ts) | No | [] | Array of country codes to hide (blacklist) |
262
+ | theme | 'light' \| 'dark' | No | 'light' | Theme for the country picker |
263
+ | language | [ICountrySelectLanguages](lib/interfaces/countrySelectLanguages.ts) | No | 'eng' | Language for country names (see supported languages below) |
264
+ | showSearchInput | boolean | No | true | Whether to show the search input field |
265
+ | searchPlaceholder | string | No | 'Search country...' | Placeholder text for search input |
266
+ | minBottomsheetHeight | number \| string | No | 30% | Minimum height for bottom sheet modal |
267
+ | maxBottomsheetHeight | number \| string | No | 80% | Maximum height for bottom sheet modal |
268
+ | initialBottomsheetHeight | number \| string | No | 50% | Initial height for bottom sheet modal |
269
+ | disabledBackdropPress | boolean | No | false | Whether to disable backdrop press to close |
270
+ | removedBackdrop | boolean | No | false | Whether to remove the backdrop completely |
271
+ | onBackdropPress | () => void | No | - | Custom callback for backdrop press |
272
+ | countryItemComponent | (item: [ICountry](lib/interfaces/country.ts)) => ReactElement | No | - | Custom component for country items |
273
+ | sectionTitleComponent | (item: [ISectionTitle](lib/interfaces/sectionTitle.ts)) => ReactElement | No | - | Custom component for section titles |
274
+ | closeButtonComponent | () => ReactElement | No | - | Custom component for closeButton |
275
+ | showCloseButton | boolean | No | false | Whether to show the close button |
276
+ | popularCountriesTitle | string | No | 'Popular Countries' | Popular Countries section title |
277
+ | allCountriesTitle | string | No | 'All Countries' | All Countries section title |
278
+ | showsVerticalScrollIndicator | boolean | No | false | Displays a horizontal scroll indicator |
214
279
 
215
280
  <br>
216
281
 
@@ -262,8 +327,11 @@ When utilizing this package, you may need to target the CountrySelect component
262
327
 
263
328
  ```js
264
329
  const countrySelect = getByTestId('countrySelectSearchInput');
330
+ const countrySelectBackdrop = getByTestId('countrySelectBackdrop');
265
331
  const countrySelectList = getByTestId('countrySelectList');
332
+ const countrySelectSearchInput = getByTestId('countrySelectSearchInput');
266
333
  const countrySelectItem = getByTestId('countrySelectItem');
334
+ const countrySelectCloseButton = getByTestId('countrySelectCloseButton');
267
335
  ```
268
336
 
269
337
  <br>
Binary file
@@ -7,7 +7,15 @@ import {ICountryItemProps, ICountrySelectLanguages} from '../../interface';
7
7
  const DEFAULT_LANGUAGE: ICountrySelectLanguages = 'eng';
8
8
 
9
9
  export const CountryItem = memo<ICountryItemProps>(
10
- ({item, onSelect, onClose, theme = 'light', language}) => {
10
+ ({
11
+ item,
12
+ onSelect,
13
+ onClose,
14
+ theme = 'light',
15
+ language,
16
+ modalType,
17
+ countrySelectStyle,
18
+ }) => {
11
19
  const styles = createStyles(theme);
12
20
 
13
21
  return (
@@ -16,21 +24,51 @@ export const CountryItem = memo<ICountryItemProps>(
16
24
  accessibilityRole="button"
17
25
  accessibilityLabel="Country Select Item"
18
26
  accessibilityHint="Click to select a country"
19
- style={styles.countryItem}
27
+ style={[
28
+ styles.countryItem,
29
+ modalType === 'popup'
30
+ ? countrySelectStyle?.popup?.countryItem
31
+ : countrySelectStyle?.bottomSheet?.countryItem,
32
+ ]}
20
33
  onPress={() => {
21
34
  onSelect(item);
22
35
  onClose();
23
36
  }}>
24
- <Text testID="countrySelectItemFlag" style={styles.flag}>
37
+ <Text
38
+ testID="countrySelectItemFlag"
39
+ style={[
40
+ styles.flag,
41
+ modalType === 'popup'
42
+ ? countrySelectStyle?.popup?.flag
43
+ : countrySelectStyle?.bottomSheet?.flag,
44
+ ]}>
25
45
  {item.flag}
26
46
  </Text>
27
- <View style={styles.countryInfo}>
47
+ <View
48
+ style={[
49
+ styles.countryInfo,
50
+ modalType === 'popup'
51
+ ? countrySelectStyle?.popup?.countryInfo
52
+ : countrySelectStyle?.bottomSheet?.countryInfo,
53
+ ]}>
28
54
  <Text
29
55
  testID="countrySelectItemCallingCode"
30
- style={styles.callingCode}>
56
+ style={[
57
+ styles.callingCode,
58
+ modalType === 'popup'
59
+ ? countrySelectStyle?.popup?.callingCode
60
+ : countrySelectStyle?.bottomSheet?.callingCode,
61
+ ]}>
31
62
  {item.idd.root}
32
63
  </Text>
33
- <Text testID="countrySelectItemName" style={styles.countryName}>
64
+ <Text
65
+ testID="countrySelectItemName"
66
+ style={[
67
+ styles.countryName,
68
+ modalType === 'popup'
69
+ ? countrySelectStyle?.popup?.countryName
70
+ : countrySelectStyle?.bottomSheet?.countryName,
71
+ ]}>
34
72
  {item?.translations[language]?.common ||
35
73
  item?.translations[DEFAULT_LANGUAGE]?.common}
36
74
  </Text>
@@ -1,19 +1,25 @@
1
1
  /* eslint-disable react-native/no-inline-styles */
2
2
  /* eslint-disable react-hooks/exhaustive-deps */
3
- import React, {useCallback, useMemo, useState} from 'react';
3
+ import React, {useCallback, useMemo, useState, useRef, useEffect} from 'react';
4
4
  import {
5
5
  View,
6
6
  TextInput,
7
7
  FlatList,
8
+ useWindowDimensions,
8
9
  Pressable,
10
+ Animated,
11
+ PanResponder,
9
12
  ListRenderItem,
10
13
  Modal,
14
+ Keyboard,
11
15
  Text,
16
+ TouchableOpacity,
12
17
  } from 'react-native';
13
18
 
14
19
  import {CountryItem} from '../CountryItem';
15
20
 
16
21
  import {createStyles} from '../styles';
22
+ import parseHeight from '../../utils/parseHeight';
17
23
  import countries from '../../constants/countries.json';
18
24
  import {translations} from '../../utils/getTranslation';
19
25
  import {
@@ -23,30 +29,207 @@ import {
23
29
  IListItem,
24
30
  } from '../../interface';
25
31
 
32
+ const ITEM_HEIGHT = 56;
33
+ const SECTION_HEADER_HEIGHT = 40;
34
+
35
+ const MIN_HEIGHT_PERCENTAGE = 0.3;
36
+ const MAX_HEIGHT_PERCENTAGE = 0.9;
37
+ const INITIAL_HEIGHT_PERCENTAGE = 0.5;
38
+
26
39
  const DEFAULT_LANGUAGE: ICountrySelectLanguages = 'eng';
27
40
 
28
41
  export const CountrySelect: React.FC<ICountrySelectProps> = ({
29
42
  visible,
30
43
  onClose,
31
44
  onSelect,
45
+ modalType = 'bottomSheet',
32
46
  theme = 'light',
47
+ isFullScreen = false,
48
+ countrySelectStyle,
33
49
  popularCountries = [],
34
50
  visibleCountries = [],
35
51
  hiddenCountries = [],
36
52
  language = DEFAULT_LANGUAGE,
53
+ showSearchInput = true,
54
+ searchPlaceholder,
55
+ showCloseButton = false,
56
+ minBottomsheetHeight,
57
+ maxBottomsheetHeight,
58
+ initialBottomsheetHeight,
37
59
  disabledBackdropPress,
38
60
  removedBackdrop,
39
61
  onBackdropPress,
40
62
  sectionTitleComponent,
41
63
  countryItemComponent,
64
+ closeButtonComponent,
42
65
  popularCountriesTitle,
43
66
  allCountriesTitle,
44
67
  showsVerticalScrollIndicator = false,
45
68
  ...props
46
69
  }) => {
70
+ const {height: windowHeight} = useWindowDimensions();
47
71
  const styles = createStyles(theme);
48
72
 
49
73
  const [searchQuery, setSearchQuery] = useState('');
74
+ const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
75
+ const [bottomSheetSize, setBottomSheetSize] = useState({
76
+ minHeight: MIN_HEIGHT_PERCENTAGE * windowHeight,
77
+ maxHeight: MAX_HEIGHT_PERCENTAGE * windowHeight,
78
+ initialHeight: INITIAL_HEIGHT_PERCENTAGE * windowHeight,
79
+ });
80
+
81
+ const sheetHeight = useRef(
82
+ new Animated.Value(bottomSheetSize.initialHeight),
83
+ ).current;
84
+ const lastHeight = useRef(bottomSheetSize.initialHeight);
85
+ const dragStartY = useRef(0);
86
+
87
+ useEffect(() => {
88
+ if (modalType === 'popup') {
89
+ return;
90
+ }
91
+
92
+ const DRAG_HANDLE_HEIGHT = 20;
93
+ const availableHeight = windowHeight - DRAG_HANDLE_HEIGHT;
94
+
95
+ const parsedMinHeight = parseHeight(minBottomsheetHeight, availableHeight);
96
+ const parsedMaxHeight = parseHeight(maxBottomsheetHeight, availableHeight);
97
+ const parsedInitialHeight = parseHeight(
98
+ initialBottomsheetHeight,
99
+ availableHeight,
100
+ );
101
+
102
+ setBottomSheetSize({
103
+ minHeight: parsedMinHeight || MIN_HEIGHT_PERCENTAGE * availableHeight,
104
+ maxHeight: parsedMaxHeight || MAX_HEIGHT_PERCENTAGE * availableHeight,
105
+ initialHeight:
106
+ parsedInitialHeight || INITIAL_HEIGHT_PERCENTAGE * availableHeight,
107
+ });
108
+ }, [
109
+ minBottomsheetHeight,
110
+ maxBottomsheetHeight,
111
+ initialBottomsheetHeight,
112
+ windowHeight,
113
+ modalType,
114
+ ]);
115
+
116
+ // Resets to initial height when the modal is opened
117
+ useEffect(() => {
118
+ if (modalType === 'popup') {
119
+ return;
120
+ }
121
+
122
+ if (visible) {
123
+ sheetHeight.setValue(bottomSheetSize.initialHeight);
124
+ lastHeight.current = bottomSheetSize.initialHeight;
125
+ }
126
+ }, [visible, bottomSheetSize.initialHeight, modalType]);
127
+
128
+ useEffect(() => {
129
+ if (modalType === 'popup') {
130
+ return;
131
+ }
132
+
133
+ if (isKeyboardVisible) {
134
+ sheetHeight.setValue(
135
+ parseHeight(bottomSheetSize.maxHeight, windowHeight),
136
+ );
137
+ lastHeight.current = bottomSheetSize.maxHeight;
138
+ } else {
139
+ sheetHeight.setValue(parseHeight(lastHeight.current, windowHeight));
140
+ }
141
+ }, [isKeyboardVisible, modalType]);
142
+
143
+ // Monitors keyboard events
144
+ useEffect(() => {
145
+ if (modalType === 'popup') {
146
+ return;
147
+ }
148
+
149
+ const keyboardDidShowListener = Keyboard.addListener(
150
+ 'keyboardDidShow',
151
+ () => {
152
+ setIsKeyboardVisible(true);
153
+ },
154
+ );
155
+ const keyboardDidHideListener = Keyboard.addListener(
156
+ 'keyboardDidHide',
157
+ () => {
158
+ setIsKeyboardVisible(false);
159
+ },
160
+ );
161
+
162
+ return () => {
163
+ keyboardDidShowListener?.remove();
164
+ keyboardDidHideListener?.remove();
165
+ };
166
+ }, [modalType]);
167
+
168
+ const handlePanResponder = useMemo(
169
+ () =>
170
+ PanResponder.create({
171
+ onStartShouldSetPanResponder: () => {
172
+ // Only respond to touches on the drag handle
173
+ return true;
174
+ },
175
+ onMoveShouldSetPanResponder: (evt, gestureState) => {
176
+ // Only respond to vertical movements with sufficient distance
177
+ return Math.abs(gestureState.dy) > 5;
178
+ },
179
+ onPanResponderGrant: e => {
180
+ dragStartY.current = e.nativeEvent.pageY;
181
+ sheetHeight.stopAnimation();
182
+ },
183
+ onPanResponderMove: e => {
184
+ const currentY = e.nativeEvent.pageY;
185
+ const dy = currentY - dragStartY.current;
186
+ const proposedHeight = lastHeight.current - dy;
187
+
188
+ // Allows free dragging but with smooth limits
189
+ sheetHeight.setValue(proposedHeight);
190
+ },
191
+ onPanResponderRelease: e => {
192
+ const currentY = e.nativeEvent.pageY;
193
+ const dy = currentY - dragStartY.current;
194
+ const currentHeight = lastHeight.current - dy;
195
+
196
+ // Close modal if the final height is less than minHeight
197
+ if (currentHeight < bottomSheetSize.minHeight) {
198
+ Animated.timing(sheetHeight, {
199
+ toValue: 0,
200
+ duration: 200,
201
+ useNativeDriver: false,
202
+ }).start(onClose);
203
+ return;
204
+ }
205
+
206
+ // Snap to nearest limit
207
+ const finalHeight = Math.min(
208
+ Math.max(currentHeight, bottomSheetSize.minHeight),
209
+ bottomSheetSize.maxHeight,
210
+ );
211
+
212
+ Animated.spring(sheetHeight, {
213
+ toValue: finalHeight,
214
+ useNativeDriver: false,
215
+ tension: 50,
216
+ friction: 12,
217
+ }).start(() => {
218
+ lastHeight.current = finalHeight;
219
+ });
220
+ },
221
+ onPanResponderTerminate: () => {
222
+ // Reset to last stable height if gesture is terminated
223
+ Animated.spring(sheetHeight, {
224
+ toValue: lastHeight.current,
225
+ useNativeDriver: false,
226
+ tension: 50,
227
+ friction: 12,
228
+ }).start();
229
+ },
230
+ }),
231
+ [bottomSheetSize, sheetHeight, onClose],
232
+ );
50
233
 
51
234
  // Obtain the country name in the selected language
52
235
  const getCountryNameInLanguage = (country: ICountry): string => {
@@ -96,15 +279,24 @@ export const CountrySelect: React.FC<ICountrySelectProps> = ({
96
279
 
97
280
  const filteredCountries = countriesData.filter(country => {
98
281
  const countryName = getCountryNameInLanguage(country);
282
+ const normalizedCountryName = normalizeCountryName(
283
+ countryName.toLowerCase(),
284
+ );
285
+ const normalizedQuery = normalizeCountryName(query);
99
286
  const callingCode = country.idd.root.toLowerCase();
100
287
  const flag = country.flag.toLowerCase();
288
+ const countryCode = country.cca2.toLowerCase();
289
+
101
290
  return (
102
- countryName.includes(query) ||
291
+ normalizedCountryName.includes(normalizedQuery) ||
292
+ countryName.toLowerCase().includes(query) ||
103
293
  callingCode.includes(query) ||
104
- flag.includes(query)
294
+ flag.includes(query) ||
295
+ countryCode.includes(query)
105
296
  );
106
297
  });
107
298
 
299
+ // Ordenar os paรญses filtrados alfabeticamente
108
300
  return sortCountriesAlphabetically(filteredCountries);
109
301
  }
110
302
 
@@ -164,6 +356,98 @@ export const CountrySelect: React.FC<ICountrySelectProps> = ({
164
356
  [],
165
357
  );
166
358
 
359
+ const getItemLayout = useCallback(
360
+ (data: IListItem[] | null | undefined, index: number) => {
361
+ let offset = 0;
362
+ let length = ITEM_HEIGHT;
363
+
364
+ if (data) {
365
+ const item = data[index];
366
+ if ('isSection' in item) {
367
+ length = SECTION_HEADER_HEIGHT;
368
+ }
369
+ }
370
+
371
+ return {
372
+ length,
373
+ offset: offset + index * ITEM_HEIGHT,
374
+ index,
375
+ };
376
+ },
377
+ [],
378
+ );
379
+
380
+ const renderCloseButton = () => {
381
+ if (closeButtonComponent) {
382
+ return closeButtonComponent();
383
+ }
384
+
385
+ return (
386
+ <TouchableOpacity
387
+ testID="countrySelectCloseButton"
388
+ accessibilityRole="button"
389
+ accessibilityLabel="Country Select Modal Close Button"
390
+ accessibilityHint="Click to close the Country Select modal"
391
+ style={[styles.closeButton, countrySelectStyle?.popup?.closeButton]}
392
+ activeOpacity={0.6}
393
+ onPress={onClose}>
394
+ <Text
395
+ style={[
396
+ styles.closeButtonText,
397
+ countrySelectStyle?.popup?.closeButtonText,
398
+ ]}>
399
+ {'\u00D7'}
400
+ </Text>
401
+ </TouchableOpacity>
402
+ );
403
+ };
404
+
405
+ const renderSearchInput = () => {
406
+ return (
407
+ <TextInput
408
+ testID="countrySelectSearchInput"
409
+ accessibilityRole="text"
410
+ accessibilityLabel="Country Select Search Input"
411
+ accessibilityHint="Type to search for a country"
412
+ style={[
413
+ styles.searchInput,
414
+ modalType === 'popup'
415
+ ? countrySelectStyle?.popup?.searchInput
416
+ : countrySelectStyle?.bottomSheet?.searchInput,
417
+ ]}
418
+ placeholder={
419
+ searchPlaceholder ||
420
+ translations.searchPlaceholder[language as ICountrySelectLanguages]
421
+ }
422
+ placeholderTextColor={
423
+ (modalType === 'popup'
424
+ ? countrySelectStyle?.popup?.searchInputPlaceholder?.color
425
+ : countrySelectStyle?.bottomSheet?.searchInputPlaceholder?.color) ||
426
+ styles.searchInputPlaceholder.color
427
+ }
428
+ value={searchQuery}
429
+ onChangeText={setSearchQuery}
430
+ />
431
+ );
432
+ };
433
+
434
+ const renderFlatList = () => {
435
+ return (
436
+ <FlatList
437
+ testID="countrySelectList"
438
+ accessibilityRole="list"
439
+ accessibilityLabel="Country Select List"
440
+ accessibilityHint="List of countries"
441
+ data={getCountries}
442
+ keyExtractor={keyExtractor}
443
+ renderItem={renderItem}
444
+ getItemLayout={getItemLayout}
445
+ keyboardShouldPersistTaps="handled"
446
+ showsVerticalScrollIndicator={showsVerticalScrollIndicator || false}
447
+ />
448
+ );
449
+ };
450
+
167
451
  const renderItem: ListRenderItem<IListItem> = useCallback(
168
452
  ({item, index}) => {
169
453
  if ('isSection' in item) {
@@ -174,7 +458,12 @@ export const CountrySelect: React.FC<ICountrySelectProps> = ({
174
458
  <Text
175
459
  testID="countrySelectSectionTitle"
176
460
  accessibilityRole="header"
177
- style={styles.sectionTitle}>
461
+ style={[
462
+ styles.sectionTitle,
463
+ modalType === 'popup'
464
+ ? countrySelectStyle?.popup?.sectionTitle
465
+ : countrySelectStyle?.bottomSheet?.sectionTitle,
466
+ ]}>
178
467
  {popularCountriesTitle && index === 0
179
468
  ? popularCountriesTitle
180
469
  : allCountriesTitle && index > 0
@@ -195,6 +484,8 @@ export const CountrySelect: React.FC<ICountrySelectProps> = ({
195
484
  onClose={onClose}
196
485
  theme={theme}
197
486
  language={language}
487
+ modalType={modalType}
488
+ countrySelectStyle={countrySelectStyle}
198
489
  />
199
490
  );
200
491
  },
@@ -208,58 +499,111 @@ export const CountrySelect: React.FC<ICountrySelectProps> = ({
208
499
  ],
209
500
  );
210
501
 
502
+ if (modalType === 'popup' || isFullScreen) {
503
+ return (
504
+ <Modal
505
+ visible={visible}
506
+ transparent
507
+ animationType="fade"
508
+ onRequestClose={onClose}
509
+ statusBarTranslucent
510
+ {...props}>
511
+ <Pressable
512
+ testID="countrySelectBackdrop"
513
+ accessibilityRole="button"
514
+ accessibilityLabel="Country Select Modal Backdrop"
515
+ accessibilityHint="Click to close the Country Select modal"
516
+ disabled={disabledBackdropPress || removedBackdrop}
517
+ style={[
518
+ styles.backdrop,
519
+ {alignItems: 'center', justifyContent: 'center'},
520
+ countrySelectStyle?.popup?.backdrop,
521
+ removedBackdrop && {backgroundColor: 'transparent'},
522
+ ]}
523
+ onPress={onBackdropPress || onClose}>
524
+ <Pressable
525
+ style={[
526
+ styles.popupContainer,
527
+ countrySelectStyle?.popup?.popupContainer,
528
+ isFullScreen && {
529
+ flex: 1,
530
+ width: '100%',
531
+ height: '100%',
532
+ },
533
+ ]}>
534
+ <View
535
+ style={[
536
+ styles.popupContent,
537
+ countrySelectStyle?.popup?.popupContent,
538
+ isFullScreen && {
539
+ borderRadius: 0,
540
+ },
541
+ ]}>
542
+ {(isFullScreen || showSearchInput || showCloseButton) && (
543
+ <View
544
+ style={[
545
+ styles.searchContainer,
546
+ countrySelectStyle?.popup?.searchContainer,
547
+ ]}>
548
+ {(isFullScreen || showCloseButton) && renderCloseButton()}
549
+ {showSearchInput && renderSearchInput()}
550
+ </View>
551
+ )}
552
+
553
+ {renderFlatList()}
554
+ </View>
555
+ </Pressable>
556
+ </Pressable>
557
+ </Modal>
558
+ );
559
+ }
560
+
211
561
  return (
212
562
  <Modal
213
563
  visible={visible}
214
564
  transparent
215
- animationType="fade"
565
+ animationType="slide"
216
566
  onRequestClose={onClose}
217
567
  statusBarTranslucent
218
568
  {...props}>
219
- <Pressable
569
+ <View
220
570
  style={[
221
571
  styles.backdrop,
222
- {alignItems: 'center', justifyContent: 'center'},
223
572
  removedBackdrop && {backgroundColor: 'transparent'},
224
- ]}
225
- disabled={disabledBackdropPress || removedBackdrop}
226
- onPress={onBackdropPress || onClose}>
227
- <Pressable style={styles.popupContainer}>
228
- <View style={styles.popupContent}>
229
- <View style={styles.searchContainer}>
230
- <TextInput
231
- testID="countrySelectSearchInput"
232
- accessibilityRole="text"
233
- accessibilityLabel="Country Select Search Input"
234
- accessibilityHint="Type to search for a country"
235
- style={styles.searchInput}
236
- placeholder={
237
- translations.searchPlaceholder[
238
- language as ICountrySelectLanguages
239
- ]
240
- }
241
- placeholderTextColor={styles.searchInputPlaceholder.color}
242
- value={searchQuery}
243
- onChangeText={setSearchQuery}
244
- />
245
- </View>
246
-
247
- <FlatList
248
- testID="countrySelectList"
249
- accessibilityRole="list"
250
- accessibilityLabel="Country Select List"
251
- accessibilityHint="List of countries"
252
- data={getCountries}
253
- keyExtractor={keyExtractor}
254
- renderItem={renderItem}
255
- keyboardShouldPersistTaps="handled"
256
- showsVerticalScrollIndicator={
257
- showsVerticalScrollIndicator || false
258
- }
259
- />
573
+ ]}>
574
+ <Pressable
575
+ testID="countrySelectBackdrop"
576
+ accessibilityRole="button"
577
+ accessibilityLabel="Country Select Modal Backdrop"
578
+ accessibilityHint="Click to close the Country Select modal"
579
+ disabled={disabledBackdropPress || removedBackdrop}
580
+ style={{flex: 1}}
581
+ onPress={onBackdropPress || onClose}
582
+ />
583
+ <View style={styles.sheetContainer} pointerEvents="auto">
584
+ <View {...handlePanResponder.panHandlers} style={styles.dragHandle}>
585
+ <View style={styles.dragIndicator} />
260
586
  </View>
261
- </Pressable>
262
- </Pressable>
587
+ <Animated.View
588
+ style={[
589
+ styles.sheetContent,
590
+ {
591
+ height: sheetHeight,
592
+ minHeight: bottomSheetSize.minHeight,
593
+ maxHeight: bottomSheetSize.maxHeight,
594
+ },
595
+ ]}>
596
+ {(showSearchInput || showCloseButton) && (
597
+ <View style={styles.searchContainer}>
598
+ {showCloseButton && renderCloseButton()}
599
+ {showSearchInput && renderSearchInput()}
600
+ </View>
601
+ )}
602
+
603
+ <Animated.View style={{flex: 1}}>{renderFlatList()}</Animated.View>
604
+ </Animated.View>
605
+ </View>
606
+ </View>
263
607
  </Modal>
264
608
  );
265
609
  };
@@ -1,12 +1,13 @@
1
- import {StyleSheet} from 'react-native';
1
+ import {Platform, StatusBar, StyleSheet} from 'react-native';
2
2
 
3
- export const createStyles = (theme: 'light' | 'dark') =>
3
+ export const createStyles = theme =>
4
4
  StyleSheet.create({
5
5
  backdrop: {
6
6
  flex: 1,
7
7
  backgroundColor: 'rgba(0, 0, 0, 0.5)',
8
8
  },
9
9
  popupContainer: {
10
+ marginTop: StatusBar.currentHeight,
10
11
  width: '90%',
11
12
  maxWidth: 600,
12
13
  height: '60%',
@@ -22,15 +23,47 @@ export const createStyles = (theme: 'light' | 'dark') =>
22
23
  alignSelf: 'center',
23
24
  padding: 16,
24
25
  },
26
+ sheetContainer: {
27
+ marginTop: StatusBar.currentHeight,
28
+ position: 'absolute',
29
+ bottom: 0,
30
+ left: 0,
31
+ right: 0,
32
+ width: '100%',
33
+ alignItems: 'center',
34
+ },
35
+ sheetContent: {
36
+ width: '100%',
37
+ backgroundColor: theme === 'dark' ? '#202020' : '#FFFFFF',
38
+ padding: 16,
39
+ paddingTop: 0,
40
+ },
41
+ dragHandle: {
42
+ width: '100%',
43
+ height: 24,
44
+ justifyContent: 'center',
45
+ alignItems: 'center',
46
+ backgroundColor: theme === 'dark' ? '#202020' : '#FFFFFF',
47
+ borderTopLeftRadius: 20,
48
+ borderTopRightRadius: 20,
49
+ marginBottom: -1,
50
+ },
51
+ dragIndicator: {
52
+ width: 40,
53
+ height: 4,
54
+ backgroundColor: theme === 'dark' ? '#FFFFFF40' : '#00000040',
55
+ borderRadius: 2,
56
+ },
25
57
  searchContainer: {
26
58
  marginBottom: 16,
27
- paddingTop: 16,
59
+ paddingTop: 8,
28
60
  flexDirection: 'row',
29
61
  },
30
62
  searchInput: {
31
63
  flex: 1,
32
64
  borderRadius: 8,
33
65
  paddingHorizontal: 16,
66
+ minHeight: 44,
34
67
  fontSize: 16,
35
68
  borderColor: theme === 'dark' ? '#F3F3F3' : '#303030',
36
69
  borderWidth: 1,
@@ -57,6 +90,13 @@ export const createStyles = (theme: 'light' | 'dark') =>
57
90
  flex: 0.1,
58
91
  fontSize: 16,
59
92
  color: theme === 'dark' ? '#FFFFFF' : '#000000',
93
+ fontFamily:
94
+ Platform.OS === 'web'
95
+ ? typeof navigator !== 'undefined' &&
96
+ navigator?.userAgent?.includes('Win')
97
+ ? 'TwemojiMozilla'
98
+ : 'System'
99
+ : 'System',
60
100
  },
61
101
  countryInfo: {
62
102
  flex: 0.9,
@@ -80,4 +120,19 @@ export const createStyles = (theme: 'light' | 'dark') =>
80
120
  paddingVertical: 8,
81
121
  color: theme === 'dark' ? '#FFFFFF' : '#000000',
82
122
  },
123
+ closeButton: {
124
+ marginRight: 10,
125
+ paddingHorizontal: 18,
126
+ alignItems: 'center',
127
+ justifyContent: 'center',
128
+ backgroundColor: theme === 'dark' ? '#303030' : '#F3F3F3',
129
+ borderColor: theme === 'dark' ? '#F3F3F3' : '#303030',
130
+ borderWidth: 1,
131
+ borderRadius: 12,
132
+ },
133
+ closeButtonText: {
134
+ fontSize: 24,
135
+ lineHeight: 28,
136
+ color: theme === 'dark' ? '#FFFFFF' : '#000000',
137
+ },
83
138
  });
package/lib/index.d.ts CHANGED
@@ -5,9 +5,35 @@ import {
5
5
  ICountryCca2,
6
6
  ICountryItemProps,
7
7
  ICountrySelectProps,
8
+ ICountrySelectStyle,
8
9
  ICountrySelectLanguages,
9
10
  } from './interface';
10
11
 
12
+ declare function getAllCountries(): ICountry[];
13
+
14
+ declare function getCountryByCca2(cca2: string): ICountry | undefined;
15
+
16
+ declare function getCountryByCca3(cca3: string): ICountry | undefined;
17
+
18
+ declare function getCountriesByCallingCode(
19
+ callingCode: string,
20
+ ): ICountry[] | undefined;
21
+
22
+ declare function getCountriesByName(
23
+ name: string,
24
+ language: ICountrySelectLanguages,
25
+ ): ICountry[] | undefined;
26
+
27
+ declare function getCountriesByRegion(region: string): ICountry[] | undefined;
28
+
29
+ declare function getCountriesBySubregion(
30
+ subregion: string,
31
+ ): ICountry[] | undefined;
32
+
33
+ declare function getContriesDependents(cca2: string): ICountry[] | undefined;
34
+
35
+ declare function getCountriesIndependents(): ICountry[] | undefined;
36
+
11
37
  declare const CountrySelect: React.FC<ICountrySelectProps>;
12
38
 
13
39
  export default CountrySelect;
@@ -17,5 +43,15 @@ export {
17
43
  ICountryCca2,
18
44
  ICountryItemProps,
19
45
  ICountrySelectProps,
46
+ ICountrySelectStyle,
20
47
  ICountrySelectLanguages,
48
+ getAllCountries,
49
+ getCountryByCca2,
50
+ getCountryByCca3,
51
+ getCountriesByCallingCode,
52
+ getCountriesByName,
53
+ getCountriesByRegion,
54
+ getCountriesBySubregion,
55
+ getContriesDependents,
56
+ getCountriesIndependents,
21
57
  };
package/lib/index.tsx CHANGED
@@ -4,15 +4,40 @@ import {
4
4
  ICountryCca2,
5
5
  ICountryItemProps,
6
6
  ICountrySelectProps,
7
+ ICountrySelectStyle,
7
8
  ICountrySelectLanguages,
8
9
  } from './interface';
10
+ import {
11
+ getAllCountries,
12
+ getCountryByCca2,
13
+ getCountryByCca3,
14
+ getCountriesByCallingCode,
15
+ getCountriesByName,
16
+ getCountriesByRegion,
17
+ getCountriesBySubregion,
18
+ getContriesDependents,
19
+ getCountriesIndependents,
20
+ } from './utils/countryHelpers';
9
21
 
10
22
  export default CountrySelect;
11
23
 
24
+ export {
25
+ getAllCountries,
26
+ getCountryByCca2,
27
+ getCountryByCca3,
28
+ getCountriesByCallingCode,
29
+ getCountriesByName,
30
+ getCountriesByRegion,
31
+ getCountriesBySubregion,
32
+ getContriesDependents,
33
+ getCountriesIndependents,
34
+ };
35
+
12
36
  export type {
13
37
  ICountry,
14
38
  ICountryCca2,
15
39
  ICountryItemProps,
16
40
  ICountrySelectProps,
41
+ ICountrySelectStyle,
17
42
  ICountrySelectLanguages,
18
43
  };
@@ -1,4 +1,5 @@
1
1
  import {ICountry} from './country';
2
+ import {ICountrySelectStyle} from './countrySelectStyles';
2
3
  import {ICountrySelectLanguages} from './countrySelectLanguages';
3
4
 
4
5
  export interface ICountryItemProps {
@@ -7,4 +8,6 @@ export interface ICountryItemProps {
7
8
  onClose: () => void;
8
9
  language: ICountrySelectLanguages;
9
10
  theme?: 'light' | 'dark';
11
+ modalType?: 'bottomSheet' | 'popup';
12
+ countrySelectStyle?: ICountrySelectStyle;
10
13
  }
@@ -1,24 +1,35 @@
1
+ import * as React from 'react';
1
2
  import {ModalProps} from 'react-native';
2
-
3
3
  import {ICountry} from './country';
4
4
  import {ICountryCca2} from './countryCca2';
5
- import {ISectionTitle} from './sectionTitle';
6
5
  import {ICountrySelectLanguages} from './countrySelectLanguages';
6
+ import {ICountrySelectStyle} from './countrySelectStyles';
7
+ import {ISectionTitle} from './sectionTitle';
7
8
 
8
9
  export interface ICountrySelectProps extends ModalProps {
9
10
  visible: boolean;
10
11
  onClose: () => void;
11
12
  onSelect: (country: ICountry) => void;
13
+ modalType?: 'bottomSheet' | 'popup';
14
+ countrySelectStyle?: ICountrySelectStyle;
12
15
  theme?: 'light' | 'dark';
16
+ isFullScreen?: boolean;
13
17
  popularCountries?: string[];
14
18
  visibleCountries?: ICountryCca2[];
15
19
  hiddenCountries?: ICountryCca2[];
16
20
  language?: ICountrySelectLanguages;
21
+ showSearchInput?: boolean;
22
+ searchPlaceholder?: string;
23
+ showCloseButton?: boolean;
24
+ minBottomsheetHeight?: number | string;
25
+ maxBottomsheetHeight?: number | string;
26
+ initialBottomsheetHeight?: number | string;
17
27
  disabledBackdropPress?: boolean;
18
28
  removedBackdrop?: boolean;
19
29
  onBackdropPress?: () => void;
20
30
  countryItemComponent?: (item: ICountry) => React.ReactElement;
21
31
  sectionTitleComponent?: (item: ISectionTitle) => React.ReactElement;
32
+ closeButtonComponent?: () => React.ReactElement;
22
33
  popularCountriesTitle?: string;
23
34
  allCountriesTitle?: string;
24
35
  showsVerticalScrollIndicator?: boolean;
@@ -0,0 +1,34 @@
1
+ import {StyleProp, TextStyle, ViewStyle} from 'react-native';
2
+
3
+ interface IBaseModalStyle {
4
+ backdrop?: StyleProp<ViewStyle>;
5
+ closeButton?: StyleProp<ViewStyle>;
6
+ closeButtonText?: StyleProp<TextStyle>;
7
+ searchContainer?: StyleProp<ViewStyle>;
8
+ searchInput?: StyleProp<TextStyle>;
9
+ searchInputPlaceholder?: {
10
+ color: string;
11
+ };
12
+ sectionTitle?: StyleProp<TextStyle>;
13
+ list?: StyleProp<ViewStyle>;
14
+ countryItem?: StyleProp<ViewStyle>;
15
+ flag?: StyleProp<TextStyle>;
16
+ countryInfo?: StyleProp<ViewStyle>;
17
+ callingCode?: StyleProp<TextStyle>;
18
+ countryName?: StyleProp<TextStyle>;
19
+ }
20
+
21
+ interface IPopupStyle extends IBaseModalStyle {
22
+ popupContainer?: StyleProp<ViewStyle>;
23
+ popupContent?: StyleProp<ViewStyle>;
24
+ }
25
+
26
+ interface IBottomSheetStyle extends IBaseModalStyle {
27
+ sheetContainer?: StyleProp<ViewStyle>;
28
+ sheetContent?: StyleProp<ViewStyle>;
29
+ }
30
+
31
+ export interface ICountrySelectStyle {
32
+ popup?: IPopupStyle;
33
+ bottomSheet?: IBottomSheetStyle;
34
+ }
@@ -1,4 +1,3 @@
1
- // Re-export all interfaces from their respective files
2
1
  export * from './countryCca2';
3
2
  export * from './countrySelectLanguages';
4
3
  export * from './country';
@@ -6,3 +5,4 @@ export * from './countrySelectProps';
6
5
  export * from './countryItemProps';
7
6
  export * from './sectionTitle';
8
7
  export * from './itemList';
8
+ export * from './countrySelectStyles';
@@ -0,0 +1,59 @@
1
+ import {ICountry, ICountryCca2} from '../interface';
2
+ import countriesData from '../constants/countries.json';
3
+
4
+ const countries: ICountry[] = countriesData as unknown as ICountry[];
5
+
6
+ export const getAllCountries = (): ICountry[] => {
7
+ return countries;
8
+ };
9
+
10
+ export const getCountriesByCallingCode = (callingCode: string): ICountry[] => {
11
+ return countries.filter(
12
+ (country: ICountry) => country.idd.root === callingCode,
13
+ );
14
+ };
15
+
16
+ export const getCountriesByName = (
17
+ name: string,
18
+ language: keyof ICountry['translations'] = 'eng',
19
+ ): ICountry[] => {
20
+ return countries.filter((country: ICountry) => {
21
+ const translation = country.translations[language];
22
+ if (translation) {
23
+ return (
24
+ translation.common.toLowerCase().includes(name.toLowerCase()) ||
25
+ translation.official.toLowerCase().includes(name.toLowerCase())
26
+ );
27
+ }
28
+ return (
29
+ country.name.common.toLowerCase().includes(name.toLowerCase()) ||
30
+ country.name.official.toLowerCase().includes(name.toLowerCase())
31
+ );
32
+ });
33
+ };
34
+
35
+ export const getCountryByCca2 = (cca2: ICountryCca2): ICountry | undefined => {
36
+ return countries.find((country: ICountry) => country.cca2 === cca2);
37
+ };
38
+
39
+ export const getCountryByCca3 = (cca3: string): ICountry | undefined => {
40
+ return countries.find((country: ICountry) => country.cca3 === cca3);
41
+ };
42
+
43
+ export const getCountriesByRegion = (region: string): ICountry[] => {
44
+ return countries.filter((country: ICountry) => country.region === region);
45
+ };
46
+
47
+ export const getCountriesBySubregion = (subregion: string): ICountry[] => {
48
+ return countries.filter(
49
+ (country: ICountry) => country.subregion === subregion,
50
+ );
51
+ };
52
+
53
+ export const getCountriesIndependents = (): ICountry[] => {
54
+ return countries.filter((country: ICountry) => country.independent);
55
+ };
56
+
57
+ export const getContriesDependents = (): ICountry[] => {
58
+ return countries.filter((country: ICountry) => !country.independent);
59
+ };
@@ -0,0 +1,35 @@
1
+ const parseHeight = (
2
+ value: number | string | undefined,
3
+ windowHeight: number,
4
+ ): number => {
5
+ if (value === undefined) {
6
+ return 0;
7
+ }
8
+
9
+ const MIN_ALLOWED_PERCENTAGE = 0.1; // 10%
10
+ const MAX_ALLOWED_PERCENTAGE = windowHeight; // 100%
11
+
12
+ if (typeof value === 'number') {
13
+ return Math.min(
14
+ Math.max(value, MIN_ALLOWED_PERCENTAGE * MAX_ALLOWED_PERCENTAGE),
15
+ MAX_ALLOWED_PERCENTAGE,
16
+ );
17
+ }
18
+
19
+ if (typeof value === 'string') {
20
+ const percentageMatch = value.match(/^(\d+(?:\.\d+)?)%$/);
21
+
22
+ if (percentageMatch) {
23
+ const percentage = parseFloat(percentageMatch[1]) / 100;
24
+ const height = percentage * MAX_ALLOWED_PERCENTAGE;
25
+ return Math.min(
26
+ Math.max(height, MIN_ALLOWED_PERCENTAGE * MAX_ALLOWED_PERCENTAGE),
27
+ MAX_ALLOWED_PERCENTAGE,
28
+ );
29
+ }
30
+ }
31
+
32
+ return 0;
33
+ };
34
+
35
+ export default parseHeight;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-country-select",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "๐ŸŒ 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
5
  "main": "lib/index.tsx",
6
6
  "types": "lib/index.d.ts",