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