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.
- package/README.md +50 -3
- package/lib/assets/images/preview.png +0 -0
- package/lib/components/AlphabeticFilter/index.tsx +180 -0
- package/lib/components/BottomSheetModal/index.tsx +225 -0
- package/lib/components/CloseButton/index.tsx +38 -0
- package/lib/components/CountryItem/index.tsx +27 -19
- package/lib/components/CountrySelect/index.tsx +298 -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 +46 -7
- 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,12 +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}
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
434
|
+
<PopupModal
|
|
541
435
|
visible={visible}
|
|
542
|
-
|
|
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
|
-
|
|
548
|
-
|
|
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
|
-
<
|
|
459
|
+
<BottomSheetModal
|
|
609
460
|
visible={visible}
|
|
610
|
-
|
|
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
|
-
|
|
616
|
-
|
|
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
|
};
|