react-native-country-select 0.2.6 → 0.3.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.
@@ -1,56 +1,42 @@
1
1
  /* eslint-disable react-native/no-inline-styles */
2
2
  /* eslint-disable react-hooks/exhaustive-deps */
3
- import React, {useCallback, useMemo, useState, useRef, useEffect} from 'react';
4
- import {
5
- View,
6
- TextInput,
7
- FlatList,
8
- useWindowDimensions,
9
- Pressable,
10
- Animated,
11
- PanResponder,
12
- ListRenderItem,
13
- Modal,
14
- Keyboard,
15
- Text,
16
- TouchableOpacity,
17
- } from 'react-native';
3
+ import React, {useCallback, useMemo, useState, useRef} from 'react';
4
+ import {View, FlatList, ListRenderItem, Text} from 'react-native';
18
5
 
6
+ import {PopupModal} from '../PopupModal';
19
7
  import {CountryItem} from '../CountryItem';
8
+ import {CloseButton} from '../CloseButton';
9
+ import {SearchInput} from '../SearchInput';
10
+ import {FullscreenModal} from '../FullscreenModal';
11
+ import {BottomSheetModal} from '../BottomSheetModal';
12
+ import {AlphabeticFilter} from '../AlphabeticFilter';
20
13
 
21
14
  import {createStyles} from '../styles';
22
- import parseHeight from '../../utils/parseHeight';
23
- import countries from '../../constants/countries.json';
24
15
  import {translations} from '../../utils/getTranslation';
16
+ import {getCountriesList} from '../../utils/getCountriesList';
25
17
  import {
26
18
  ICountry,
27
19
  ICountrySelectProps,
28
20
  ICountrySelectLanguages,
29
21
  IListItem,
22
+ IThemeProps,
30
23
  } from '../../interface';
31
24
 
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
-
39
- const DEFAULT_LANGUAGE: ICountrySelectLanguages = 'eng';
40
-
41
25
  export const CountrySelect: React.FC<ICountrySelectProps> = ({
42
26
  visible,
43
27
  onClose,
44
28
  onSelect,
45
29
  modalType = 'bottomSheet',
46
30
  theme = 'light',
31
+ isMultiSelect = false,
47
32
  isFullScreen = false,
48
33
  countrySelectStyle,
49
34
  popularCountries = [],
50
35
  visibleCountries = [],
51
36
  hiddenCountries = [],
52
- language = DEFAULT_LANGUAGE,
37
+ language = 'eng',
53
38
  showSearchInput = true,
39
+ showAlphabetFilter = false,
54
40
  searchPlaceholder,
55
41
  searchPlaceholderTextColor,
56
42
  searchSelectionColor,
@@ -79,368 +65,196 @@ export const CountrySelect: React.FC<ICountrySelectProps> = ({
79
65
  accessibilityHintCountriesList,
80
66
  accessibilityLabelCountryItem,
81
67
  accessibilityHintCountryItem,
68
+ accessibilityLabelAlphabetFilter,
69
+ accessibilityHintAlphabetFilter,
70
+ accessibilityLabelAlphabetLetter,
71
+ accessibilityHintAlphabetLetter,
82
72
  ...props
83
73
  }) => {
84
- const [modalHeight, setModalHeight] = useState(useWindowDimensions().height);
85
- const styles = createStyles(theme, modalType, isFullScreen);
86
-
87
74
  const [searchQuery, setSearchQuery] = useState('');
88
- const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
89
- const [bottomSheetSize, setBottomSheetSize] = useState({
90
- minHeight: MIN_HEIGHT_PERCENTAGE * modalHeight,
91
- maxHeight: MAX_HEIGHT_PERCENTAGE * modalHeight,
92
- initialHeight: INITIAL_HEIGHT_PERCENTAGE * modalHeight,
93
- });
75
+ const [activeLetter, setActiveLetter] = useState<string | null>(null);
94
76
 
95
- const sheetHeight = useRef(
96
- new Animated.Value(bottomSheetSize.initialHeight),
97
- ).current;
98
- const lastHeight = useRef(bottomSheetSize.initialHeight);
99
- const dragStartY = useRef(0);
100
-
101
- useEffect(() => {
102
- if (modalType === 'popup') {
103
- return;
104
- }
77
+ const flatListRef = useRef<FlatList<IListItem>>(null);
78
+ const isProgrammaticScroll = useRef(false);
105
79
 
106
- const DRAG_HANDLE_HEIGHT = 20;
107
- const availableHeight = modalHeight - DRAG_HANDLE_HEIGHT;
108
-
109
- const parsedMinHeight = parseHeight(minBottomsheetHeight, availableHeight);
110
- const parsedMaxHeight = parseHeight(maxBottomsheetHeight, availableHeight);
111
- const parsedInitialHeight = parseHeight(
112
- initialBottomsheetHeight,
113
- availableHeight,
114
- );
115
-
116
- setBottomSheetSize({
117
- minHeight: parsedMinHeight || MIN_HEIGHT_PERCENTAGE * availableHeight,
118
- maxHeight: parsedMaxHeight || MAX_HEIGHT_PERCENTAGE * availableHeight,
119
- initialHeight:
120
- parsedInitialHeight || INITIAL_HEIGHT_PERCENTAGE * availableHeight,
80
+ const styles = createStyles(theme, modalType, isFullScreen);
81
+ const selectedCountries =
82
+ isMultiSelect && 'selectedCountries' in props
83
+ ? props.selectedCountries ?? []
84
+ : [];
85
+
86
+ const countriesList = useMemo(() => {
87
+ return getCountriesList({
88
+ searchQuery,
89
+ popularCountries,
90
+ language,
91
+ visibleCountries,
92
+ hiddenCountries,
121
93
  });
122
94
  }, [
123
- minBottomsheetHeight,
124
- maxBottomsheetHeight,
125
- initialBottomsheetHeight,
126
- modalHeight,
127
- modalType,
95
+ searchQuery,
96
+ popularCountries,
97
+ language,
98
+ visibleCountries,
99
+ hiddenCountries,
128
100
  ]);
129
101
 
130
- // Resets to initial height when the modal is opened
131
- useEffect(() => {
132
- if (modalType === 'popup') {
133
- return;
134
- }
135
-
136
- if (visible) {
137
- sheetHeight.setValue(bottomSheetSize.initialHeight);
138
- lastHeight.current = bottomSheetSize.initialHeight;
139
- }
140
- }, [visible, bottomSheetSize.initialHeight, modalType]);
141
-
142
- useEffect(() => {
143
- if (modalType === 'popup') {
144
- return;
145
- }
146
-
147
- if (isKeyboardVisible) {
148
- sheetHeight.setValue(parseHeight(bottomSheetSize.maxHeight, modalHeight));
149
- lastHeight.current = bottomSheetSize.maxHeight;
150
- } else {
151
- sheetHeight.setValue(parseHeight(lastHeight.current, modalHeight));
102
+ // Compute the index right after the "All Countries" section header (independent of localized/custom title)
103
+ const allCountriesStartIndex = useMemo(() => {
104
+ // Collect indices of section headers
105
+ const sectionIndices: number[] = [];
106
+ for (let i = 0; i < countriesList.length; i++) {
107
+ if ('isSection' in countriesList[i]) {
108
+ sectionIndices.push(i);
109
+ }
152
110
  }
153
- }, [isKeyboardVisible, modalType]);
154
-
155
- // Monitors keyboard events
156
- useEffect(() => {
157
- if (modalType === 'popup') {
158
- return;
111
+ // If there are at least two sections, the second one corresponds to "All Countries"
112
+ if (sectionIndices.length >= 2) {
113
+ return sectionIndices[1] + 1;
159
114
  }
115
+ // Otherwise, list has no popular section; start at top
116
+ return 0;
117
+ }, [countriesList]);
160
118
 
161
- const keyboardDidShowListener = Keyboard.addListener(
162
- 'keyboardDidShow',
163
- () => {
164
- setIsKeyboardVisible(true);
165
- },
166
- );
167
- const keyboardDidHideListener = Keyboard.addListener(
168
- 'keyboardDidHide',
169
- () => {
170
- setIsKeyboardVisible(false);
171
- },
172
- );
173
-
174
- return () => {
175
- keyboardDidShowListener?.remove();
176
- keyboardDidHideListener?.remove();
177
- };
178
- }, [modalType]);
179
-
180
- const handlePanResponder = useMemo(
181
- () =>
182
- PanResponder.create({
183
- onStartShouldSetPanResponder: () => {
184
- // Only respond to touches on the drag handle
185
- return true;
186
- },
187
- onMoveShouldSetPanResponder: (evt, gestureState) => {
188
- // Only respond to vertical movements with sufficient distance
189
- return Math.abs(gestureState.dy) > 5;
190
- },
191
- onPanResponderGrant: e => {
192
- dragStartY.current = e.nativeEvent.pageY;
193
- sheetHeight.stopAnimation();
194
- },
195
- onPanResponderMove: e => {
196
- const currentY = e.nativeEvent.pageY;
197
- const dy = currentY - dragStartY.current;
198
- const proposedHeight = lastHeight.current - dy;
199
-
200
- // Allows free dragging but with smooth limits
201
- sheetHeight.setValue(proposedHeight);
202
- },
203
- onPanResponderRelease: e => {
204
- const currentY = e.nativeEvent.pageY;
205
- const dy = currentY - dragStartY.current;
206
- const currentHeight = lastHeight.current - dy;
119
+ const keyExtractor = useCallback(
120
+ (item: IListItem) => ('isSection' in item ? item.title : item.cca2),
121
+ [],
122
+ );
207
123
 
208
- // Close modal if the final height is less than minHeight
209
- if (currentHeight < bottomSheetSize.minHeight) {
210
- Animated.timing(sheetHeight, {
211
- toValue: 0,
212
- duration: 200,
213
- useNativeDriver: false,
214
- }).start(onClose);
215
- return;
124
+ const handlePressLetter = useCallback(
125
+ (index: number) => {
126
+ // Mark programmatic scroll to avoid onViewableItemsChanged flicker
127
+ isProgrammaticScroll.current = true;
128
+
129
+ // Pre-set active letter immediately based on the first non-section item at or after index
130
+ let computedLetter: string | null = null;
131
+ for (let i = index; i < countriesList.length; i++) {
132
+ const item = countriesList[i];
133
+ if (!('isSection' in item)) {
134
+ const name = (item as ICountry)?.translations[language]?.common || '';
135
+ if (name) {
136
+ computedLetter = name[0].toUpperCase();
216
137
  }
138
+ break;
139
+ }
140
+ }
141
+ if (computedLetter) {
142
+ setActiveLetter(computedLetter);
143
+ }
217
144
 
218
- // Snap to nearest limit
219
- const finalHeight = Math.min(
220
- Math.max(currentHeight, bottomSheetSize.minHeight),
221
- bottomSheetSize.maxHeight,
222
- );
223
-
224
- Animated.spring(sheetHeight, {
225
- toValue: finalHeight,
226
- useNativeDriver: false,
227
- tension: 50,
228
- friction: 12,
229
- }).start(() => {
230
- lastHeight.current = finalHeight;
231
- });
232
- },
233
- onPanResponderTerminate: () => {
234
- // Reset to last stable height if gesture is terminated
235
- Animated.spring(sheetHeight, {
236
- toValue: lastHeight.current,
237
- useNativeDriver: false,
238
- tension: 50,
239
- friction: 12,
240
- }).start();
241
- },
242
- }),
243
- [bottomSheetSize, sheetHeight, onClose],
145
+ flatListRef.current?.scrollToIndex({
146
+ index,
147
+ animated: true,
148
+ viewPosition: 0,
149
+ });
150
+ },
151
+ [countriesList, language],
244
152
  );
245
153
 
246
- // Obtain the country name in the selected language
247
- const getCountryNameInLanguage = (country: ICountry): string => {
248
- return (
249
- country.translations[language]?.common ||
250
- country.translations[DEFAULT_LANGUAGE]?.common ||
251
- country.name.common ||
252
- ''
253
- );
254
- };
255
-
256
- // Normalize country name and remove accents
257
- const normalizeCountryName = (str: string) =>
258
- str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
259
-
260
- const sortCountriesAlphabetically = (
261
- countriesList: ICountry[],
262
- ): ICountry[] => {
263
- return [...countriesList].sort((a, b) => {
264
- const nameA = normalizeCountryName(
265
- getCountryNameInLanguage(a).toLowerCase(),
266
- );
267
- const nameB = normalizeCountryName(
268
- getCountryNameInLanguage(b).toLowerCase(),
269
- );
270
- return nameA.localeCompare(nameB);
271
- });
154
+ const handleCloseModal = () => {
155
+ setSearchQuery('');
156
+ setActiveLetter(null);
157
+ onClose();
272
158
  };
273
159
 
274
- const getCountries = useMemo(() => {
275
- const query = searchQuery.toLowerCase();
276
-
277
- let countriesData = countries as unknown as ICountry[];
278
-
279
- if (visibleCountries.length > 0 && hiddenCountries.length > 0) {
280
- countriesData = (countries as unknown as ICountry[]).filter(
281
- country =>
282
- visibleCountries.includes(country.cca2) &&
283
- !hiddenCountries.includes(country.cca2),
284
- );
285
- }
286
-
287
- if (visibleCountries.length > 0 && hiddenCountries.length === 0) {
288
- countriesData = (countries as unknown as ICountry[]).filter(country =>
289
- visibleCountries.includes(country.cca2),
290
- );
291
- }
292
-
293
- if (hiddenCountries.length > 0 && visibleCountries.length === 0) {
294
- countriesData = (countries as unknown as ICountry[]).filter(
295
- country => !hiddenCountries.includes(country.cca2),
296
- );
297
- }
298
-
299
- if (query.length > 0) {
300
- const filteredCountries = countriesData.filter(country => {
301
- const countryName = getCountryNameInLanguage(country);
302
- const normalizedCountryName = normalizeCountryName(
303
- countryName.toLowerCase(),
304
- );
305
- const normalizedQuery = normalizeCountryName(query);
306
- const callingCode = country.idd.root.toLowerCase();
307
- const flag = country.flag.toLowerCase();
308
- const countryCode = country.cca2.toLowerCase();
309
-
310
- return (
311
- normalizedCountryName.includes(normalizedQuery) ||
312
- countryName.toLowerCase().includes(query) ||
313
- callingCode.includes(query) ||
314
- flag.includes(query) ||
315
- countryCode.includes(query)
316
- );
317
- });
318
-
319
- return sortCountriesAlphabetically(filteredCountries);
160
+ // Memoized set of selected country codes for fast lookups
161
+ const selectedCountryCodes = useMemo(() => {
162
+ if (selectedCountries.length === 0) {
163
+ return new Set<string>();
320
164
  }
321
-
322
- const popularCountriesData = sortCountriesAlphabetically(
323
- countriesData.filter(country => popularCountries.includes(country.cca2)),
324
- );
325
-
326
- const otherCountriesData = sortCountriesAlphabetically(
327
- countriesData.filter(country => !popularCountries.includes(country.cca2)),
328
- );
329
-
330
- const result: IListItem[] = [];
331
-
332
- if (popularCountriesData.length > 0) {
333
- result.push({
334
- isSection: true as const,
335
- title:
336
- translations.popularCountriesTitle[
337
- language as ICountrySelectLanguages
338
- ],
339
- });
340
- result.push(...popularCountriesData);
341
- result.push({
342
- isSection: true as const,
343
- title:
344
- translations.allCountriesTitle[language as ICountrySelectLanguages],
345
- });
165
+ const set = new Set<string>();
166
+ for (const c of selectedCountries) {
167
+ set.add(c.cca2);
346
168
  }
169
+ return set;
170
+ }, [selectedCountries]);
347
171
 
348
- result.push(...otherCountriesData);
349
-
350
- return result;
351
- }, [
352
- searchQuery,
353
- popularCountries,
354
- language,
355
- visibleCountries,
356
- hiddenCountries,
357
- ]);
358
-
359
- const keyExtractor = useCallback(
360
- (item: IListItem) => ('isSection' in item ? item.title : item.cca2),
361
- [],
172
+ const isCountrySelected = useCallback(
173
+ (cca2: string) => selectedCountryCodes.has(cca2),
174
+ [selectedCountryCodes],
362
175
  );
363
176
 
364
- const getItemLayout = useCallback(
365
- (data: IListItem[] | null | undefined, index: number) => {
366
- let offset = 0;
367
- let length = ITEM_HEIGHT;
368
-
369
- if (data) {
370
- const item = data[index];
371
- if ('isSection' in item) {
372
- length = SECTION_HEADER_HEIGHT;
177
+ const handleSelectCountry = useCallback(
178
+ (country: ICountry) => {
179
+ if (isMultiSelect) {
180
+ if (isCountrySelected(country.cca2)) {
181
+ (onSelect as (countries: ICountry[]) => void)(
182
+ selectedCountries.filter(c => c.cca2 !== country.cca2),
183
+ );
184
+ return;
373
185
  }
186
+ (onSelect as (countries: ICountry[]) => void)([
187
+ ...selectedCountries,
188
+ country,
189
+ ]);
190
+ return;
374
191
  }
375
192
 
376
- return {
377
- length,
378
- offset: offset + index * ITEM_HEIGHT,
379
- index,
380
- };
193
+ (onSelect as (country: ICountry) => void)(country);
194
+ onClose();
381
195
  },
382
- [],
196
+ [isMultiSelect, isCountrySelected, selectedCountries],
383
197
  );
384
198
 
385
199
  const renderCloseButton = () => {
386
200
  if (closeButtonComponent) {
387
201
  return closeButtonComponent();
388
202
  }
389
-
390
203
  return (
391
- <TouchableOpacity
392
- testID="countrySelectCloseButton"
393
- accessibilityRole="button"
394
- accessibilityLabel={
395
- accessibilityLabelCloseButton ||
396
- translations.accessibilityLabelCloseButton[language]
397
- }
398
- accessibilityHint={
399
- accessibilityHintCloseButton ||
400
- translations.accessibilityHintCloseButton[language]
401
- }
402
- style={[styles.closeButton, countrySelectStyle?.closeButton]}
403
- activeOpacity={0.6}
404
- onPress={onClose}>
405
- <Text
406
- style={[styles.closeButtonText, countrySelectStyle?.closeButtonText]}>
407
- {'\u00D7'}
408
- </Text>
409
- </TouchableOpacity>
204
+ <CloseButton
205
+ theme={theme as IThemeProps}
206
+ language={language}
207
+ onClose={handleCloseModal}
208
+ countrySelectStyle={countrySelectStyle}
209
+ accessibilityLabelCloseButton={accessibilityLabelCloseButton}
210
+ accessibilityHintCloseButton={accessibilityHintCloseButton}
211
+ />
410
212
  );
411
213
  };
412
214
 
413
- const renderSearchInput = () => {
414
- return (
415
- <TextInput
416
- testID="countrySelectSearchInput"
417
- accessibilityRole="text"
418
- accessibilityLabel={
419
- accessibilityLabelSearchInput ||
420
- translations.accessibilityLabelSearchInput[language]
421
- }
422
- accessibilityHint={
423
- accessibilityHintSearchInput ||
424
- translations.accessibilityHintSearchInput[language]
425
- }
426
- style={[styles.searchInput, countrySelectStyle?.searchInput]}
427
- placeholder={
428
- searchPlaceholder ||
429
- translations.searchPlaceholder[language as ICountrySelectLanguages]
430
- }
431
- placeholderTextColor={
432
- searchPlaceholderTextColor ||
433
- (theme === 'dark' ? '#FFFFFF80' : '#00000080')
215
+ const renderSearchInput = () => (
216
+ <SearchInput
217
+ theme={theme as IThemeProps}
218
+ language={language}
219
+ value={searchQuery}
220
+ onChangeText={setSearchQuery}
221
+ countrySelectStyle={countrySelectStyle}
222
+ searchPlaceholder={searchPlaceholder}
223
+ searchPlaceholderTextColor={searchPlaceholderTextColor}
224
+ searchSelectionColor={searchSelectionColor}
225
+ accessibilityLabelSearchInput={accessibilityLabelSearchInput}
226
+ accessibilityHintSearchInput={accessibilityHintSearchInput}
227
+ />
228
+ );
229
+
230
+ const onViewableItemsChanged = useRef(
231
+ ({
232
+ viewableItems,
233
+ }: {
234
+ viewableItems: Array<{item: IListItem; index: number | null}>;
235
+ }) => {
236
+ if (isProgrammaticScroll.current) {
237
+ // Ignore transient updates while we are animating to a specific index
238
+ return;
239
+ }
240
+ let updated: string | null = null;
241
+ for (const v of viewableItems) {
242
+ const it = v.item;
243
+ const idx = v.index ?? -1;
244
+ if (!('isSection' in it) && idx >= allCountriesStartIndex) {
245
+ const name = (it as ICountry)?.translations[language]?.common || '';
246
+ if (name) {
247
+ updated = name[0].toUpperCase();
248
+ }
249
+ break;
434
250
  }
435
- selectionColor={searchSelectionColor}
436
- value={searchQuery}
437
- onChangeText={setSearchQuery}
438
- />
439
- );
440
- };
251
+ }
252
+ setActiveLetter(updated);
253
+ },
254
+ ).current;
441
255
 
442
256
  const renderFlatList = () => {
443
- if (getCountries.length === 0) {
257
+ if (countriesList.length === 0) {
444
258
  return (
445
259
  <View
446
260
  style={[
@@ -462,6 +276,7 @@ export const CountrySelect: React.FC<ICountrySelectProps> = ({
462
276
  }
463
277
  return (
464
278
  <FlatList
279
+ ref={flatListRef}
465
280
  testID="countrySelectList"
466
281
  accessibilityRole="list"
467
282
  accessibilityLabel={
@@ -472,13 +287,35 @@ export const CountrySelect: React.FC<ICountrySelectProps> = ({
472
287
  accessibilityHintCountriesList ||
473
288
  translations.accessibilityHintCountriesList[language]
474
289
  }
475
- data={getCountries}
290
+ data={countriesList}
476
291
  keyExtractor={keyExtractor}
477
292
  renderItem={renderItem}
478
- getItemLayout={getItemLayout}
479
293
  keyboardShouldPersistTaps="handled"
480
294
  showsVerticalScrollIndicator={showsVerticalScrollIndicator || false}
481
295
  style={[styles.list, countrySelectStyle?.list]}
296
+ onViewableItemsChanged={onViewableItemsChanged}
297
+ onMomentumScrollEnd={() => {
298
+ isProgrammaticScroll.current = false;
299
+ }}
300
+ onScrollEndDrag={() => {
301
+ // Fallback if momentum does not trigger
302
+ isProgrammaticScroll.current = false;
303
+ }}
304
+ onScrollToIndexFailed={({index, averageItemLength}) => {
305
+ // Simple recovery: estimate offset, then retry scrollToIndex after measurement
306
+ const estimatedOffset = Math.max(0, (averageItemLength || 0) * index);
307
+ flatListRef.current?.scrollToOffset({
308
+ offset: estimatedOffset,
309
+ animated: false,
310
+ });
311
+ setTimeout(() => {
312
+ flatListRef.current?.scrollToIndex({
313
+ index,
314
+ animated: true,
315
+ viewPosition: 0,
316
+ });
317
+ }, 100);
318
+ }}
482
319
  />
483
320
  );
484
321
  };
@@ -507,180 +344,142 @@ export const CountrySelect: React.FC<ICountrySelectProps> = ({
507
344
  return countryItemComponent(item as ICountry);
508
345
  }
509
346
 
347
+ const countryItem = item as ICountry;
348
+ const selected = isMultiSelect && isCountrySelected(countryItem.cca2);
510
349
  return (
511
350
  <CountryItem
512
- item={item as ICountry}
513
- onSelect={onSelect}
514
- onClose={onClose}
515
- theme={theme}
351
+ country={countryItem}
352
+ isSelected={selected}
353
+ onSelect={handleSelectCountry}
354
+ theme={theme as IThemeProps}
516
355
  language={language}
517
356
  countrySelectStyle={countrySelectStyle}
518
- accessibilityLabel={
519
- accessibilityLabelCountryItem ||
520
- translations.accessibilityLabelCountryItem[language]
521
- }
522
- accessibilityHint={
523
- accessibilityHintCountryItem ||
524
- translations.accessibilityHintCountryItem[language]
525
- }
357
+ accessibilityLabel={accessibilityLabelCountryItem}
358
+ accessibilityHint={accessibilityHintCountryItem}
526
359
  />
527
360
  );
528
361
  },
529
362
  [
530
- onSelect,
531
- onClose,
532
363
  styles,
533
364
  language,
534
365
  countryItemComponent,
535
366
  sectionTitleComponent,
367
+ isMultiSelect,
368
+ isCountrySelected,
536
369
  ],
537
370
  );
538
371
 
372
+ const renderAlphabetFilter = () => {
373
+ return (
374
+ <AlphabeticFilter
375
+ activeLetter={activeLetter}
376
+ onPressLetter={handlePressLetter}
377
+ theme={theme as IThemeProps}
378
+ language={language}
379
+ countries={countriesList}
380
+ allCountriesStartIndex={allCountriesStartIndex}
381
+ countrySelectStyle={countrySelectStyle}
382
+ accessibilityLabelAlphabetFilter={accessibilityLabelAlphabetFilter}
383
+ accessibilityHintAlphabetFilter={accessibilityHintAlphabetFilter}
384
+ accessibilityLabelAlphabetLetter={accessibilityLabelAlphabetLetter}
385
+ accessibilityHintAlphabetLetter={accessibilityHintAlphabetLetter}
386
+ />
387
+ );
388
+ };
389
+
390
+ const HeaderModal =
391
+ showSearchInput || showCloseButton ? (
392
+ <View
393
+ style={[styles.searchContainer, countrySelectStyle?.searchContainer]}>
394
+ {(showCloseButton || isFullScreen) && renderCloseButton()}
395
+ {showSearchInput && renderSearchInput()}
396
+ </View>
397
+ ) : null;
398
+
399
+ const ContentModal = (
400
+ <>
401
+ <View style={{flex: 1}}>{renderFlatList()}</View>
402
+ {showAlphabetFilter && <View>{renderAlphabetFilter()}</View>}
403
+ </>
404
+ );
405
+
539
406
  if (modalType === 'popup' || isFullScreen) {
407
+ if (isFullScreen) {
408
+ return (
409
+ <FullscreenModal
410
+ visible={visible}
411
+ onRequestClose={handleCloseModal}
412
+ statusBarTranslucent
413
+ removedBackdrop={removedBackdrop}
414
+ disabledBackdropPress={disabledBackdropPress}
415
+ onBackdropPress={onBackdropPress}
416
+ accessibilityLabelBackdrop={
417
+ accessibilityLabelBackdrop ||
418
+ translations.accessibilityLabelBackdrop[language]
419
+ }
420
+ accessibilityHintBackdrop={
421
+ accessibilityHintBackdrop ||
422
+ translations.accessibilityHintBackdrop[language]
423
+ }
424
+ styles={styles}
425
+ countrySelectStyle={countrySelectStyle}
426
+ header={HeaderModal}
427
+ {...props}>
428
+ {ContentModal}
429
+ </FullscreenModal>
430
+ );
431
+ }
432
+
540
433
  return (
541
- <Modal
434
+ <PopupModal
542
435
  visible={visible}
543
- transparent
544
- animationType="fade"
545
- onRequestClose={onClose}
436
+ onRequestClose={handleCloseModal}
546
437
  statusBarTranslucent
438
+ removedBackdrop={removedBackdrop}
439
+ disabledBackdropPress={disabledBackdropPress}
440
+ onBackdropPress={onBackdropPress}
441
+ accessibilityLabelBackdrop={
442
+ accessibilityLabelBackdrop ||
443
+ translations.accessibilityLabelBackdrop[language]
444
+ }
445
+ accessibilityHintBackdrop={
446
+ accessibilityHintBackdrop ||
447
+ translations.accessibilityHintBackdrop[language]
448
+ }
449
+ styles={styles}
450
+ countrySelectStyle={countrySelectStyle}
451
+ header={HeaderModal}
547
452
  {...props}>
548
- <View
549
- testID="countrySelectContainer"
550
- style={[
551
- styles.container,
552
- countrySelectStyle?.container,
553
- isFullScreen && {
554
- flex: 1,
555
- width: '100%',
556
- height: '100%',
557
- },
558
- ]}>
559
- <Pressable
560
- testID="countrySelectBackdrop"
561
- accessibilityRole="button"
562
- accessibilityLabel={
563
- accessibilityLabelBackdrop ||
564
- translations.accessibilityLabelBackdrop[language]
565
- }
566
- accessibilityHint={
567
- accessibilityHintBackdrop ||
568
- translations.accessibilityHintBackdrop[language]
569
- }
570
- disabled={disabledBackdropPress || removedBackdrop}
571
- style={[
572
- styles.backdrop,
573
- {alignItems: 'center', justifyContent: 'center'},
574
- countrySelectStyle?.backdrop,
575
- removedBackdrop && {backgroundColor: 'transparent'},
576
- ]}
577
- onPress={onBackdropPress || onClose}
578
- />
579
- <View
580
- testID="countrySelectContent"
581
- style={[
582
- styles.content,
583
- countrySelectStyle?.content,
584
- isFullScreen && {
585
- borderRadius: 0,
586
- width: '100%',
587
- height: '100%',
588
- },
589
- ]}>
590
- {(isFullScreen || showSearchInput || showCloseButton) && (
591
- <View
592
- style={[
593
- styles.searchContainer,
594
- countrySelectStyle?.searchContainer,
595
- ]}>
596
- {(isFullScreen || showCloseButton) && renderCloseButton()}
597
- {showSearchInput && renderSearchInput()}
598
- </View>
599
- )}
600
-
601
- {renderFlatList()}
602
- </View>
603
- </View>
604
- </Modal>
453
+ {ContentModal}
454
+ </PopupModal>
605
455
  );
606
456
  }
607
457
 
608
458
  return (
609
- <Modal
459
+ <BottomSheetModal
610
460
  visible={visible}
611
- transparent
612
- animationType="slide"
613
- onRequestClose={onClose}
461
+ onRequestClose={handleCloseModal}
614
462
  statusBarTranslucent
463
+ removedBackdrop={removedBackdrop}
464
+ disabledBackdropPress={disabledBackdropPress}
465
+ onBackdropPress={onBackdropPress}
466
+ accessibilityLabelBackdrop={
467
+ accessibilityLabelBackdrop ||
468
+ translations.accessibilityLabelBackdrop[language]
469
+ }
470
+ accessibilityHintBackdrop={
471
+ accessibilityHintBackdrop ||
472
+ translations.accessibilityHintBackdrop[language]
473
+ }
474
+ styles={styles}
475
+ countrySelectStyle={countrySelectStyle}
476
+ minBottomsheetHeight={minBottomsheetHeight}
477
+ maxBottomsheetHeight={maxBottomsheetHeight}
478
+ initialBottomsheetHeight={initialBottomsheetHeight}
479
+ dragHandleIndicatorComponent={dragHandleIndicatorComponent}
480
+ header={HeaderModal}
615
481
  {...props}>
616
- <View
617
- testID="countrySelectContainer"
618
- style={[styles.container, countrySelectStyle?.container]}
619
- onLayout={event => {
620
- const {height} = event.nativeEvent.layout;
621
- setModalHeight(height);
622
- }}>
623
- <Pressable
624
- testID="countrySelectBackdrop"
625
- accessibilityRole="button"
626
- accessibilityLabel={
627
- accessibilityLabelBackdrop ||
628
- translations.accessibilityLabelBackdrop[language]
629
- }
630
- accessibilityHint={
631
- accessibilityHintBackdrop ||
632
- translations.accessibilityHintBackdrop[language]
633
- }
634
- disabled={disabledBackdropPress || removedBackdrop}
635
- style={[
636
- styles.backdrop,
637
- countrySelectStyle?.backdrop,
638
- removedBackdrop && {backgroundColor: 'transparent'},
639
- ]}
640
- onPress={onBackdropPress || onClose}
641
- />
642
- <Animated.View
643
- testID="countrySelectContent"
644
- style={[
645
- styles.content,
646
- countrySelectStyle?.content,
647
- {
648
- height: sheetHeight,
649
- minHeight: bottomSheetSize.minHeight,
650
- maxHeight: bottomSheetSize.maxHeight,
651
- },
652
- ]}>
653
- <View
654
- {...handlePanResponder.panHandlers}
655
- style={[
656
- styles.dragHandleContainer,
657
- countrySelectStyle?.dragHandleContainer,
658
- ]}>
659
- {dragHandleIndicatorComponent ? (
660
- dragHandleIndicatorComponent()
661
- ) : (
662
- <View
663
- style={[
664
- styles.dragHandleIndicator,
665
- countrySelectStyle?.dragHandleIndicator,
666
- ]}
667
- />
668
- )}
669
- </View>
670
- {(showSearchInput || showCloseButton) && (
671
- <View
672
- style={[
673
- styles.searchContainer,
674
- countrySelectStyle?.searchContainer,
675
- ]}>
676
- {showCloseButton && renderCloseButton()}
677
- {showSearchInput && renderSearchInput()}
678
- </View>
679
- )}
680
-
681
- <Animated.View style={{flex: 1}}>{renderFlatList()}</Animated.View>
682
- </Animated.View>
683
- </View>
684
- </Modal>
482
+ {ContentModal}
483
+ </BottomSheetModal>
685
484
  );
686
485
  };