react-native-country-select 0.2.6 → 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 +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
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<br>
|
|
2
2
|
|
|
3
3
|
<div align = "center">
|
|
4
|
-
<img src="lib/assets/images/preview.png" alt="React Native
|
|
4
|
+
<img src="lib/assets/images/preview.png" alt="React Native Country Picker and Select Lib preview">
|
|
5
5
|
</div>
|
|
6
6
|
|
|
7
7
|
<br>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<h1 align="center">React Native Country Select</h1>
|
|
10
10
|
|
|
11
11
|
<p>
|
|
12
|
-
🌍
|
|
12
|
+
🌍 React Native country picker with flags, search, TypeScript, i18n, and offline support. Lightweight, customizable, and designed with a modern UI.
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
15
|
<br>
|
|
@@ -246,6 +246,46 @@ export default function App() {
|
|
|
246
246
|
|
|
247
247
|
<br>
|
|
248
248
|
|
|
249
|
+
- Multi Select Country
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
import React, {useState} from 'react';
|
|
253
|
+
import {Text, TouchableOpacity, View} from 'react-native';
|
|
254
|
+
|
|
255
|
+
import CountrySelect, {ICountry} from 'react-native-country-select';
|
|
256
|
+
|
|
257
|
+
export default function App() {
|
|
258
|
+
const [showPicker, setShowPicker] = useState<boolean>(false);
|
|
259
|
+
const [selectedCountries, setSelectedCountries] = useState<ICountry[]>([]);
|
|
260
|
+
|
|
261
|
+
const handleCountrySelect = (countries: ICountry[]) => {
|
|
262
|
+
setSelectedCountries(countries);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<View
|
|
267
|
+
style={{
|
|
268
|
+
flex: 1,
|
|
269
|
+
}}>
|
|
270
|
+
<TouchableOpacity onPress={() => setShowPicker(true)}>
|
|
271
|
+
<Text>Select Countries</Text>
|
|
272
|
+
</TouchableOpacity>
|
|
273
|
+
<Text>Countries: {selectedCountries.length}</Text>
|
|
274
|
+
|
|
275
|
+
<CountrySelect
|
|
276
|
+
visible={showPicker}
|
|
277
|
+
isMultiSelect
|
|
278
|
+
selectedCountries={selectedCountries}
|
|
279
|
+
onSelect={handleCountrySelect}
|
|
280
|
+
onClose={() => setShowPicker(false)}
|
|
281
|
+
/>
|
|
282
|
+
</View>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
<br>
|
|
288
|
+
|
|
249
289
|
### Modal Styles ([modalStyles](https://github.com/AstrOOnauta/react-native-country-select/blob/main/lib/interface/countrySelectStyles.ts))
|
|
250
290
|
|
|
251
291
|
| Property | Type | Description |
|
|
@@ -280,6 +320,8 @@ export default function App() {
|
|
|
280
320
|
| onSelect | (country: [ICountry](lib/interfaces/country.ts)) => void | Yes | - | Callback function called when a country is selected |
|
|
281
321
|
| modalType | 'bottomSheet' \| 'popup' | No | 'bottomSheet' | Type of modal to display |
|
|
282
322
|
| countrySelectStyle | [ICountrySelectStyle](lib/interfaces/countrySelectStyles.ts) | No | - | Custom styles for the country picker |
|
|
323
|
+
| isMultiSelect | boolean | No | false | Whether the user can select multiple options |
|
|
324
|
+
| selectedCountries | [ICountry[]](lib/interfaces/country.ts) | No | - | Array of countries to show in multi select mode |
|
|
283
325
|
| isFullScreen | boolean | No | false | Whether the modal should be full screen |
|
|
284
326
|
| popularCountries | string[] | No | [] | Array of country codes to show in popular section |
|
|
285
327
|
| visibleCountries | [ICountryCca2[]](lib/interfaces/countryCca2.ts) | No | [] | Array of country codes to show (whitelist) |
|
|
@@ -287,6 +329,7 @@ export default function App() {
|
|
|
287
329
|
| theme | 'light' \| 'dark' | No | 'light' | Theme for the country picker |
|
|
288
330
|
| language | [ICountrySelectLanguages](lib/interfaces/countrySelectLanguages.ts) | No | 'eng' | Language for country names (see supported languages below) |
|
|
289
331
|
| showSearchInput | boolean | No | true | Whether to show the search input field |
|
|
332
|
+
| showAlphabetFilter | boolean | No | false | Whether to show the alphabetic filter on modal |
|
|
290
333
|
| searchPlaceholder | string | No | 'Search country...' | Placeholder text for search input |
|
|
291
334
|
| searchPlaceholderTextColor | string | No | '#00000080' | Placeholder text color for search input |
|
|
292
335
|
| searchSelectionColor | string | No | default | Highlight, selection handle and cursor color of the search input |
|
|
@@ -381,7 +424,11 @@ Ensure your app is inclusive and usable by everyone by leveraging built-in React
|
|
|
381
424
|
- `accessibilityLabelCountriesList`: Accessibility label for the countries list;
|
|
382
425
|
- `accessibilityHintCountriesList`: Accessibility hint for the countries list;
|
|
383
426
|
- `accessibilityLabelCountryItem`: Accessibility label for individual country items;
|
|
384
|
-
- `accessibilityHintCountryItem`: Accessibility hint for individual country
|
|
427
|
+
- `accessibilityHintCountryItem`: Accessibility hint for individual country;
|
|
428
|
+
- `accessibilityLabelAlphabetFilter`: Accessibility label for alphabet filter list;
|
|
429
|
+
- `accessibilityHintAlphabetFilter`: Accessibility hint for alphabet filter list;
|
|
430
|
+
- `accessibilityLabelAlphabetLetter`: Accessibility label for individual alphabet filter letter;
|
|
431
|
+
- `accessibilityHintAlphabetLetter`: Accessibility hint for individual alphabet filter letter.
|
|
385
432
|
|
|
386
433
|
<br>
|
|
387
434
|
|
|
Binary file
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/* eslint-disable no-undef-init */
|
|
2
|
+
/* eslint-disable react-hooks/exhaustive-deps */
|
|
3
|
+
/* eslint-disable react-native/no-inline-styles */
|
|
4
|
+
import React, {useEffect, useMemo, useRef} from 'react';
|
|
5
|
+
import {ScrollView, Text, TouchableOpacity, View} from 'react-native';
|
|
6
|
+
|
|
7
|
+
import {createStyles} from '../styles';
|
|
8
|
+
import {translations} from '../../utils/getTranslation';
|
|
9
|
+
import {createAlphabet} from '../../utils/createAlphabet';
|
|
10
|
+
import {AlphabeticFilterProps} from '../../interface/alfabeticFilterProps';
|
|
11
|
+
import {normalizeCountryName} from '../../utils/normalizeCountryName';
|
|
12
|
+
|
|
13
|
+
const ALPHABET_VIEWPORT_HEIGHT = 0;
|
|
14
|
+
const ALPHABET_ITEM_SIZE = 28;
|
|
15
|
+
const ALPHABET_VERTICAL_PADDING = 12;
|
|
16
|
+
|
|
17
|
+
export const AlphabeticFilter: React.FC<AlphabeticFilterProps> = ({
|
|
18
|
+
activeLetter,
|
|
19
|
+
onPressLetter,
|
|
20
|
+
theme = 'light',
|
|
21
|
+
language,
|
|
22
|
+
countries,
|
|
23
|
+
allCountriesStartIndex,
|
|
24
|
+
countrySelectStyle,
|
|
25
|
+
accessibilityLabelAlphabetFilter,
|
|
26
|
+
accessibilityHintAlphabetFilter,
|
|
27
|
+
accessibilityLabelAlphabetLetter,
|
|
28
|
+
accessibilityHintAlphabetLetter,
|
|
29
|
+
}) => {
|
|
30
|
+
const styles = createStyles(theme);
|
|
31
|
+
const alphabetScrollRef = useRef<ScrollView>(null);
|
|
32
|
+
|
|
33
|
+
const letterIndexMap = useMemo(() => {
|
|
34
|
+
const map: Record<string, number> = {};
|
|
35
|
+
for (let i = allCountriesStartIndex; i < countries.length; i++) {
|
|
36
|
+
const item = countries[i];
|
|
37
|
+
if ('isSection' in item) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const country: any = item as any;
|
|
41
|
+
// Use English/common name to anchor alphabet jumps consistently
|
|
42
|
+
const name = country?.name?.common || '';
|
|
43
|
+
const first = (name?.[0] || '').toUpperCase();
|
|
44
|
+
if (first && map[first] === undefined) {
|
|
45
|
+
map[first] = i;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return map;
|
|
49
|
+
}, [countries, allCountriesStartIndex, language]);
|
|
50
|
+
|
|
51
|
+
const alphabet = createAlphabet();
|
|
52
|
+
|
|
53
|
+
const scrollAlphabetToLetter = (letter: string) => {
|
|
54
|
+
const letterIdx = alphabet.indexOf(letter);
|
|
55
|
+
if (letterIdx >= 0) {
|
|
56
|
+
const centerOffset = Math.max(
|
|
57
|
+
0,
|
|
58
|
+
ALPHABET_VIEWPORT_HEIGHT / 2 - ALPHABET_ITEM_SIZE / 2,
|
|
59
|
+
);
|
|
60
|
+
const y = Math.max(
|
|
61
|
+
0,
|
|
62
|
+
letterIdx * ALPHABET_ITEM_SIZE -
|
|
63
|
+
centerOffset +
|
|
64
|
+
ALPHABET_VERTICAL_PADDING,
|
|
65
|
+
);
|
|
66
|
+
alphabetScrollRef.current?.scrollTo({y, animated: true});
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!activeLetter) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
scrollAlphabetToLetter(activeLetter);
|
|
75
|
+
}, [activeLetter, alphabet]);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<ScrollView
|
|
79
|
+
testID="countrySelectAlphabetFilter"
|
|
80
|
+
accessibilityRole="list"
|
|
81
|
+
accessibilityLabel={
|
|
82
|
+
accessibilityLabelAlphabetFilter ||
|
|
83
|
+
translations.accessibilityLabelAlphabetFilter[language]
|
|
84
|
+
}
|
|
85
|
+
accessibilityHint={
|
|
86
|
+
accessibilityHintAlphabetFilter ||
|
|
87
|
+
translations.accessibilityHintAlphabetFilter[language]
|
|
88
|
+
}
|
|
89
|
+
ref={alphabetScrollRef}
|
|
90
|
+
style={[styles.alphabetContainer, countrySelectStyle?.alphabetContainer]}
|
|
91
|
+
contentContainerStyle={{alignItems: 'center', paddingVertical: 12}}
|
|
92
|
+
showsVerticalScrollIndicator={false}>
|
|
93
|
+
{alphabet.map(letter => {
|
|
94
|
+
const enabled = letterIndexMap[letter] !== undefined;
|
|
95
|
+
const isActive = activeLetter === letter;
|
|
96
|
+
if (enabled) {
|
|
97
|
+
return (
|
|
98
|
+
<TouchableOpacity
|
|
99
|
+
key={letter}
|
|
100
|
+
onPress={() => {
|
|
101
|
+
// Compute first index for this letter using normalized display name (same as sorting)
|
|
102
|
+
const lower = letter.toLowerCase();
|
|
103
|
+
let idxToGo: number | undefined = undefined;
|
|
104
|
+
for (
|
|
105
|
+
let i = allCountriesStartIndex;
|
|
106
|
+
i < countries.length;
|
|
107
|
+
i++
|
|
108
|
+
) {
|
|
109
|
+
const it = countries[i] as any;
|
|
110
|
+
if ('isSection' in it) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const displayName =
|
|
114
|
+
it?.translations?.[language]?.common ||
|
|
115
|
+
it?.name?.common ||
|
|
116
|
+
'';
|
|
117
|
+
const normalized = normalizeCountryName(
|
|
118
|
+
displayName.toLowerCase(),
|
|
119
|
+
);
|
|
120
|
+
if (normalized.startsWith(lower)) {
|
|
121
|
+
idxToGo = i;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (idxToGo !== undefined) {
|
|
126
|
+
onPressLetter(idxToGo);
|
|
127
|
+
}
|
|
128
|
+
scrollAlphabetToLetter(letter);
|
|
129
|
+
}}
|
|
130
|
+
style={[
|
|
131
|
+
styles.alphabetLetter,
|
|
132
|
+
isActive && styles.alphabetLetterActive,
|
|
133
|
+
countrySelectStyle?.alphabetLetter,
|
|
134
|
+
]}
|
|
135
|
+
accessibilityRole="button"
|
|
136
|
+
accessibilityHint={
|
|
137
|
+
accessibilityHintAlphabetLetter ||
|
|
138
|
+
translations.accessibilityHintAlphabetLetter[language] +
|
|
139
|
+
` ${letter}`
|
|
140
|
+
}
|
|
141
|
+
accessibilityLabel={
|
|
142
|
+
accessibilityLabelAlphabetLetter ||
|
|
143
|
+
translations.accessibilityLabelAlphabetLetter[language] +
|
|
144
|
+
` ${letter}`
|
|
145
|
+
}>
|
|
146
|
+
<Text
|
|
147
|
+
style={[
|
|
148
|
+
styles.alphabetLetterText,
|
|
149
|
+
isActive && styles.alphabetLetterTextActive,
|
|
150
|
+
countrySelectStyle?.alphabetLetterText,
|
|
151
|
+
]}>
|
|
152
|
+
{letter}
|
|
153
|
+
</Text>
|
|
154
|
+
</TouchableOpacity>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return (
|
|
158
|
+
<View
|
|
159
|
+
key={letter}
|
|
160
|
+
style={[
|
|
161
|
+
styles.alphabetLetter,
|
|
162
|
+
styles.alphabetLetterDisabled,
|
|
163
|
+
countrySelectStyle?.alphabetLetter,
|
|
164
|
+
]}>
|
|
165
|
+
<Text
|
|
166
|
+
style={[
|
|
167
|
+
styles.alphabetLetterText,
|
|
168
|
+
styles.alphabetLetterTextDisabled,
|
|
169
|
+
countrySelectStyle?.alphabetLetterText,
|
|
170
|
+
]}>
|
|
171
|
+
{letter}
|
|
172
|
+
</Text>
|
|
173
|
+
</View>
|
|
174
|
+
);
|
|
175
|
+
})}
|
|
176
|
+
</ScrollView>
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export default AlphabeticFilter;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/* eslint-disable react-native/no-inline-styles */
|
|
2
|
+
import React, {useEffect, useMemo, useRef, useState} from 'react';
|
|
3
|
+
import {
|
|
4
|
+
Animated,
|
|
5
|
+
Modal,
|
|
6
|
+
ModalProps,
|
|
7
|
+
Pressable,
|
|
8
|
+
View,
|
|
9
|
+
PanResponder,
|
|
10
|
+
Keyboard,
|
|
11
|
+
NativeSyntheticEvent,
|
|
12
|
+
} from 'react-native';
|
|
13
|
+
|
|
14
|
+
import parseHeight from '../../utils/parseHeight';
|
|
15
|
+
import {ICountrySelectStyle} from '../../interface';
|
|
16
|
+
|
|
17
|
+
interface BottomSheetModalProps extends ModalProps {
|
|
18
|
+
visible: boolean;
|
|
19
|
+
onRequestClose: (event: NativeSyntheticEvent<any>) => void;
|
|
20
|
+
statusBarTranslucent?: boolean;
|
|
21
|
+
removedBackdrop?: boolean;
|
|
22
|
+
disabledBackdropPress?: boolean;
|
|
23
|
+
onBackdropPress?: () => void;
|
|
24
|
+
accessibilityLabelBackdrop?: string;
|
|
25
|
+
accessibilityHintBackdrop?: string;
|
|
26
|
+
styles: ICountrySelectStyle;
|
|
27
|
+
countrySelectStyle?: ICountrySelectStyle;
|
|
28
|
+
minBottomsheetHeight?: number | string;
|
|
29
|
+
maxBottomsheetHeight?: number | string;
|
|
30
|
+
initialBottomsheetHeight?: number | string;
|
|
31
|
+
dragHandleIndicatorComponent?: () => React.ReactElement;
|
|
32
|
+
header?: React.ReactNode;
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const MIN_HEIGHT_PERCENTAGE = 0.3;
|
|
37
|
+
const MAX_HEIGHT_PERCENTAGE = 0.9;
|
|
38
|
+
const INITIAL_HEIGHT_PERCENTAGE = 0.5;
|
|
39
|
+
|
|
40
|
+
export const BottomSheetModal: React.FC<BottomSheetModalProps> = ({
|
|
41
|
+
visible,
|
|
42
|
+
onRequestClose,
|
|
43
|
+
statusBarTranslucent,
|
|
44
|
+
removedBackdrop,
|
|
45
|
+
disabledBackdropPress,
|
|
46
|
+
onBackdropPress,
|
|
47
|
+
accessibilityLabelBackdrop,
|
|
48
|
+
accessibilityHintBackdrop,
|
|
49
|
+
styles,
|
|
50
|
+
countrySelectStyle,
|
|
51
|
+
minBottomsheetHeight,
|
|
52
|
+
maxBottomsheetHeight,
|
|
53
|
+
initialBottomsheetHeight,
|
|
54
|
+
dragHandleIndicatorComponent,
|
|
55
|
+
header,
|
|
56
|
+
children,
|
|
57
|
+
...props
|
|
58
|
+
}) => {
|
|
59
|
+
const [modalHeight, setModalHeight] = useState(0);
|
|
60
|
+
const [bottomSheetSize, setBottomSheetSize] = useState({
|
|
61
|
+
minHeight: 0,
|
|
62
|
+
maxHeight: 0,
|
|
63
|
+
initialHeight: 0,
|
|
64
|
+
});
|
|
65
|
+
const sheetHeight = useRef(new Animated.Value(0)).current;
|
|
66
|
+
const lastHeightRef = useRef(0);
|
|
67
|
+
const dragStartYRef = useRef(0);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const DRAG_HANDLE_HEIGHT = 20;
|
|
71
|
+
const availableHeight = Math.max(0, modalHeight - DRAG_HANDLE_HEIGHT);
|
|
72
|
+
const parsedMinHeight = parseHeight(minBottomsheetHeight, availableHeight);
|
|
73
|
+
const parsedMaxHeight = parseHeight(maxBottomsheetHeight, availableHeight);
|
|
74
|
+
const parsedInitialHeight = parseHeight(
|
|
75
|
+
initialBottomsheetHeight,
|
|
76
|
+
availableHeight,
|
|
77
|
+
);
|
|
78
|
+
setBottomSheetSize({
|
|
79
|
+
minHeight: parsedMinHeight || MIN_HEIGHT_PERCENTAGE * availableHeight,
|
|
80
|
+
maxHeight: parsedMaxHeight || MAX_HEIGHT_PERCENTAGE * availableHeight,
|
|
81
|
+
initialHeight:
|
|
82
|
+
parsedInitialHeight || INITIAL_HEIGHT_PERCENTAGE * availableHeight,
|
|
83
|
+
});
|
|
84
|
+
}, [
|
|
85
|
+
modalHeight,
|
|
86
|
+
minBottomsheetHeight,
|
|
87
|
+
maxBottomsheetHeight,
|
|
88
|
+
initialBottomsheetHeight,
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (visible) {
|
|
93
|
+
sheetHeight.setValue(bottomSheetSize.initialHeight);
|
|
94
|
+
lastHeightRef.current = bottomSheetSize.initialHeight;
|
|
95
|
+
}
|
|
96
|
+
}, [visible, bottomSheetSize.initialHeight, sheetHeight]);
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const show = Keyboard.addListener('keyboardDidShow', () => {
|
|
100
|
+
sheetHeight.setValue(bottomSheetSize.maxHeight);
|
|
101
|
+
lastHeightRef.current = bottomSheetSize.maxHeight;
|
|
102
|
+
});
|
|
103
|
+
const hide = Keyboard.addListener('keyboardDidHide', () => {
|
|
104
|
+
sheetHeight.setValue(lastHeightRef.current);
|
|
105
|
+
});
|
|
106
|
+
return () => {
|
|
107
|
+
show?.remove();
|
|
108
|
+
hide?.remove();
|
|
109
|
+
};
|
|
110
|
+
}, [bottomSheetSize.maxHeight, sheetHeight]);
|
|
111
|
+
|
|
112
|
+
const panHandlers = useMemo(
|
|
113
|
+
() =>
|
|
114
|
+
PanResponder.create({
|
|
115
|
+
onStartShouldSetPanResponder: () => true,
|
|
116
|
+
onMoveShouldSetPanResponder: (_evt, gestureState) =>
|
|
117
|
+
Math.abs(gestureState.dy) > 5,
|
|
118
|
+
onPanResponderGrant: e => {
|
|
119
|
+
dragStartYRef.current = e.nativeEvent.pageY;
|
|
120
|
+
sheetHeight.stopAnimation();
|
|
121
|
+
},
|
|
122
|
+
onPanResponderMove: e => {
|
|
123
|
+
const currentY = e.nativeEvent.pageY;
|
|
124
|
+
const dy = currentY - dragStartYRef.current;
|
|
125
|
+
const proposedHeight = lastHeightRef.current - dy;
|
|
126
|
+
sheetHeight.setValue(proposedHeight);
|
|
127
|
+
},
|
|
128
|
+
onPanResponderRelease: e => {
|
|
129
|
+
const currentY = e.nativeEvent.pageY;
|
|
130
|
+
const dy = currentY - dragStartYRef.current;
|
|
131
|
+
const currentHeight = lastHeightRef.current - dy;
|
|
132
|
+
if (currentHeight < bottomSheetSize.minHeight) {
|
|
133
|
+
Animated.timing(sheetHeight, {
|
|
134
|
+
toValue: 0,
|
|
135
|
+
duration: 200,
|
|
136
|
+
useNativeDriver: false,
|
|
137
|
+
}).start();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const finalHeight = Math.min(
|
|
141
|
+
Math.max(currentHeight, bottomSheetSize.minHeight),
|
|
142
|
+
bottomSheetSize.maxHeight,
|
|
143
|
+
);
|
|
144
|
+
Animated.spring(sheetHeight, {
|
|
145
|
+
toValue: finalHeight,
|
|
146
|
+
useNativeDriver: false,
|
|
147
|
+
tension: 50,
|
|
148
|
+
friction: 12,
|
|
149
|
+
}).start(() => {
|
|
150
|
+
lastHeightRef.current = finalHeight;
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
onPanResponderTerminate: () => {
|
|
154
|
+
Animated.spring(sheetHeight, {
|
|
155
|
+
toValue: lastHeightRef.current,
|
|
156
|
+
useNativeDriver: false,
|
|
157
|
+
tension: 50,
|
|
158
|
+
friction: 12,
|
|
159
|
+
}).start();
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
[bottomSheetSize.minHeight, bottomSheetSize.maxHeight, sheetHeight],
|
|
163
|
+
);
|
|
164
|
+
return (
|
|
165
|
+
<Modal
|
|
166
|
+
visible={visible}
|
|
167
|
+
transparent
|
|
168
|
+
animationType="slide"
|
|
169
|
+
onRequestClose={onRequestClose}
|
|
170
|
+
statusBarTranslucent={statusBarTranslucent}
|
|
171
|
+
{...props}>
|
|
172
|
+
<View
|
|
173
|
+
testID="countrySelectContainer"
|
|
174
|
+
style={[styles.container, countrySelectStyle?.container]}
|
|
175
|
+
onLayout={e => setModalHeight(e.nativeEvent.layout.height)}>
|
|
176
|
+
<Pressable
|
|
177
|
+
testID="countrySelectBackdrop"
|
|
178
|
+
accessibilityRole="button"
|
|
179
|
+
accessibilityLabel={accessibilityLabelBackdrop}
|
|
180
|
+
accessibilityHint={accessibilityHintBackdrop}
|
|
181
|
+
disabled={disabledBackdropPress || removedBackdrop}
|
|
182
|
+
style={[
|
|
183
|
+
styles.backdrop,
|
|
184
|
+
countrySelectStyle?.backdrop,
|
|
185
|
+
removedBackdrop && {backgroundColor: 'transparent'},
|
|
186
|
+
]}
|
|
187
|
+
onPress={onBackdropPress || onRequestClose}
|
|
188
|
+
/>
|
|
189
|
+
<Animated.View
|
|
190
|
+
testID="countrySelectContent"
|
|
191
|
+
style={[
|
|
192
|
+
styles.content,
|
|
193
|
+
countrySelectStyle?.content,
|
|
194
|
+
{
|
|
195
|
+
height: sheetHeight,
|
|
196
|
+
minHeight: bottomSheetSize.minHeight,
|
|
197
|
+
maxHeight: bottomSheetSize.maxHeight,
|
|
198
|
+
},
|
|
199
|
+
]}>
|
|
200
|
+
<View
|
|
201
|
+
{...panHandlers.panHandlers}
|
|
202
|
+
style={[
|
|
203
|
+
styles.dragHandleContainer,
|
|
204
|
+
countrySelectStyle?.dragHandleContainer,
|
|
205
|
+
]}>
|
|
206
|
+
{dragHandleIndicatorComponent ? (
|
|
207
|
+
dragHandleIndicatorComponent()
|
|
208
|
+
) : (
|
|
209
|
+
<View
|
|
210
|
+
style={[
|
|
211
|
+
styles.dragHandleIndicator,
|
|
212
|
+
countrySelectStyle?.dragHandleIndicator,
|
|
213
|
+
]}
|
|
214
|
+
/>
|
|
215
|
+
)}
|
|
216
|
+
</View>
|
|
217
|
+
{header}
|
|
218
|
+
<Animated.View style={{flex: 1, flexDirection: 'row'}}>
|
|
219
|
+
{children}
|
|
220
|
+
</Animated.View>
|
|
221
|
+
</Animated.View>
|
|
222
|
+
</View>
|
|
223
|
+
</Modal>
|
|
224
|
+
);
|
|
225
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Text, TouchableOpacity} from 'react-native';
|
|
3
|
+
|
|
4
|
+
import {createStyles} from '../styles';
|
|
5
|
+
import {ICloseButtonProps} from '../../interface';
|
|
6
|
+
import {translations} from '../../utils/getTranslation';
|
|
7
|
+
|
|
8
|
+
export const CloseButton: React.FC<ICloseButtonProps> = ({
|
|
9
|
+
theme,
|
|
10
|
+
language,
|
|
11
|
+
onClose,
|
|
12
|
+
countrySelectStyle,
|
|
13
|
+
accessibilityLabelCloseButton,
|
|
14
|
+
accessibilityHintCloseButton,
|
|
15
|
+
}) => {
|
|
16
|
+
const styles = createStyles(theme);
|
|
17
|
+
return (
|
|
18
|
+
<TouchableOpacity
|
|
19
|
+
testID="countrySelectCloseButton"
|
|
20
|
+
accessibilityRole="button"
|
|
21
|
+
accessibilityLabel={
|
|
22
|
+
accessibilityLabelCloseButton ||
|
|
23
|
+
translations.accessibilityLabelCloseButton[language]
|
|
24
|
+
}
|
|
25
|
+
accessibilityHint={
|
|
26
|
+
accessibilityHintCloseButton ||
|
|
27
|
+
translations.accessibilityHintCloseButton[language]
|
|
28
|
+
}
|
|
29
|
+
style={[styles.closeButton, countrySelectStyle?.closeButton]}
|
|
30
|
+
activeOpacity={0.6}
|
|
31
|
+
onPress={onClose}>
|
|
32
|
+
<Text
|
|
33
|
+
style={[styles.closeButtonText, countrySelectStyle?.closeButtonText]}>
|
|
34
|
+
{'\u00D7'}
|
|
35
|
+
</Text>
|
|
36
|
+
</TouchableOpacity>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
@@ -3,17 +3,15 @@ import {View, Text, TouchableOpacity} from 'react-native';
|
|
|
3
3
|
|
|
4
4
|
import {createStyles} from '../styles';
|
|
5
5
|
import {translations} from '../../utils/getTranslation';
|
|
6
|
-
import {ICountryItemProps
|
|
7
|
-
|
|
8
|
-
const DEFAULT_LANGUAGE: ICountrySelectLanguages = 'eng';
|
|
6
|
+
import {ICountryItemProps} from '../../interface';
|
|
9
7
|
|
|
10
8
|
export const CountryItem = memo<ICountryItemProps>(
|
|
11
9
|
({
|
|
12
|
-
|
|
10
|
+
country,
|
|
11
|
+
isSelected,
|
|
13
12
|
onSelect,
|
|
14
|
-
onClose,
|
|
15
13
|
theme = 'light',
|
|
16
|
-
language,
|
|
14
|
+
language = 'eng',
|
|
17
15
|
countrySelectStyle,
|
|
18
16
|
accessibilityLabel,
|
|
19
17
|
accessibilityHint,
|
|
@@ -26,33 +24,43 @@ export const CountryItem = memo<ICountryItemProps>(
|
|
|
26
24
|
accessibilityRole="button"
|
|
27
25
|
accessibilityLabel={
|
|
28
26
|
accessibilityLabel ||
|
|
29
|
-
translations.accessibilityLabelCountryItem[language]
|
|
27
|
+
translations.accessibilityLabelCountryItem[language] +
|
|
28
|
+
` ${country.translations[language]?.common}`
|
|
30
29
|
}
|
|
31
30
|
accessibilityHint={
|
|
32
31
|
accessibilityHint ||
|
|
33
|
-
translations.accessibilityHintCountryItem[language]
|
|
32
|
+
translations.accessibilityHintCountryItem[language] +
|
|
33
|
+
` ${country.translations[language]?.common}`
|
|
34
34
|
}
|
|
35
|
-
style={[
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
35
|
+
style={[
|
|
36
|
+
styles.countryItem,
|
|
37
|
+
countrySelectStyle?.countryItem,
|
|
38
|
+
isSelected && styles.countryItemSelected,
|
|
39
|
+
]}
|
|
40
|
+
onPress={() => onSelect(country)}>
|
|
40
41
|
<Text
|
|
41
42
|
testID="countrySelectItemFlag"
|
|
42
43
|
style={[styles.flag, countrySelectStyle?.flag]}>
|
|
43
|
-
{
|
|
44
|
+
{country.flag || country.cca2}
|
|
44
45
|
</Text>
|
|
45
46
|
<View style={[styles.countryInfo, countrySelectStyle?.countryInfo]}>
|
|
46
47
|
<Text
|
|
47
48
|
testID="countrySelectItemCallingCode"
|
|
48
|
-
style={[
|
|
49
|
-
|
|
49
|
+
style={[
|
|
50
|
+
styles.callingCode,
|
|
51
|
+
countrySelectStyle?.callingCode,
|
|
52
|
+
isSelected && styles.callingCodeSelected,
|
|
53
|
+
]}>
|
|
54
|
+
{country.idd.root}
|
|
50
55
|
</Text>
|
|
51
56
|
<Text
|
|
52
57
|
testID="countrySelectItemName"
|
|
53
|
-
style={[
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
style={[
|
|
59
|
+
styles.countryName,
|
|
60
|
+
countrySelectStyle?.countryName,
|
|
61
|
+
isSelected && styles.countryNameSelected,
|
|
62
|
+
]}>
|
|
63
|
+
{country?.translations[language]?.common}
|
|
56
64
|
</Text>
|
|
57
65
|
</View>
|
|
58
66
|
</TouchableOpacity>
|