1import Ionicons from '@expo/vector-icons/build/Ionicons'; 2import { useNavigation } from '@react-navigation/native'; 3import React from 'react'; 4import { 5 Dimensions, 6 LayoutAnimation, 7 LayoutChangeEvent, 8 StyleSheet, 9 Text, 10 TextInput, 11 TextStyle, 12 TouchableOpacity, 13 View, 14} from 'react-native'; 15 16const Layout = { 17 window: { 18 width: Dimensions.get('window').width, 19 }, 20}; 21const SearchContainerHorizontalMargin = 10; 22const SearchContainerWidth = Layout.window.width - SearchContainerHorizontalMargin * 2; 23 24const SearchIcon = () => ( 25 <View style={styles.searchIconContainer}> 26 <Ionicons name="ios-search" size={18} color="#ccc" /> 27 </View> 28); 29 30export default function SearchBar({ 31 textColor, 32 cancelButtonText, 33 tintColor, 34 placeholderTextColor, 35 onChangeQuery, 36 onSubmit, 37 onCancelPress, 38 initialValue = '', 39}: { 40 initialValue?: string; 41 cancelButtonText?: string; 42 selectionColor?: string; 43 tintColor: string; 44 placeholderTextColor?: string; 45 underlineColorAndroid?: string; 46 textColor?: string; 47 onSubmit?: (query: string) => void; 48 onChangeQuery?: (query: string) => void; 49 onCancelPress?: (goBack: () => void) => void; 50}) { 51 const navigation = useNavigation(); 52 const [text, setText] = React.useState(initialValue); 53 const [showCancelButton, setShowCancelButton] = React.useState(false); 54 const [inputWidth, setInputWidth] = React.useState(SearchContainerWidth); 55 const _textInput = React.useRef<TextInput>(null); 56 57 React.useEffect(() => { 58 requestAnimationFrame(() => { 59 _textInput.current?.focus(); 60 }); 61 }, []); 62 63 const _handleLayoutCancelButton = (e: LayoutChangeEvent) => { 64 if (showCancelButton) { 65 return; 66 } 67 68 const cancelButtonWidth = e.nativeEvent.layout.width; 69 70 requestAnimationFrame(() => { 71 LayoutAnimation.configureNext({ 72 duration: 200, 73 create: { 74 type: LayoutAnimation.Types.linear, 75 property: LayoutAnimation.Properties.opacity, 76 }, 77 update: { 78 type: LayoutAnimation.Types.spring, 79 springDamping: 0.9, 80 initialVelocity: 10, 81 }, 82 }); 83 setShowCancelButton(true); 84 setInputWidth(SearchContainerWidth - cancelButtonWidth); 85 }); 86 }; 87 88 const _handleChangeText = (text: string) => { 89 setText(text); 90 onChangeQuery?.(text); 91 }; 92 93 const _handleSubmit = () => { 94 onSubmit?.(text); 95 _textInput.current?.blur?.(); 96 }; 97 98 const _handlePressCancelButton = () => { 99 if (onCancelPress) { 100 onCancelPress(navigation.goBack); 101 } else { 102 navigation.goBack(); 103 } 104 }; 105 106 const searchInputStyle: TextStyle = {}; 107 if (textColor) { 108 searchInputStyle.color = textColor; 109 } 110 111 return ( 112 <View style={styles.container}> 113 <View style={[styles.searchContainer, { width: inputWidth }]}> 114 <TextInput 115 ref={_textInput} 116 clearButtonMode="while-editing" 117 onChangeText={_handleChangeText} 118 value={text} 119 autoCapitalize="none" 120 autoCorrect={false} 121 returnKeyType="search" 122 placeholder="Search" 123 placeholderTextColor={placeholderTextColor || '#ccc'} 124 onSubmitEditing={_handleSubmit} 125 style={[styles.searchInput, searchInputStyle]} 126 /> 127 128 <SearchIcon /> 129 </View> 130 131 <View 132 key={showCancelButton ? 'visible-cancel-button' : 'layout-only-cancel-button'} 133 style={[styles.buttonContainer, { opacity: showCancelButton ? 1 : 0 }]}> 134 <TouchableOpacity 135 style={styles.button} 136 hitSlop={{ top: 15, bottom: 15, left: 15, right: 20 }} 137 onLayout={_handleLayoutCancelButton} 138 onPress={_handlePressCancelButton}> 139 <Text 140 style={{ 141 fontSize: 17, 142 color: tintColor || '#007AFF', 143 }}> 144 {cancelButtonText || 'Cancel'} 145 </Text> 146 </TouchableOpacity> 147 </View> 148 </View> 149 ); 150} 151 152const styles = StyleSheet.create({ 153 container: { 154 flex: 1, 155 flexDirection: 'row', 156 }, 157 buttonContainer: { 158 position: 'absolute', 159 right: 0, 160 top: 0, 161 paddingTop: 15, 162 flexDirection: 'row', 163 alignItems: 'center', 164 justifyContent: 'center', 165 }, 166 button: { 167 paddingRight: 17, 168 paddingLeft: 2, 169 }, 170 searchContainer: { 171 height: 30, 172 width: SearchContainerWidth, 173 backgroundColor: '#f2f2f2', 174 borderRadius: 5, 175 marginHorizontal: SearchContainerHorizontalMargin, 176 marginTop: 10, 177 paddingLeft: 27, 178 }, 179 searchIconContainer: { 180 position: 'absolute', 181 left: 7, 182 top: 6, 183 bottom: 0, 184 }, 185 searchInput: { 186 flex: 1, 187 fontSize: 14, 188 paddingTop: 1, 189 }, 190}); 191