1import { createStackNavigator, HeaderBackButton, StackScreenProps } from '@react-navigation/stack'; 2import Fuse from 'fuse.js'; 3import React from 'react'; 4import { Animated, Platform, StyleSheet, View } from 'react-native'; 5import { useSafeAreaInsets } from 'react-native-safe-area-context'; 6 7import ExpoAPIIcon from '../components/ExpoAPIIcon'; 8import SearchBar from '../components/SearchBar'; 9import { Colors } from '../constants'; 10import ComponentListScreen from './ComponentListScreen'; 11import { ScreenItems as ApiScreenItems } from './ExpoApisScreen'; 12import { ScreenItems as ComponentScreenItems } from './ExpoComponentsScreen'; 13 14const fuse = new Fuse(ApiScreenItems.concat(ComponentScreenItems), { keys: ['name'] }); 15 16const APPBAR_HEIGHT = Platform.OS === 'ios' ? 50 : 56; 17const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56; 18 19function Header({ 20 children, 21 backButton, 22 tintColor, 23 navigation, 24}: { 25 children?: React.ReactNode; 26 backButton?: boolean; 27 tintColor?: string; 28 navigation: any; 29}) { 30 const { top } = useSafeAreaInsets(); 31 // @todo: this is static and we don't know if it's visible or not on iOS. 32 // need to use a more reliable and cross-platform API when one exists, like 33 // LayoutContext. We also don't know if it's translucent or not on Android 34 // and depend on react-native-safe-area-view to tell us. 35 const STATUSBAR_HEIGHT = top || 8; 36 37 return ( 38 <Animated.View 39 style={[ 40 styles.container, 41 { paddingTop: STATUSBAR_HEIGHT, height: STATUSBAR_HEIGHT + APPBAR_HEIGHT }, 42 ]}> 43 <View style={styles.appBar}> 44 <View style={[StyleSheet.absoluteFill, { flexDirection: 'row' }]}> 45 {backButton && ( 46 <HeaderBackButton 47 onPress={() => navigation.goBack()} 48 pressColorAndroid={tintColor || '#fff'} 49 tintColor={tintColor} 50 /> 51 )} 52 {children} 53 </View> 54 </View> 55 </Animated.View> 56 ); 57} 58 59function SearchScreen({ route }: StackScreenProps<SearchStack, 'search'>) { 60 const query = route?.params?.q ?? ''; 61 62 const apis = React.useMemo(() => fuse.search(query).map(({ item }) => item), [query]); 63 64 const renderItemRight = React.useCallback( 65 ({ name }) => <ExpoAPIIcon name={name} style={{ marginRight: 10, marginLeft: 6 }} />, 66 [] 67 ); 68 69 return <ComponentListScreen renderItemRight={renderItemRight} apis={apis} />; 70} 71 72type SearchStack = { 73 search: { q?: string }; 74}; 75 76const Stack = createStackNavigator<SearchStack>(); 77 78export default () => ( 79 <Stack.Navigator> 80 <Stack.Screen 81 name="search" 82 component={SearchScreen} 83 options={({ navigation, route }) => ({ 84 header: () => ( 85 <Header 86 navigation={navigation} 87 tintColor={Colors.tintColor} 88 backButton={Platform.OS === 'android'}> 89 <SearchBar 90 initialValue={route?.params?.q ?? ''} 91 onChangeQuery={q => navigation.setParams({ q })} 92 underlineColorAndroid="#fff" 93 tintColor={Colors.tintColor} 94 /> 95 </Header> 96 ), 97 })} 98 /> 99 </Stack.Navigator> 100); 101 102const styles = { 103 container: { 104 backgroundColor: '#fff', 105 106 ...Platform.select({ 107 ios: { 108 borderBottomWidth: StyleSheet.hairlineWidth, 109 borderBottomColor: '#A7A7AA', 110 }, 111 default: { 112 shadowColor: 'black', 113 shadowOpacity: 0.1, 114 shadowRadius: StyleSheet.hairlineWidth, 115 shadowOffset: { 116 width: 0, 117 height: StyleSheet.hairlineWidth, 118 }, 119 elevation: 4, 120 }, 121 }), 122 }, 123 appBar: { 124 flex: 1, 125 }, 126 header: { 127 flexDirection: 'row', 128 }, 129 item: { 130 justifyContent: 'center', 131 alignItems: 'center', 132 backgroundColor: 'transparent', 133 }, 134 title: { 135 bottom: 0, 136 left: TITLE_OFFSET, 137 right: TITLE_OFFSET, 138 top: 0, 139 position: 'absolute', 140 alignItems: Platform.OS === 'ios' ? 'center' : 'flex-start', 141 }, 142 left: { 143 left: 0, 144 bottom: 0, 145 top: 0, 146 position: 'absolute', 147 }, 148 right: { 149 right: 0, 150 bottom: 0, 151 top: 0, 152 position: 'absolute', 153 }, 154}; 155