index.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. import React, { useRef, useState, useCallback } from 'react';
  2. import {
  3. View,
  4. Text,
  5. TouchableOpacity,
  6. StyleSheet,
  7. StyleProp,
  8. ViewStyle,
  9. FlatList,
  10. TextInput,
  11. Dimensions,
  12. Platform
  13. } from 'react-native';
  14. import ActionSheet, { ActionSheetRef } from 'react-native-actions-sheet';
  15. import ChevronIcon from 'assets/icons/travels-screens/down-arrow.svg';
  16. import { Colors } from 'src/theme';
  17. import CloseSvg from 'assets/icons/close.svg';
  18. import CheckSvg from 'assets/icons/mark.svg';
  19. export interface SelectSheetProps<T extends Record<string, any>> {
  20. data: T[];
  21. labelField: keyof T;
  22. valueField: keyof T;
  23. value?: T[keyof T] | null;
  24. onChange: (item: T) => void;
  25. placeholder?: string;
  26. searchable?: boolean;
  27. searchPlaceholder?: string;
  28. disabled?: boolean;
  29. style?: StyleProp<ViewStyle>;
  30. renderItem?: (item: T, isSelected: boolean) => React.ReactNode;
  31. name?: string;
  32. hideTitle?: boolean;
  33. initialSnapPoint?: number;
  34. }
  35. export const SelectSheet = <T extends Record<string, any>>({
  36. data,
  37. labelField,
  38. valueField,
  39. value,
  40. onChange,
  41. placeholder = 'Select...',
  42. searchable = false,
  43. searchPlaceholder = 'Search...',
  44. disabled = false,
  45. style,
  46. renderItem,
  47. name,
  48. hideTitle = false,
  49. initialSnapPoint = 70
  50. }: SelectSheetProps<T>) => {
  51. const sheetRef = useRef<ActionSheetRef>(null);
  52. const [query, setQuery] = useState('');
  53. const selectedItem = value != null ? data.find((item) => item[valueField] === value) : null;
  54. const filteredData =
  55. searchable && query.trim()
  56. ? data.filter((item) => String(item[labelField]).toLowerCase().includes(query.toLowerCase()))
  57. : data;
  58. const handleSelect = useCallback(
  59. (item: T) => {
  60. sheetRef.current?.hide();
  61. onChange(item);
  62. },
  63. [onChange]
  64. );
  65. const handleOpen = useCallback(() => {
  66. if (disabled) return;
  67. setQuery('');
  68. sheetRef.current?.show();
  69. }, [disabled]);
  70. return (
  71. <>
  72. <TouchableOpacity
  73. onPress={handleOpen}
  74. activeOpacity={disabled ? 1 : 0.7}
  75. style={[styles.trigger, disabled && styles.triggerDisabled, style]}
  76. >
  77. <Text
  78. style={[styles.triggerText, !selectedItem && styles.placeholderText]}
  79. numberOfLines={1}
  80. >
  81. {selectedItem ? String(selectedItem[labelField]) : placeholder}
  82. </Text>
  83. <ChevronIcon width={12} height={12} fill={Colors.TEXT_GRAY} style={styles.chevron} />
  84. </TouchableOpacity>
  85. <ActionSheet
  86. ref={sheetRef}
  87. gestureEnabled={Platform.OS !== 'android'}
  88. closeOnTouchBackdrop
  89. snapPoints={[initialSnapPoint, 100]}
  90. initialSnapIndex={0}
  91. defaultOverlayOpacity={0.4}
  92. containerStyle={styles.sheetContainer}
  93. onClose={() => setQuery('')}
  94. >
  95. <View style={styles.sheetContent}>
  96. <TouchableOpacity
  97. onPress={() => sheetRef.current?.hide()}
  98. style={[styles.closeBtnAlone, Platform.OS === 'ios' ? { paddingTop: 6 } : {}]}
  99. hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
  100. >
  101. <CloseSvg width={15} height={15} />
  102. </TouchableOpacity>
  103. {!hideTitle && (
  104. <View style={styles.titleRow}>
  105. <Text style={[styles.sheetTitle, Platform.OS === 'android' ? { paddingTop: 0 } : {}]}>
  106. {placeholder}
  107. </Text>
  108. </View>
  109. )}
  110. {searchable && (
  111. <View style={styles.searchWrapper}>
  112. <TextInput
  113. style={styles.searchInput}
  114. placeholder={searchPlaceholder}
  115. placeholderTextColor={Colors.LIGHT_GRAY}
  116. value={query}
  117. onChangeText={setQuery}
  118. autoCorrect={false}
  119. clearButtonMode="while-editing"
  120. />
  121. </View>
  122. )}
  123. <FlatList
  124. data={filteredData}
  125. keyExtractor={(item: T, i: number) => String(item[valueField] ?? i)}
  126. keyboardShouldPersistTaps="handled"
  127. style={styles.list}
  128. contentContainerStyle={styles.listContent}
  129. initialNumToRender={20}
  130. renderItem={({ item }: { item: T }) => {
  131. const isSelected = item[valueField] === value;
  132. if (renderItem) {
  133. return (
  134. <TouchableOpacity onPress={() => handleSelect(item)} activeOpacity={0.7}>
  135. {renderItem(item, isSelected)}
  136. </TouchableOpacity>
  137. );
  138. }
  139. return (
  140. <TouchableOpacity
  141. style={styles.option}
  142. onPress={() => handleSelect(item)}
  143. activeOpacity={0.7}
  144. >
  145. <Text
  146. style={[styles.optionText, isSelected && styles.optionTextSelectedWeight]}
  147. numberOfLines={1}
  148. >
  149. {String(item[labelField])}
  150. </Text>
  151. {isSelected && (
  152. <View style={styles.checkmark}>
  153. <CheckSvg fill={Colors.DARK_BLUE} height={12} width={15} />
  154. </View>
  155. )}
  156. </TouchableOpacity>
  157. );
  158. }}
  159. />
  160. </View>
  161. </ActionSheet>
  162. </>
  163. );
  164. };
  165. const styles = StyleSheet.create({
  166. trigger: {
  167. height: 40,
  168. backgroundColor: Colors.FILL_LIGHT,
  169. borderRadius: 8,
  170. paddingHorizontal: 12,
  171. flexDirection: 'row',
  172. alignItems: 'center',
  173. justifyContent: 'space-between'
  174. },
  175. triggerDisabled: {
  176. opacity: 0.5
  177. },
  178. triggerText: {
  179. flex: 1,
  180. fontSize: 15,
  181. color: Colors.DARK_BLUE,
  182. marginRight: 6
  183. },
  184. placeholderText: {
  185. color: Colors.TEXT_GRAY
  186. },
  187. sheetContent: {
  188. height: Dimensions.get('window').height * 0.9,
  189. backgroundColor: Colors.WHITE,
  190. borderTopLeftRadius: 20,
  191. borderTopRightRadius: 20,
  192. overflow: 'hidden'
  193. },
  194. sheetContainer: {
  195. height: '90%',
  196. backgroundColor: Colors.WHITE,
  197. borderTopLeftRadius: 20,
  198. borderTopRightRadius: 20
  199. },
  200. indicator: {
  201. width: 44,
  202. height: 5,
  203. borderRadius: 3,
  204. backgroundColor: Colors.LIGHT_GRAY,
  205. marginTop: 8
  206. },
  207. searchWrapper: {
  208. paddingHorizontal: 16,
  209. paddingVertical: 12
  210. },
  211. searchInput: {
  212. height: 44,
  213. backgroundColor: Colors.FILL_LIGHT,
  214. borderRadius: 6,
  215. paddingHorizontal: 16,
  216. fontSize: 16,
  217. color: Colors.DARK_BLUE
  218. },
  219. list: {
  220. flex: 1
  221. },
  222. listContent: {
  223. paddingBottom: 40,
  224. paddingTop: 8
  225. },
  226. option: {
  227. flexDirection: 'row',
  228. alignItems: 'center',
  229. paddingHorizontal: 24,
  230. paddingVertical: 16
  231. },
  232. optionText: {
  233. flex: 1,
  234. fontSize: 16,
  235. color: Colors.DARK_BLUE
  236. },
  237. optionTextSelectedWeight: {
  238. fontWeight: '700'
  239. },
  240. checkmark: {
  241. marginLeft: 12
  242. },
  243. checkmarkText: {
  244. fontSize: 18,
  245. color: Colors.DARK_BLUE,
  246. fontWeight: '700'
  247. },
  248. chevron: {
  249. marginLeft: 2
  250. },
  251. titleRow: {
  252. flexDirection: 'row',
  253. alignItems: 'center',
  254. borderBottomWidth: StyleSheet.hairlineWidth,
  255. borderBottomColor: Colors.DARK_LIGHT
  256. },
  257. sheetTitle: {
  258. flex: 1,
  259. fontSize: 16,
  260. fontWeight: '700',
  261. color: Colors.DARK_BLUE,
  262. paddingVertical: 12,
  263. paddingHorizontal: 24,
  264. textAlign: 'center'
  265. },
  266. closeBtn: {
  267. paddingHorizontal: 16,
  268. paddingVertical: 12
  269. },
  270. closeBtnAlone: {
  271. alignSelf: 'flex-end',
  272. paddingHorizontal: 16,
  273. paddingTop: 16
  274. }
  275. });