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 }: { name: string }) => ( 66 <ExpoAPIIcon name={name} style={{ marginRight: 10, marginLeft: 6 }} /> 67 ), 68 [] 69 ); 70 71 return <ComponentListScreen renderItemRight={renderItemRight} apis={apis} />; 72} 73 74type SearchStack = { 75 search: { q?: string }; 76}; 77 78const Stack = createStackNavigator<SearchStack>(); 79 80export default () => ( 81 <Stack.Navigator> 82 <Stack.Screen 83 name="search" 84 component={SearchScreen} 85 options={({ navigation, route }) => ({ 86 header: () => ( 87 <Header 88 navigation={navigation} 89 tintColor={Colors.tintColor} 90 backButton={Platform.OS === 'android'}> 91 <SearchBar 92 initialValue={route?.params?.q ?? ''} 93 onChangeQuery={(q) => navigation.setParams({ q })} 94 underlineColorAndroid="#fff" 95 tintColor={Colors.tintColor} 96 /> 97 </Header> 98 ), 99 })} 100 /> 101 </Stack.Navigator> 102); 103 104const styles = { 105 container: { 106 backgroundColor: '#fff', 107 108 ...Platform.select({ 109 ios: { 110 borderBottomWidth: StyleSheet.hairlineWidth, 111 borderBottomColor: '#A7A7AA', 112 }, 113 default: { 114 shadowColor: 'black', 115 shadowOpacity: 0.1, 116 shadowRadius: StyleSheet.hairlineWidth, 117 shadowOffset: { 118 width: 0, 119 height: StyleSheet.hairlineWidth, 120 }, 121 elevation: 4, 122 }, 123 }), 124 }, 125 appBar: { 126 flex: 1, 127 }, 128 header: { 129 flexDirection: 'row', 130 }, 131 item: { 132 justifyContent: 'center', 133 alignItems: 'center', 134 backgroundColor: 'transparent', 135 }, 136 title: { 137 bottom: 0, 138 left: TITLE_OFFSET, 139 right: TITLE_OFFSET, 140 top: 0, 141 position: 'absolute', 142 alignItems: Platform.OS === 'ios' ? 'center' : 'flex-start', 143 }, 144 left: { 145 left: 0, 146 bottom: 0, 147 top: 0, 148 position: 'absolute', 149 }, 150 right: { 151 right: 0, 152 bottom: 0, 153 top: 0, 154 position: 'absolute', 155 }, 156}; 157