react-native-country-select 0.2.5 → 0.3.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.
@@ -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,12 +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}
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
+ }}
481
319
  />
482
320
  );
483
321
  };
@@ -506,180 +344,142 @@ export const CountrySelect: React.FC<ICountrySelectProps> = ({
506
344
  return countryItemComponent(item as ICountry);
507
345
  }
508
346
 
347
+ const countryItem = item as ICountry;
348
+ const selected = isMultiSelect && isCountrySelected(countryItem.cca2);
509
349
  return (
510
350
  <CountryItem
511
- item={item as ICountry}
512
- onSelect={onSelect}
513
- onClose={onClose}
514
- theme={theme}
351
+ country={countryItem}
352
+ isSelected={selected}
353
+ onSelect={handleSelectCountry}
354
+ theme={theme as IThemeProps}
515
355
  language={language}
516
356
  countrySelectStyle={countrySelectStyle}
517
- accessibilityLabel={
518
- accessibilityLabelCountryItem ||
519
- translations.accessibilityLabelCountryItem[language]
520
- }
521
- accessibilityHint={
522
- accessibilityHintCountryItem ||
523
- translations.accessibilityHintCountryItem[language]
524
- }
357
+ accessibilityLabel={accessibilityLabelCountryItem}
358
+ accessibilityHint={accessibilityHintCountryItem}
525
359
  />
526
360
  );
527
361
  },
528
362
  [
529
- onSelect,
530
- onClose,
531
363
  styles,
532
364
  language,
533
365
  countryItemComponent,
534
366
  sectionTitleComponent,
367
+ isMultiSelect,
368
+ isCountrySelected,
535
369
  ],
536
370
  );
537
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
+
538
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
+
539
433
  return (
540
- <Modal
434
+ <PopupModal
541
435
  visible={visible}
542
- transparent
543
- animationType="fade"
544
- onRequestClose={onClose}
436
+ onRequestClose={handleCloseModal}
545
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}
546
452
  {...props}>
547
- <View
548
- testID="countrySelectContainer"
549
- style={[
550
- styles.container,
551
- countrySelectStyle?.container,
552
- isFullScreen && {
553
- flex: 1,
554
- width: '100%',
555
- height: '100%',
556
- },
557
- ]}>
558
- <Pressable
559
- testID="countrySelectBackdrop"
560
- accessibilityRole="button"
561
- accessibilityLabel={
562
- accessibilityLabelBackdrop ||
563
- translations.accessibilityLabelBackdrop[language]
564
- }
565
- accessibilityHint={
566
- accessibilityHintBackdrop ||
567
- translations.accessibilityHintBackdrop[language]
568
- }
569
- disabled={disabledBackdropPress || removedBackdrop}
570
- style={[
571
- styles.backdrop,
572
- {alignItems: 'center', justifyContent: 'center'},
573
- countrySelectStyle?.backdrop,
574
- removedBackdrop && {backgroundColor: 'transparent'},
575
- ]}
576
- onPress={onBackdropPress || onClose}
577
- />
578
- <View
579
- testID="countrySelectContent"
580
- style={[
581
- styles.content,
582
- countrySelectStyle?.content,
583
- isFullScreen && {
584
- borderRadius: 0,
585
- width: '100%',
586
- height: '100%',
587
- },
588
- ]}>
589
- {(isFullScreen || showSearchInput || showCloseButton) && (
590
- <View
591
- style={[
592
- styles.searchContainer,
593
- countrySelectStyle?.searchContainer,
594
- ]}>
595
- {(isFullScreen || showCloseButton) && renderCloseButton()}
596
- {showSearchInput && renderSearchInput()}
597
- </View>
598
- )}
599
-
600
- {renderFlatList()}
601
- </View>
602
- </View>
603
- </Modal>
453
+ {ContentModal}
454
+ </PopupModal>
604
455
  );
605
456
  }
606
457
 
607
458
  return (
608
- <Modal
459
+ <BottomSheetModal
609
460
  visible={visible}
610
- transparent
611
- animationType="slide"
612
- onRequestClose={onClose}
461
+ onRequestClose={handleCloseModal}
613
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}
614
481
  {...props}>
615
- <View
616
- testID="countrySelectContainer"
617
- style={[styles.container, countrySelectStyle?.container]}
618
- onLayout={event => {
619
- const {height} = event.nativeEvent.layout;
620
- setModalHeight(height);
621
- }}>
622
- <Pressable
623
- testID="countrySelectBackdrop"
624
- accessibilityRole="button"
625
- accessibilityLabel={
626
- accessibilityLabelBackdrop ||
627
- translations.accessibilityLabelBackdrop[language]
628
- }
629
- accessibilityHint={
630
- accessibilityHintBackdrop ||
631
- translations.accessibilityHintBackdrop[language]
632
- }
633
- disabled={disabledBackdropPress || removedBackdrop}
634
- style={[
635
- styles.backdrop,
636
- countrySelectStyle?.backdrop,
637
- removedBackdrop && {backgroundColor: 'transparent'},
638
- ]}
639
- onPress={onBackdropPress || onClose}
640
- />
641
- <Animated.View
642
- testID="countrySelectContent"
643
- style={[
644
- styles.content,
645
- countrySelectStyle?.content,
646
- {
647
- height: sheetHeight,
648
- minHeight: bottomSheetSize.minHeight,
649
- maxHeight: bottomSheetSize.maxHeight,
650
- },
651
- ]}>
652
- <View
653
- {...handlePanResponder.panHandlers}
654
- style={[
655
- styles.dragHandleContainer,
656
- countrySelectStyle?.dragHandleContainer,
657
- ]}>
658
- {dragHandleIndicatorComponent ? (
659
- dragHandleIndicatorComponent()
660
- ) : (
661
- <View
662
- style={[
663
- styles.dragHandleIndicator,
664
- countrySelectStyle?.dragHandleIndicator,
665
- ]}
666
- />
667
- )}
668
- </View>
669
- {(showSearchInput || showCloseButton) && (
670
- <View
671
- style={[
672
- styles.searchContainer,
673
- countrySelectStyle?.searchContainer,
674
- ]}>
675
- {showCloseButton && renderCloseButton()}
676
- {showSearchInput && renderSearchInput()}
677
- </View>
678
- )}
679
-
680
- <Animated.View style={{flex: 1}}>{renderFlatList()}</Animated.View>
681
- </Animated.View>
682
- </View>
683
- </Modal>
482
+ {ContentModal}
483
+ </BottomSheetModal>
684
484
  );
685
485
  };