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