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.
- package/README.md +77 -23
- package/lib/assets/images/preview.png +0 -0
- package/lib/components/AlphabeticFilter/index.tsx +180 -0
- package/lib/components/BottomSheetModal/index.tsx +223 -0
- package/lib/components/CloseButton/index.tsx +38 -0
- package/lib/components/CountryItem/index.tsx +27 -19
- package/lib/components/CountrySelect/index.tsx +297 -498
- package/lib/components/FullscreenModal/index.tsx +85 -0
- package/lib/components/PopupModal/index.tsx +77 -0
- package/lib/components/SearchInput/index.tsx +47 -0
- package/lib/components/index.ts +0 -1
- package/lib/components/styles.js +41 -1
- package/lib/interface/alfabeticFilterProps.ts +18 -0
- package/lib/interface/closeButtonProps.ts +12 -0
- package/lib/interface/countryItemProps.ts +4 -3
- package/lib/interface/countrySelectProps.ts +25 -5
- package/lib/interface/countrySelectStyles.ts +7 -0
- package/lib/interface/index.ts +3 -0
- package/lib/interface/searchInputProps.ts +16 -0
- package/lib/interface/theme.ts +3 -0
- package/lib/utils/createAlphabet.ts +3 -0
- package/lib/utils/getCountriesList.ts +100 -0
- package/lib/utils/getCountryNameInLanguage.ts +8 -0
- package/lib/utils/getTranslation.ts +145 -1
- package/lib/utils/normalizeCountryName.ts +3 -0
- package/lib/utils/sortCountriesAlphabetically.ts +17 -0
- package/package.json +18 -12
|
@@ -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
|
|
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 =
|
|
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 [
|
|
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
|
|
96
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
95
|
+
searchQuery,
|
|
96
|
+
popularCountries,
|
|
97
|
+
language,
|
|
98
|
+
visibleCountries,
|
|
99
|
+
hiddenCountries,
|
|
128
100
|
]);
|
|
129
101
|
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
323
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
365
|
-
(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
377
|
-
|
|
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
|
-
<
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
);
|
|
440
|
-
};
|
|
251
|
+
}
|
|
252
|
+
setActiveLetter(updated);
|
|
253
|
+
},
|
|
254
|
+
).current;
|
|
441
255
|
|
|
442
256
|
const renderFlatList = () => {
|
|
443
|
-
if (
|
|
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={
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
434
|
+
<PopupModal
|
|
542
435
|
visible={visible}
|
|
543
|
-
|
|
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
|
-
|
|
549
|
-
|
|
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
|
-
<
|
|
459
|
+
<BottomSheetModal
|
|
610
460
|
visible={visible}
|
|
611
|
-
|
|
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
|
-
|
|
617
|
-
|
|
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
|
};
|