@avaiyakapil/react-native-country-picker 1.0.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.
@@ -0,0 +1,887 @@
1
+ import React, { useCallback, useMemo, useState, useRef, useEffect } from 'react';
2
+ import {
3
+ FlatList,
4
+ Image,
5
+ ImageStyle,
6
+ KeyboardAvoidingView,
7
+ Modal,
8
+ Platform,
9
+ Pressable,
10
+ StyleProp,
11
+ Text,
12
+ TextInput,
13
+ TextStyle,
14
+ View,
15
+ ViewStyle,
16
+ } from 'react-native';
17
+ import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
18
+ import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
19
+
20
+ import countriesJSON from './countries-emoji.json';
21
+ import { createStyles } from './styles';
22
+ import { Country, CountryCode } from './types';
23
+
24
+ const countriesData: Record<string, Country> = countriesJSON;
25
+ const allCountriesData: Country[] = Object.keys(countriesData).map(key => ({
26
+ countryCode: key as CountryCode,
27
+ currency: countriesData[key]?.currency ?? [''],
28
+ callingCode: countriesData[key]?.callingCode ?? [''],
29
+ region: countriesData[key]?.region ?? '',
30
+ subregion: countriesData[key]?.subregion ?? '',
31
+ flag: countriesData[key]?.flag ?? '',
32
+ name: countriesData[key]?.name ?? { common: '' },
33
+ }));
34
+
35
+ interface CountryPickerProps {
36
+ countryCode: CountryCode;
37
+ onSelect: (countryCode: CountryCode, country: Country) => void;
38
+
39
+ // Colors
40
+ colors?: {
41
+ grayLight?: string;
42
+ white?: string;
43
+ grayBackground?: string;
44
+ gray?: string;
45
+ dark?: string;
46
+ };
47
+
48
+ // Text customization
49
+ headerText?: string;
50
+ searchPlaceholder?: string;
51
+ otherText?: string;
52
+
53
+ // Icon customization
54
+ iconColor?: string;
55
+ dropdownIconName?: string;
56
+ dropdownIconSize?: number;
57
+ closeIconName?: string;
58
+ closeIconSize?: number;
59
+ searchIconName?: string;
60
+ searchIconSize?: number;
61
+ clearIconName?: string;
62
+ clearIconSize?: number;
63
+ checkIconName?: string;
64
+ checkIconSize?: number;
65
+
66
+ // Display options
67
+ showFlag?: boolean;
68
+ showCallingCode?: boolean;
69
+ showCountryName?: boolean;
70
+ flagSize?: number;
71
+
72
+ // Modal customization
73
+ modalAnimationType?: 'none' | 'slide' | 'fade';
74
+ modalPresentationStyle?: 'fullScreen' | 'pageSheet' | 'formSheet' | 'overFullScreen';
75
+
76
+ // Search customization
77
+ enableSearch?: boolean;
78
+ onSearch?: (searchText: string, filteredCountries: Country[]) => void;
79
+ customSearchFilter?: (item: Country, searchText: string) => boolean;
80
+
81
+ // Filtering
82
+ excludedCountries?: CountryCode[];
83
+ includedCountries?: CountryCode[];
84
+ customCountryList?: Country[];
85
+
86
+ // Callbacks
87
+ onOpen?: () => void;
88
+ onClose?: () => void;
89
+
90
+ // Custom render functions
91
+ renderFlag?: (flagUri: string, style?: StyleProp<ImageStyle>) => React.ReactNode;
92
+ renderCountryName?: (country: Country) => React.ReactNode;
93
+ renderCallingCode?: (callingCode: string) => React.ReactNode;
94
+ renderListItem?: (country: Country, isSelected: boolean, onPress: () => void) => React.ReactNode;
95
+ renderSelectedCountry?: (country: Country) => React.ReactNode;
96
+
97
+ // Custom styles
98
+ containerStyle?: StyleProp<ViewStyle>;
99
+ flagStyle?: StyleProp<ImageStyle>;
100
+ callingCodeStyle?: StyleProp<TextStyle>;
101
+ modalStyle?: StyleProp<ViewStyle>;
102
+ searchInputStyle?: StyleProp<TextStyle>;
103
+ listItemStyle?: StyleProp<ViewStyle>;
104
+ headerStyle?: StyleProp<ViewStyle>;
105
+ headerTextStyle?: StyleProp<TextStyle>;
106
+ searchContainerStyle?: StyleProp<ViewStyle>;
107
+ listContainerStyle?: StyleProp<ViewStyle>;
108
+ selectedItemStyle?: StyleProp<ViewStyle>;
109
+
110
+ // Typography & Sizing
111
+ fontSize?: {
112
+ callingCode?: number;
113
+ countryName?: number;
114
+ header?: number;
115
+ search?: number;
116
+ listItem?: number;
117
+ otherSection?: number;
118
+ };
119
+ fontWeight?: {
120
+ callingCode?: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
121
+ countryName?: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
122
+ header?: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
123
+ };
124
+ containerHeight?: number;
125
+ containerWidth?: number | string;
126
+ modalMaxHeight?: number;
127
+ listItemHeight?: number;
128
+
129
+ // Spacing & Layout
130
+ spacing?: {
131
+ containerPadding?: number;
132
+ flagMargin?: number;
133
+ callingCodeMargin?: number;
134
+ headerPadding?: number;
135
+ searchMargin?: number;
136
+ listItemMargin?: number;
137
+ listItemPadding?: number;
138
+ };
139
+ borderRadius?: {
140
+ container?: number;
141
+ flag?: number;
142
+ search?: number;
143
+ listItem?: number;
144
+ modal?: number;
145
+ };
146
+
147
+ // Border & Shadow
148
+ borderWidth?: {
149
+ container?: number;
150
+ search?: number;
151
+ listItem?: number;
152
+ };
153
+ shadow?: {
154
+ container?: boolean;
155
+ modal?: boolean;
156
+ listItem?: boolean;
157
+ };
158
+ elevation?: {
159
+ container?: number;
160
+ modal?: number;
161
+ listItem?: number;
162
+ };
163
+
164
+ // Data & Sorting
165
+ preferredCountries?: CountryCode[];
166
+ sortCountries?: 'name' | 'callingCode' | 'region' | 'custom' | 'none';
167
+ customSortFunction?: (a: Country, b: Country) => number;
168
+ groupByRegion?: boolean;
169
+ showRegionHeaders?: boolean;
170
+ showCurrency?: boolean;
171
+ showSubregion?: boolean;
172
+
173
+ // Behavior
174
+ autoFocusSearch?: boolean;
175
+ closeOnSelect?: boolean;
176
+ scrollToSelected?: boolean;
177
+ highlightSelected?: boolean;
178
+ selectedItemBackgroundColor?: string;
179
+ selectedItemTextColor?: string;
180
+ emptyStateText?: string;
181
+ emptyStateComponent?: React.ReactNode;
182
+
183
+ // Performance
184
+ initialNumToRender?: number;
185
+ maxToRenderPerBatch?: number;
186
+ windowSize?: number;
187
+ removeClippedSubviews?: boolean;
188
+ updateCellsBatchingPeriod?: number;
189
+
190
+ // List Configuration
191
+ keyExtractor?: (item: Country, index: number) => string;
192
+ getItemLayout?: (data: Country[] | null | undefined, index: number) => { length: number; offset: number; index: number };
193
+ ListHeaderComponent?: React.ComponentType<any> | React.ReactElement | null;
194
+ ListFooterComponent?: React.ComponentType<any> | React.ReactElement | null;
195
+ ListEmptyComponent?: React.ComponentType<any> | React.ReactElement | null;
196
+ ItemSeparatorComponent?: React.ComponentType<any> | React.ReactElement | null;
197
+
198
+ // Accessibility
199
+ accessibilityLabel?: {
200
+ container?: string;
201
+ search?: string;
202
+ closeButton?: string;
203
+ listItem?: (country: Country) => string;
204
+ };
205
+ testID?: {
206
+ container?: string;
207
+ search?: string;
208
+ closeButton?: string;
209
+ modal?: string;
210
+ list?: string;
211
+ };
212
+
213
+ // Advanced Styling
214
+ theme?: 'light' | 'dark' | 'auto';
215
+ rippleColor?: string; // Android ripple effect
216
+ activeOpacity?: number;
217
+ underlayColor?: string;
218
+
219
+ // Other options
220
+ disabled?: boolean;
221
+ keyboardVerticalOffset?: number;
222
+ showOtherSection?: boolean;
223
+ otherSectionIndex?: number;
224
+ loading?: boolean;
225
+ loadingComponent?: React.ReactNode;
226
+ }
227
+
228
+ export const CountryPicker: React.FC<CountryPickerProps> = ({
229
+ countryCode,
230
+ onSelect,
231
+ colors,
232
+ headerText = 'Country/region',
233
+ searchPlaceholder = 'Search country/region',
234
+ otherText = 'Other',
235
+ iconColor = '#000000',
236
+ dropdownIconName = 'keyboard-arrow-down',
237
+ dropdownIconSize = 24,
238
+ closeIconName = 'close',
239
+ closeIconSize = 24,
240
+ searchIconName = 'search',
241
+ searchIconSize = 20,
242
+ clearIconName = 'cancel',
243
+ clearIconSize = 20,
244
+ checkIconName = 'check',
245
+ checkIconSize = 24,
246
+ showFlag = true,
247
+ showCallingCode = true,
248
+ showCountryName = true,
249
+ flagSize = 28,
250
+ modalAnimationType = 'slide',
251
+ modalPresentationStyle = 'fullScreen',
252
+ enableSearch = true,
253
+ onSearch,
254
+ customSearchFilter,
255
+ excludedCountries = [],
256
+ includedCountries,
257
+ customCountryList,
258
+ onOpen,
259
+ onClose,
260
+ renderFlag,
261
+ renderCountryName,
262
+ renderCallingCode,
263
+ renderListItem,
264
+ renderSelectedCountry,
265
+ containerStyle,
266
+ flagStyle,
267
+ callingCodeStyle,
268
+ modalStyle,
269
+ searchInputStyle,
270
+ listItemStyle,
271
+ headerStyle,
272
+ headerTextStyle,
273
+ searchContainerStyle,
274
+ listContainerStyle,
275
+ selectedItemStyle,
276
+ fontSize,
277
+ fontWeight,
278
+ containerHeight,
279
+ containerWidth,
280
+ modalMaxHeight,
281
+ listItemHeight,
282
+ spacing,
283
+ borderRadius,
284
+ borderWidth,
285
+ shadow,
286
+ elevation,
287
+ preferredCountries,
288
+ sortCountries = 'name',
289
+ customSortFunction,
290
+ groupByRegion = false,
291
+ showRegionHeaders = false,
292
+ showCurrency = false,
293
+ showSubregion = false,
294
+ autoFocusSearch = false,
295
+ closeOnSelect = true,
296
+ scrollToSelected = false,
297
+ highlightSelected = true,
298
+ selectedItemBackgroundColor,
299
+ selectedItemTextColor,
300
+ emptyStateText = 'No countries found',
301
+ emptyStateComponent,
302
+ initialNumToRender = 10,
303
+ maxToRenderPerBatch = 10,
304
+ windowSize = 21,
305
+ removeClippedSubviews = true,
306
+ updateCellsBatchingPeriod = 50,
307
+ keyExtractor,
308
+ getItemLayout,
309
+ ListHeaderComponent,
310
+ ListFooterComponent,
311
+ ListEmptyComponent,
312
+ ItemSeparatorComponent,
313
+ accessibilityLabel,
314
+ testID,
315
+ theme = 'light',
316
+ rippleColor,
317
+ activeOpacity = 0.7,
318
+ underlayColor,
319
+ disabled = false,
320
+ keyboardVerticalOffset = 0,
321
+ showOtherSection = false,
322
+ otherSectionIndex = 3,
323
+ loading = false,
324
+ loadingComponent,
325
+ }) => {
326
+ const [openCountryPicker, setOpenCountryPicker] = useState<boolean>(false);
327
+ const [search, setSearch] = useState<string>('');
328
+
329
+ const styles = useMemo(() => createStyles(colors), [colors]);
330
+
331
+ // Filter, sort, and organize countries
332
+ const availableCountries = useMemo(() => {
333
+ let filtered: Country[];
334
+
335
+ if (customCountryList) {
336
+ filtered = customCountryList;
337
+ } else {
338
+ filtered = allCountriesData;
339
+ if (includedCountries && includedCountries.length > 0) {
340
+ filtered = filtered.filter(country =>
341
+ includedCountries.includes(country.countryCode as CountryCode)
342
+ );
343
+ }
344
+ if (excludedCountries && excludedCountries.length > 0) {
345
+ filtered = filtered.filter(
346
+ country => !excludedCountries.includes(country.countryCode as CountryCode)
347
+ );
348
+ }
349
+ }
350
+
351
+ // Sort countries
352
+ if (sortCountries !== 'none') {
353
+ if (sortCountries === 'custom' && customSortFunction) {
354
+ filtered = [...filtered].sort(customSortFunction);
355
+ } else if (sortCountries === 'name') {
356
+ filtered = [...filtered].sort((a, b) =>
357
+ (a.name?.common || '').localeCompare(b.name?.common || '')
358
+ );
359
+ } else if (sortCountries === 'callingCode') {
360
+ filtered = [...filtered].sort((a, b) =>
361
+ (a.callingCode[0] || '').localeCompare(b.callingCode[0] || '')
362
+ );
363
+ } else if (sortCountries === 'region') {
364
+ filtered = [...filtered].sort((a, b) => {
365
+ const regionCompare = (a.region || '').localeCompare(b.region || '');
366
+ if (regionCompare !== 0) return regionCompare;
367
+ return (a.name?.common || '').localeCompare(b.name?.common || '');
368
+ });
369
+ }
370
+ }
371
+
372
+ // Move preferred countries to top (keep them sorted alphabetically)
373
+ if (preferredCountries && preferredCountries.length > 0) {
374
+ const preferred = filtered
375
+ .filter(country =>
376
+ preferredCountries.includes(country.countryCode as CountryCode)
377
+ )
378
+ .sort((a, b) =>
379
+ (a.name?.common || '').localeCompare(b.name?.common || '')
380
+ );
381
+ const others = filtered.filter(
382
+ country => !preferredCountries.includes(country.countryCode as CountryCode)
383
+ );
384
+ filtered = [...preferred, ...others];
385
+ }
386
+
387
+ return filtered;
388
+ }, [
389
+ customCountryList,
390
+ includedCountries,
391
+ excludedCountries,
392
+ sortCountries,
393
+ customSortFunction,
394
+ preferredCountries,
395
+ ]);
396
+
397
+ const [countries, setCountries] = useState<Country[]>(availableCountries);
398
+
399
+ const selected = useMemo(
400
+ () => countriesData[countryCode] ?? countriesJSON.BE,
401
+ [countryCode],
402
+ );
403
+
404
+ const onSearchCountry = useCallback(
405
+ (text: string) => {
406
+ setSearch(text);
407
+ if (text) {
408
+ let result: Country[];
409
+ if (customSearchFilter) {
410
+ result = availableCountries.filter(item =>
411
+ customSearchFilter(item, text)
412
+ );
413
+ } else {
414
+ result = availableCountries.filter(item => {
415
+ const itemData = item.name?.common?.toLowerCase() ?? '';
416
+ const searchText = text.toLowerCase();
417
+ return itemData.indexOf(searchText) > -1;
418
+ });
419
+ }
420
+ setCountries(result);
421
+ onSearch?.(text, result);
422
+ } else {
423
+ setCountries(availableCountries);
424
+ onSearch?.('', availableCountries);
425
+ }
426
+ },
427
+ [availableCountries, customSearchFilter, onSearch]
428
+ );
429
+
430
+ const handleOpen = useCallback(() => {
431
+ if (disabled) return;
432
+ setOpenCountryPicker(true);
433
+ setCountries(availableCountries);
434
+ onOpen?.();
435
+ }, [disabled, availableCountries, onOpen]);
436
+
437
+ const handleClose = useCallback(() => {
438
+ setOpenCountryPicker(false);
439
+ onSearchCountry('');
440
+ onClose?.();
441
+ }, [onSearchCountry, onClose]);
442
+
443
+ const handleSelect = useCallback(
444
+ (code: CountryCode, country: Country) => {
445
+ onSelect(code, country);
446
+ if (closeOnSelect) {
447
+ handleClose();
448
+ }
449
+ },
450
+ [onSelect, handleClose, closeOnSelect]
451
+ );
452
+
453
+ // Create dynamic styles with all the new props
454
+ const dynamicContainerStyle = useMemo(
455
+ () => [
456
+ styles.container,
457
+ containerHeight && { height: containerHeight },
458
+ containerWidth && { width: containerWidth },
459
+ spacing?.containerPadding && { padding: spacing.containerPadding },
460
+ borderRadius?.container && { borderRadius: borderRadius.container },
461
+ borderWidth?.container && { borderWidth: borderWidth.container },
462
+ shadow?.container && {
463
+ shadowColor: '#000',
464
+ shadowOffset: { width: 0, height: 2 },
465
+ shadowOpacity: 0.1,
466
+ shadowRadius: 4,
467
+ },
468
+ elevation?.container && Platform.OS === 'android' && {
469
+ elevation: elevation.container,
470
+ },
471
+ containerStyle,
472
+ ],
473
+ [
474
+ styles.container,
475
+ containerHeight,
476
+ containerWidth,
477
+ spacing?.containerPadding,
478
+ borderRadius?.container,
479
+ borderWidth?.container,
480
+ shadow?.container,
481
+ elevation?.container,
482
+ containerStyle,
483
+ ]
484
+ );
485
+
486
+ const dynamicModalStyle = useMemo(
487
+ () => [
488
+ styles.view,
489
+ modalMaxHeight && { maxHeight: modalMaxHeight },
490
+ borderRadius?.modal && { borderRadius: borderRadius.modal },
491
+ shadow?.modal && {
492
+ shadowColor: '#000',
493
+ shadowOffset: { width: 0, height: 2 },
494
+ shadowOpacity: 0.1,
495
+ shadowRadius: 4,
496
+ },
497
+ elevation?.modal && Platform.OS === 'android' && {
498
+ elevation: elevation.modal,
499
+ },
500
+ modalStyle,
501
+ ],
502
+ [
503
+ styles.view,
504
+ modalMaxHeight,
505
+ borderRadius?.modal,
506
+ shadow?.modal,
507
+ elevation?.modal,
508
+ modalStyle,
509
+ ]
510
+ );
511
+
512
+ const dynamicSearchStyle = useMemo(
513
+ () => [
514
+ styles.searchView,
515
+ spacing?.searchMargin && {
516
+ marginTop: spacing.searchMargin,
517
+ marginBottom: spacing.searchMargin,
518
+ marginHorizontal: spacing.searchMargin,
519
+ },
520
+ borderRadius?.search && { borderRadius: borderRadius.search },
521
+ borderWidth?.search && { borderWidth: borderWidth.search },
522
+ searchContainerStyle,
523
+ ],
524
+ [
525
+ styles.searchView,
526
+ spacing?.searchMargin,
527
+ borderRadius?.search,
528
+ borderWidth?.search,
529
+ searchContainerStyle,
530
+ ]
531
+ );
532
+
533
+ const dynamicSearchInputStyle = useMemo(
534
+ () => [
535
+ styles.search,
536
+ fontSize?.search && { fontSize: fontSize.search },
537
+ searchInputStyle,
538
+ ],
539
+ [styles.search, fontSize?.search, searchInputStyle]
540
+ );
541
+
542
+ const dynamicHeaderStyle = useMemo(
543
+ () => [
544
+ styles.flexView,
545
+ spacing?.headerPadding && { paddingHorizontal: spacing.headerPadding },
546
+ headerStyle,
547
+ ],
548
+ [styles.flexView, spacing?.headerPadding, headerStyle]
549
+ );
550
+
551
+ const dynamicHeaderTextStyle = useMemo(
552
+ () => [
553
+ styles.headerText,
554
+ fontSize?.header && { fontSize: fontSize.header },
555
+ fontWeight?.header && { fontWeight: fontWeight.header },
556
+ headerTextStyle,
557
+ ],
558
+ [styles.headerText, fontSize?.header, fontWeight?.header, headerTextStyle]
559
+ );
560
+
561
+ const dynamicCallingCodeStyle = useMemo(
562
+ () => [
563
+ styles.callingCode,
564
+ fontSize?.callingCode && { fontSize: fontSize.callingCode },
565
+ fontWeight?.callingCode && { fontWeight: fontWeight.callingCode },
566
+ spacing?.callingCodeMargin && { marginLeft: spacing.callingCodeMargin },
567
+ callingCodeStyle,
568
+ ],
569
+ [
570
+ styles.callingCode,
571
+ fontSize?.callingCode,
572
+ fontWeight?.callingCode,
573
+ spacing?.callingCodeMargin,
574
+ callingCodeStyle,
575
+ ]
576
+ );
577
+
578
+ const dynamicListItemStyle = useMemo(
579
+ () => [
580
+ styles.listViewContainer,
581
+ spacing?.listItemMargin && {
582
+ marginHorizontal: spacing.listItemMargin,
583
+ marginVertical: spacing.listItemMargin,
584
+ },
585
+ spacing?.listItemPadding && { padding: spacing.listItemPadding },
586
+ borderRadius?.listItem && { borderRadius: borderRadius.listItem },
587
+ borderWidth?.listItem && { borderWidth: borderWidth.listItem },
588
+ shadow?.listItem && {
589
+ shadowColor: '#000',
590
+ shadowOffset: { width: 0, height: 1 },
591
+ shadowOpacity: 0.05,
592
+ shadowRadius: 2,
593
+ },
594
+ elevation?.listItem && Platform.OS === 'android' && {
595
+ elevation: elevation.listItem,
596
+ },
597
+ listItemStyle,
598
+ ],
599
+ [
600
+ styles.listViewContainer,
601
+ spacing?.listItemMargin,
602
+ spacing?.listItemPadding,
603
+ borderRadius?.listItem,
604
+ borderWidth?.listItem,
605
+ shadow?.listItem,
606
+ elevation?.listItem,
607
+ listItemStyle,
608
+ ]
609
+ );
610
+
611
+ const dynamicCountryNameStyle = useMemo(
612
+ () => [
613
+ styles.name,
614
+ fontSize?.listItem && { fontSize: fontSize.listItem },
615
+ fontWeight?.countryName && { fontWeight: fontWeight.countryName },
616
+ spacing?.flagMargin && { marginLeft: spacing.flagMargin },
617
+ ],
618
+ [
619
+ styles.name,
620
+ fontSize?.listItem,
621
+ fontWeight?.countryName,
622
+ spacing?.flagMargin,
623
+ ]
624
+ );
625
+
626
+ const flagButtonStyle = useMemo(
627
+ () => [
628
+ styles.flagButton,
629
+ {
630
+ height: flagSize,
631
+ width: flagSize,
632
+ borderRadius: borderRadius?.flag ?? flagSize / 2,
633
+ },
634
+ spacing?.flagMargin && { marginLeft: spacing.flagMargin },
635
+ flagStyle,
636
+ ],
637
+ [styles.flagButton, flagSize, borderRadius?.flag, spacing?.flagMargin, flagStyle]
638
+ );
639
+
640
+ const flagListStyle = useMemo(
641
+ () => [
642
+ styles.flag,
643
+ {
644
+ height: flagSize,
645
+ width: flagSize,
646
+ borderRadius: borderRadius?.flag ?? flagSize / 2,
647
+ },
648
+ ],
649
+ [styles.flag, flagSize, borderRadius?.flag]
650
+ );
651
+
652
+ const renderDefaultFlag = (flagUri: string, style?: StyleProp<ImageStyle>) => (
653
+ <Image style={[flagButtonStyle, style]} source={{ uri: flagUri }} />
654
+ );
655
+
656
+ const renderDefaultCountryName = (country: Country) => (
657
+ <Text style={dynamicCountryNameStyle}>
658
+ {country?.name?.common}
659
+ {showCallingCode && country.callingCode[0]
660
+ ? ` (+${country?.callingCode[0]})`
661
+ : ''}
662
+ {showCurrency && country.currency[0] ? ` - ${country.currency[0]}` : ''}
663
+ {showSubregion && country.subregion ? ` (${country.subregion})` : ''}
664
+ </Text>
665
+ );
666
+
667
+ const renderDefaultCallingCode = (callingCode: string) => (
668
+ <Text style={dynamicCallingCodeStyle}>
669
+ {callingCode ? `+${callingCode}` : ''}
670
+ </Text>
671
+ );
672
+
673
+ const searchInputRef = useRef<TextInput>(null);
674
+
675
+ useEffect(() => {
676
+ if (openCountryPicker && autoFocusSearch && enableSearch) {
677
+ setTimeout(() => {
678
+ searchInputRef.current?.focus();
679
+ }, 100);
680
+ }
681
+ }, [openCountryPicker, autoFocusSearch, enableSearch]);
682
+
683
+ return (
684
+ <>
685
+ {loading && loadingComponent ? (
686
+ loadingComponent
687
+ ) : (
688
+ <>
689
+ <Pressable
690
+ onPress={handleOpen}
691
+ style={dynamicContainerStyle}
692
+ disabled={disabled}
693
+ accessibilityLabel={accessibilityLabel?.container}
694
+ testID={testID?.container}
695
+ android_ripple={rippleColor ? { color: rippleColor } : undefined}
696
+ activeOpacity={activeOpacity}
697
+ underlayColor={underlayColor}
698
+ >
699
+ {renderSelectedCountry ? (
700
+ renderSelectedCountry(selected)
701
+ ) : (
702
+ <>
703
+ {showFlag &&
704
+ (renderFlag
705
+ ? renderFlag(selected?.flag, flagButtonStyle)
706
+ : renderDefaultFlag(selected?.flag))}
707
+ {showCallingCode &&
708
+ (renderCallingCode
709
+ ? renderCallingCode(selected.callingCode[0] || '')
710
+ : renderDefaultCallingCode(selected.callingCode[0] || ''))}
711
+ {showCountryName && (
712
+ <Text style={dynamicCountryNameStyle}>
713
+ {selected?.name?.common}
714
+ </Text>
715
+ )}
716
+ <MaterialIcons
717
+ name={dropdownIconName}
718
+ size={dropdownIconSize}
719
+ color={iconColor}
720
+ style={styles.dropdownIcon}
721
+ />
722
+ </>
723
+ )}
724
+ </Pressable>
725
+ <Modal
726
+ visible={openCountryPicker}
727
+ statusBarTranslucent
728
+ animationType={modalAnimationType}
729
+ presentationStyle={modalPresentationStyle}
730
+ >
731
+ <SafeAreaProvider>
732
+ <SafeAreaView style={dynamicModalStyle} testID={testID?.modal}>
733
+ <View style={dynamicHeaderStyle}>
734
+ <Text style={dynamicHeaderTextStyle}>{headerText}</Text>
735
+ <Pressable
736
+ onPress={handleClose}
737
+ style={styles.close}
738
+ accessibilityLabel={accessibilityLabel?.closeButton}
739
+ testID={testID?.closeButton}
740
+ >
741
+ <MaterialIcons
742
+ name={closeIconName}
743
+ size={closeIconSize}
744
+ color={iconColor}
745
+ />
746
+ </Pressable>
747
+ </View>
748
+ <KeyboardAvoidingView
749
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
750
+ style={{ flex: 1 }}
751
+ keyboardVerticalOffset={keyboardVerticalOffset}
752
+ >
753
+ {enableSearch && (
754
+ <View style={dynamicSearchStyle}>
755
+ <MaterialIcons
756
+ name={searchIconName}
757
+ size={searchIconSize}
758
+ color={colors?.gray || '#999999'}
759
+ />
760
+ <TextInput
761
+ ref={searchInputRef}
762
+ placeholder={searchPlaceholder}
763
+ placeholderTextColor={colors?.gray || '#999999'}
764
+ style={dynamicSearchInputStyle}
765
+ value={search}
766
+ onChangeText={text => onSearchCountry(text)}
767
+ accessibilityLabel={accessibilityLabel?.search}
768
+ testID={testID?.search}
769
+ />
770
+ {search ? (
771
+ <Pressable
772
+ style={styles.clearText}
773
+ onPress={() => onSearchCountry('')}
774
+ >
775
+ <MaterialIcons
776
+ name={clearIconName}
777
+ size={clearIconSize}
778
+ color={colors?.gray || '#999999'}
779
+ />
780
+ </Pressable>
781
+ ) : null}
782
+ </View>
783
+ )}
784
+ <FlatList
785
+ data={countries}
786
+ keyboardShouldPersistTaps="handled"
787
+ keyboardDismissMode="interactive"
788
+ contentContainerStyle={[styles.listContainer, listContainerStyle]}
789
+ renderItem={({ item, index }) => {
790
+ const isSelected =
791
+ selected?.callingCode === item?.callingCode;
792
+
793
+ if (renderListItem) {
794
+ return (
795
+ <View key={keyExtractor ? keyExtractor(item, index) : index.toString()}>
796
+ {renderListItem(item, isSelected, () =>
797
+ handleSelect(item?.countryCode ?? 'BE', item)
798
+ )}
799
+ </View>
800
+ );
801
+ }
802
+
803
+ const itemStyle = [
804
+ dynamicListItemStyle,
805
+ isSelected && highlightSelected && selectedItemStyle,
806
+ isSelected &&
807
+ highlightSelected &&
808
+ selectedItemBackgroundColor && {
809
+ backgroundColor: selectedItemBackgroundColor,
810
+ },
811
+ ];
812
+
813
+ return (
814
+ <View
815
+ key={keyExtractor ? keyExtractor(item, index) : index.toString()}
816
+ style={itemStyle}
817
+ >
818
+ <Pressable
819
+ onPress={() => handleSelect(item?.countryCode ?? 'BE', item)}
820
+ style={styles.listView}
821
+ accessibilityLabel={
822
+ accessibilityLabel?.listItem
823
+ ? accessibilityLabel.listItem(item)
824
+ : `${item.name?.common} ${item.callingCode[0] ? `+${item.callingCode[0]}` : ''}`
825
+ }
826
+ android_ripple={rippleColor ? { color: rippleColor } : undefined}
827
+ activeOpacity={activeOpacity}
828
+ >
829
+ <View style={styles.listIconView}>
830
+ {showFlag &&
831
+ (renderFlag
832
+ ? renderFlag(item?.flag, flagListStyle)
833
+ : (
834
+ <Image
835
+ style={flagListStyle}
836
+ source={{ uri: item?.flag }}
837
+ />
838
+ ))}
839
+ {showCountryName &&
840
+ (renderCountryName
841
+ ? renderCountryName(item)
842
+ : renderDefaultCountryName(item))}
843
+ </View>
844
+ {isSelected ? (
845
+ <MaterialIcons
846
+ name={checkIconName}
847
+ size={checkIconSize}
848
+ color={selectedItemTextColor || iconColor}
849
+ />
850
+ ) : null}
851
+ </Pressable>
852
+ </View>
853
+ );
854
+ }}
855
+ keyExtractor={keyExtractor || ((item, index) => item.countryCode || index.toString())}
856
+ getItemLayout={getItemLayout}
857
+ ListHeaderComponent={ListHeaderComponent}
858
+ ListFooterComponent={ListFooterComponent}
859
+ ListEmptyComponent={
860
+ ListEmptyComponent ||
861
+ (emptyStateComponent || (
862
+ <View style={{ padding: 20, alignItems: 'center' }}>
863
+ <Text style={{ color: colors?.gray || '#999999' }}>
864
+ {emptyStateText}
865
+ </Text>
866
+ </View>
867
+ ))
868
+ }
869
+ ItemSeparatorComponent={ItemSeparatorComponent}
870
+ initialNumToRender={initialNumToRender}
871
+ maxToRenderPerBatch={maxToRenderPerBatch}
872
+ windowSize={windowSize}
873
+ removeClippedSubviews={removeClippedSubviews}
874
+ updateCellsBatchingPeriod={updateCellsBatchingPeriod}
875
+ testID={testID?.list}
876
+ />
877
+ </KeyboardAvoidingView>
878
+ </SafeAreaView>
879
+ </SafeAreaProvider>
880
+ </Modal>
881
+ </>
882
+ )}
883
+ </>
884
+ );
885
+ };
886
+
887
+ export default CountryPicker;