├── .gitignore ├── README.md ├── example ├── .expo-shared │ └── assets.json ├── .gitignore ├── App.tsx ├── __generated__ │ └── AppEntry.js ├── app.json ├── assets │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash.png ├── babel.config.js ├── metro.config.js ├── package.json └── tsconfig.json ├── lib ├── README.md ├── index.d.ts ├── index.js ├── package.json └── src │ ├── Header.js │ ├── SearchBar.ios.js │ ├── SearchBar.js │ └── SearchLayout.js ├── package.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | example/node_modules/**/* 3 | example/.expo/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-navigation-addon-search-layout 2 | 3 | A plain but perfectly acceptable search layout screen that looks good on iOS and Android. 4 | 5 | ## Installation 6 | 7 | ``` 8 | npm install react-navigation-addon-search-layout 9 | ``` 10 | 11 | This requires that you have `react-native-vector-icons` installed in your project, it uses the `Ionicons` font family. If you use the Expo managed workflow, it will work out of the box as that comes preinstalled and is available through `@expo/vector-icons`'. 12 | 13 | ## Usage 14 | 15 | See [example/App.tsx](https://github.com/react-navigation/search-layout/blob/master/example/App.tsx). 16 | -------------------------------------------------------------------------------- /example/.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p8 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | 12 | # macOS 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /example/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { View, Text, Platform, StyleSheet } from "react-native"; 3 | import { 4 | NavigationContainer, 5 | useNavigation, 6 | RouteProp, 7 | } from "@react-navigation/native"; 8 | import { createStackNavigator } from "@react-navigation/stack"; 9 | import { RectButton, BorderlessButton } from "react-native-gesture-handler"; 10 | import SearchLayout from "react-navigation-addon-search-layout"; 11 | import { Ionicons } from "@expo/vector-icons"; 12 | import { StatusBar } from "expo-status-bar"; 13 | 14 | type RootStackParamList = { 15 | Home: undefined; 16 | Search: undefined; 17 | Result: { text: string }; 18 | }; 19 | 20 | type ResultScreenRouteProp = RouteProp; 21 | 22 | function HomeScreen() { 23 | return ( 24 | 25 | Hello there!!! 26 | 27 | ); 28 | } 29 | 30 | function SearchScreen() { 31 | const [searchText, setSearchText] = useState(""); 32 | const navigation = useNavigation(); 33 | 34 | const _handleQueryChange = (searchText: string) => { 35 | setSearchText(searchText); 36 | }; 37 | 38 | const _executeSearch = () => { 39 | alert("do search!"); 40 | }; 41 | 42 | return ( 43 | 44 | {searchText ? ( 45 | 53 | navigation.navigate("Result", { 54 | text: searchText, 55 | }) 56 | } 57 | > 58 | {searchText}! 59 | 60 | ) : null} 61 | 62 | ); 63 | } 64 | 65 | function ResultScreen(props: ResultScreenRouteProp) { 66 | return ( 67 | 68 | {props.params.text} result! 69 | 70 | ); 71 | } 72 | 73 | const Stack = createStackNavigator(); 74 | 75 | export default function App() { 76 | return ( 77 | <> 78 | 79 | 80 | { 85 | const navigation = useNavigation(); 86 | return ( 87 | navigation.navigate("Search")} 89 | style={{ marginRight: 15 }} 90 | > 91 | 96 | 97 | ); 98 | }, 99 | }} 100 | /> 101 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | ); 117 | } 118 | 119 | const styles = StyleSheet.create({ 120 | container: { 121 | flex: 1, 122 | justifyContent: "center", 123 | alignItems: "center", 124 | }, 125 | }); 126 | -------------------------------------------------------------------------------- /example/__generated__/AppEntry.js: -------------------------------------------------------------------------------- 1 | // @generated by expo-yarn-workspaces 2 | 3 | import 'expo/build/Expo.fx'; 4 | import { activateKeepAwake } from 'expo-keep-awake'; 5 | import registerRootComponent from 'expo/build/launch/registerRootComponent'; 6 | 7 | import App from '../App'; 8 | 9 | if (__DEV__) { 10 | activateKeepAwake(); 11 | } 12 | 13 | registerRootComponent(App); 14 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "example", 4 | "slug": "example", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "splash": { 9 | "image": "./assets/splash.png", 10 | "resizeMode": "contain", 11 | "backgroundColor": "#ffffff" 12 | }, 13 | "updates": { 14 | "fallbackToCacheTimeout": 0 15 | }, 16 | "assetBundlePatterns": [ 17 | "**/*" 18 | ], 19 | "ios": { 20 | "supportsTablet": true 21 | }, 22 | "android": { 23 | "adaptiveIcon": { 24 | "foregroundImage": "./assets/adaptive-icon.png", 25 | "backgroundColor": "#FFFFFF" 26 | } 27 | }, 28 | "web": { 29 | "favicon": "./assets/favicon.png" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-navigation/search-layout/35922cabce9f62b3aa90fe5656a587aa6a0aaf2b/example/assets/adaptive-icon.png -------------------------------------------------------------------------------- /example/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-navigation/search-layout/35922cabce9f62b3aa90fe5656a587aa6a0aaf2b/example/assets/favicon.png -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-navigation/search-layout/35922cabce9f62b3aa90fe5656a587aa6a0aaf2b/example/assets/icon.png -------------------------------------------------------------------------------- /example/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-navigation/search-layout/35922cabce9f62b3aa90fe5656a587aa6a0aaf2b/example/assets/splash.png -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const { createMetroConfiguration } = require('expo-yarn-workspaces'); 2 | 3 | const defaultConfig = createMetroConfiguration(__dirname); 4 | 5 | module.exports = { 6 | ...defaultConfig, 7 | server: { 8 | ...defaultConfig.server, 9 | enhanceMiddleware: (middleware) => { 10 | return (req, res, next) => { 11 | // When an asset is imported outside the project root, it has wrong path on Android 12 | // So we fix the path to correct one 13 | if (/\/packages\/.+\.png\?.+$/.test(req.url)) { 14 | req.url = `/assets/../${req.url}`; 15 | } 16 | 17 | return middleware(req, res, next); 18 | }; 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.0.0", 4 | "main": "__generated__/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web", 10 | "eject": "expo eject", 11 | "postinstall": "expo-yarn-workspaces postinstall" 12 | }, 13 | "dependencies": { 14 | "@react-native-community/masked-view": "0.1.10", 15 | "@react-navigation/native": "^5.9.3", 16 | "@react-navigation/stack": "^5.14.3", 17 | "expo": "~40.0.0", 18 | "expo-status-bar": "~1.0.3", 19 | "react": "16.13.1", 20 | "react-dom": "16.13.1", 21 | "react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz", 22 | "react-native-gesture-handler": "~1.8.0", 23 | "react-native-reanimated": "~1.13.0", 24 | "react-native-safe-area-context": "3.1.9", 25 | "react-native-screens": "~2.15.0", 26 | "react-native-web": "~0.13.12", 27 | "react-navigation-addon-search-layout": "*" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "~7.9.0", 31 | "@types/react": "~16.9.35", 32 | "@types/react-dom": "~16.9.8", 33 | "@types/react-native": "~0.63.2", 34 | "typescript": "~4.0.0" 35 | }, 36 | "private": true 37 | } 38 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "jsx": "react-native", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "noEmit": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true, 10 | "strict": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | # react-navigation-addon-search-layout 2 | 3 | A plain but perfectly acceptable search layout screen that looks good on 4 | iOS and Android. 5 | 6 | ## Installation 7 | 8 | ``` 9 | npm install react-navigation-addon-search-layout 10 | ``` 11 | 12 | This requires that you have `react-native-vector-icons` installed in 13 | your project, it uses the `Ionicons` font family. If you use the Expo 14 | managed workflow, it will work out of the box as that comes preinstalled 15 | and is available through `@expo/vector-icons`'. 16 | 17 | ## Usage 18 | 19 | Here's an example of how you can use this: 20 | 21 | ```js 22 | import * as React from 'react'; 23 | import { 24 | Animated, 25 | Button, 26 | Platform, 27 | Text, 28 | StyleSheet, 29 | View, 30 | } from 'react-native'; 31 | import { createAppContainer } from 'react-navigation'; 32 | import { 33 | createStackNavigator, 34 | StackViewTransitionConfigs, 35 | } from 'react-navigation-stack'; 36 | import { RectButton, BorderlessButton } from 'react-native-gesture-handler'; 37 | import SearchLayout from 'react-navigation-addon-search-layout'; 38 | import { Ionicons } from '@expo/vector-icons'; 39 | 40 | class HomeScreen extends React.Component { 41 | static navigationOptions = ({ navigation }) => ({ 42 | title: 'Home', 43 | headerRight: ( 44 | navigation.navigate('Search')} 46 | style={{ marginRight: 15 }}> 47 | 52 | 53 | ), 54 | }); 55 | 56 | render() { 57 | return ( 58 | 59 | Hello there!!! 60 | 61 | ); 62 | } 63 | } 64 | 65 | class ResultScreen extends React.Component { 66 | static navigationOptions = { 67 | title: 'Result', 68 | }; 69 | 70 | render() { 71 | return ( 72 | 73 | {this.props.navigation.getParam('text')} result! 74 | 75 | ); 76 | } 77 | } 78 | 79 | class SearchScreen extends React.Component { 80 | static navigationOptions = { 81 | header: null, 82 | }; 83 | 84 | state = { 85 | searchText: null, 86 | }; 87 | 88 | _handleQueryChange = searchText => { 89 | this.setState({ searchText }); 90 | }; 91 | 92 | _executeSearch = () => { 93 | alert('do search!'); 94 | }; 95 | 96 | render() { 97 | let { searchText } = this.state; 98 | 99 | return ( 100 | 103 | {searchText ? ( 104 | 112 | this.props.navigation.navigate('Result', { 113 | text: this.state.searchText, 114 | }) 115 | }> 116 | {searchText}! 117 | 118 | ) : null} 119 | 120 | ); 121 | } 122 | } 123 | 124 | let SearchStack = createStackNavigator( 125 | { 126 | Home: HomeScreen, 127 | Search: SearchScreen, 128 | }, 129 | { 130 | transitionConfig: () => StackViewTransitionConfigs.NoAnimation, 131 | navigationOptions: { 132 | header: null, 133 | }, 134 | defaultNavigationOptions: { 135 | gesturesEnabled: false, 136 | }, 137 | } 138 | ); 139 | 140 | let MainStack = createStackNavigator({ 141 | Feed: SearchStack, 142 | Result: ResultScreen, 143 | }); 144 | 145 | export default createAppContainer(MainStack); 146 | 147 | const styles = StyleSheet.create({ 148 | container: { 149 | flex: 1, 150 | alignItems: 'center', 151 | justifyContent: 'center', 152 | }, 153 | }); 154 | ``` 155 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-navigation-addon-search-layout' { 2 | import * as React from 'react'; 3 | 4 | interface SearchLayoutProps { 5 | /** Callback that fires when the text in the search input is changed **/ 6 | onChangeQuery: (query: string) => void; 7 | 8 | /** Callback that fires when the user submits the input by pressing return **/ 9 | onSubmit?: (query: string) => void; 10 | 11 | /** Background color of the header that contains the search bar **/ 12 | headerBackgroundColor?: string; 13 | 14 | /** Tint color of the header that contains the search bar. Used to color the back buttoni on Android, the cancel button on iOS, and the color of and ripple around the clear button on Android **/ 15 | headerTintColor?: string; 16 | 17 | /** Color of the placeholder text that is visible when the user has not entered any input into the search box **/ 18 | placeholderTextColor?: string; 19 | 20 | /** Color of text that the user enters into the search box **/ 21 | textColor?: string; 22 | 23 | /** FontFamily of text that the user enters into the search box and for the text shown as cancel button **/ 24 | textFontFamily?: string; 25 | 26 | /** Underline color of the text input on Android **/ 27 | searchInputUnderlineColorAndroid?: string; 28 | 29 | /** Override headerTintColor for the cancel button / clear button **/ 30 | searchInputTintColor?: string; 31 | 32 | /** Alternative to using children to render the results **/ 33 | renderResults?: () => React.ReactElement | null; 34 | } 35 | 36 | export default class SearchLayout extends React.Component {} 37 | } 38 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | export default from './src/SearchLayout'; 2 | -------------------------------------------------------------------------------- /lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-navigation-addon-search-layout", 3 | "version": "0.15.1", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "types": "index.d.ts", 7 | "files": [ 8 | "src", 9 | "index.js", 10 | "react-navigation-search-layout.d.ts" 11 | ], 12 | "dependencies": { 13 | "react-native-iphone-x-helper": "^1.3.1", 14 | "react-native-safe-area-view": "^1.1.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Animated, 4 | Platform, 5 | StatusBar, 6 | StyleSheet, 7 | View, 8 | } from 'react-native'; 9 | 10 | import { HeaderBackButton } from '@react-navigation/stack'; 11 | import { getStatusBarHeight } from 'react-native-safe-area-view'; 12 | import { isIphoneX } from 'react-native-iphone-x-helper'; 13 | import { useNavigation } from '@react-navigation/native'; 14 | 15 | // @todo: make this work properly when in landscape 16 | const hasNotch = isIphoneX(); 17 | 18 | const APPBAR_HEIGHT = Platform.OS === 'ios' ? 50 : 56; 19 | const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56; 20 | 21 | class Header extends React.PureComponent { 22 | constructor(props) { 23 | super(props); 24 | 25 | // @todo: this is static and we don't know if it's visible or not on iOS. 26 | // need to use a more reliable and cross-platform API when one exists, like 27 | // LayoutContext. We also don't know if it's translucent or not on Android 28 | // and depend on react-native-safe-area-view to tell us. 29 | const ANDROID_STATUS_BAR_HEIGHT = getStatusBarHeight 30 | ? getStatusBarHeight() 31 | : StatusBar.currentHeight; 32 | const STATUSBAR_HEIGHT = 33 | Platform.OS === 'ios' ? (hasNotch ? 40 : 25) : ANDROID_STATUS_BAR_HEIGHT; 34 | 35 | let platformContainerStyles; 36 | if (Platform.OS === 'ios') { 37 | platformContainerStyles = { 38 | borderBottomWidth: StyleSheet.hairlineWidth, 39 | borderBottomColor: '#A7A7AA', 40 | }; 41 | } else { 42 | platformContainerStyles = { 43 | shadowColor: 'black', 44 | shadowOpacity: 0.1, 45 | shadowRadius: StyleSheet.hairlineWidth, 46 | shadowOffset: { 47 | height: StyleSheet.hairlineWidth, 48 | }, 49 | elevation: 4, 50 | }; 51 | } 52 | 53 | this.styles = { 54 | container: { 55 | backgroundColor: '#fff', 56 | paddingTop: STATUSBAR_HEIGHT, 57 | height: STATUSBAR_HEIGHT + APPBAR_HEIGHT, 58 | ...platformContainerStyles, 59 | }, 60 | appBar: { 61 | flex: 1, 62 | }, 63 | header: { 64 | flexDirection: 'row', 65 | }, 66 | item: { 67 | justifyContent: 'center', 68 | alignItems: 'center', 69 | backgroundColor: 'transparent', 70 | }, 71 | title: { 72 | bottom: 0, 73 | left: TITLE_OFFSET, 74 | right: TITLE_OFFSET, 75 | top: 0, 76 | position: 'absolute', 77 | alignItems: Platform.OS === 'ios' ? 'center' : 'flex-start', 78 | }, 79 | left: { 80 | left: 0, 81 | bottom: 0, 82 | top: 0, 83 | position: 'absolute', 84 | }, 85 | right: { 86 | right: 0, 87 | bottom: 0, 88 | top: 0, 89 | position: 'absolute', 90 | }, 91 | }; 92 | } 93 | 94 | _navigateBack = () => { 95 | if (this.props.onCancelPress) { 96 | this.props.onCancelPress(this.props.navigation.goBack); 97 | } else { 98 | this.props.navigation.goBack(); 99 | } 100 | }; 101 | 102 | _maybeRenderBackButton = () => { 103 | if (!this.props.backButton) { 104 | return; 105 | } 106 | 107 | return ( 108 | 116 | ); 117 | }; 118 | 119 | render() { 120 | let { styles } = this; 121 | let headerStyle = {}; 122 | if (this.props.backgroundColor) { 123 | headerStyle.backgroundColor = this.props.backgroundColor; 124 | } 125 | 126 | return ( 127 | 128 | 129 | 130 | {this._maybeRenderBackButton()} 131 | {this.props.children} 132 | 133 | 134 | 135 | ); 136 | } 137 | } 138 | 139 | export default function (props) { 140 | const navigation = useNavigation(); 141 | 142 | return ( 143 |
144 | ); 145 | } 146 | -------------------------------------------------------------------------------- /lib/src/SearchBar.ios.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Dimensions, 4 | LayoutAnimation, 5 | StyleSheet, 6 | Text, 7 | TextInput, 8 | TouchableOpacity, 9 | TouchableWithoutFeedback, 10 | View, 11 | } from 'react-native'; 12 | import Ionicons from 'react-native-vector-icons/Ionicons'; 13 | import { useNavigation } from '@react-navigation/native'; 14 | 15 | const Layout = { 16 | window: { 17 | width: Dimensions.get('window').width, 18 | }, 19 | }; 20 | const SearchContainerHorizontalMargin = 10; 21 | const SearchContainerWidth = 22 | Layout.window.width - SearchContainerHorizontalMargin * 2; 23 | 24 | const SearchIcon = () => ( 25 | 26 | 27 | 28 | ); 29 | 30 | class PlaceholderButtonSearchBar extends React.PureComponent { 31 | static defaultProps = { 32 | placeholder: 'Search', 33 | placeholderTextColor: '#ccc', 34 | } 35 | 36 | render() { 37 | return ( 38 | 39 | 42 | 43 | 50 | 51 | 52 | 53 | 54 | 55 | ); 56 | } 57 | 58 | _handlePress = () => { 59 | this.props.navigator.push('search'); 60 | }; 61 | } 62 | 63 | class SearchBar extends React.PureComponent { 64 | state = { 65 | text: '', 66 | showCancelButton: false, 67 | inputWidth: SearchContainerWidth, 68 | }; 69 | 70 | _textInput: TextInput; 71 | 72 | componentDidMount() { 73 | requestAnimationFrame(() => { 74 | this._textInput.focus(); 75 | }); 76 | } 77 | 78 | _handleLayoutCancelButton = (e: Object) => { 79 | if (this.state.showCancelButton) { 80 | return; 81 | } 82 | 83 | const cancelButtonWidth = e.nativeEvent.layout.width; 84 | 85 | requestAnimationFrame(() => { 86 | LayoutAnimation.configureNext({ 87 | duration: 200, 88 | create: { 89 | type: LayoutAnimation.Types.linear, 90 | property: LayoutAnimation.Properties.opacity, 91 | }, 92 | update: { 93 | type: LayoutAnimation.Types.spring, 94 | springDamping: 0.9, 95 | initialVelocity: 10, 96 | }, 97 | }); 98 | 99 | this.setState({ 100 | showCancelButton: true, 101 | inputWidth: SearchContainerWidth - cancelButtonWidth, 102 | }); 103 | }); 104 | }; 105 | 106 | render() { 107 | let { inputWidth, showCancelButton } = this.state; 108 | let searchInputStyle = {}; 109 | if (this.props.textColor) { 110 | searchInputStyle.color = this.props.textColor; 111 | } 112 | if (this.props.textFontFamily) { 113 | searchInputStyle.fontFamily = this.props.textFontFamily; 114 | } 115 | 116 | return ( 117 | 118 | 119 | { 121 | this._textInput = view; 122 | }} 123 | clearButtonMode="while-editing" 124 | onChangeText={this._handleChangeText} 125 | value={this.state.text} 126 | autoCapitalize="none" 127 | autoCorrect={false} 128 | returnKeyType="search" 129 | placeholder="Search" 130 | placeholderTextColor={this.props.placeholderTextColor || '#ccc'} 131 | onSubmitEditing={this._handleSubmit} 132 | style={[styles.searchInput, searchInputStyle]} 133 | /> 134 | 135 | 136 | 137 | 138 | 148 | 153 | 159 | {this.props.cancelButtonText || 'Cancel'} 160 | 161 | 162 | 163 | 164 | ); 165 | } 166 | 167 | _handleChangeText = text => { 168 | this.setState({ text }); 169 | this.props.onChangeQuery && this.props.onChangeQuery(text); 170 | }; 171 | 172 | _handleSubmit = () => { 173 | let { text } = this.state; 174 | this.props.onSubmit && this.props.onSubmit(text); 175 | this._textInput.blur(); 176 | }; 177 | 178 | _handlePressCancelButton = () => { 179 | if (this.props.onCancelPress) { 180 | this.props.onCancelPress(this.props.navigation.goBack); 181 | } else { 182 | this.props.navigation.goBack(); 183 | } 184 | }; 185 | } 186 | 187 | export default function (props) { 188 | const navigation = useNavigation(); 189 | 190 | return ( 191 | 192 | ); 193 | } 194 | 195 | const styles = StyleSheet.create({ 196 | container: { 197 | flex: 1, 198 | flexDirection: 'row', 199 | }, 200 | buttonContainer: { 201 | position: 'absolute', 202 | right: 0, 203 | top: 0, 204 | paddingTop: 15, 205 | flexDirection: 'row', 206 | alignItems: 'center', 207 | justifyContent: 'center', 208 | }, 209 | button: { 210 | paddingRight: 17, 211 | paddingLeft: 2, 212 | }, 213 | searchContainer: { 214 | height: 30, 215 | width: SearchContainerWidth, 216 | backgroundColor: '#f2f2f2', 217 | borderRadius: 5, 218 | marginHorizontal: SearchContainerHorizontalMargin, 219 | marginTop: 10, 220 | paddingLeft: 27, 221 | }, 222 | searchIconContainer: { 223 | position: 'absolute', 224 | left: 7, 225 | top: 6, 226 | bottom: 0, 227 | }, 228 | searchInput: { 229 | flex: 1, 230 | fontSize: 14, 231 | paddingTop: 1, 232 | }, 233 | }); 234 | -------------------------------------------------------------------------------- /lib/src/SearchBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, TextInput, TouchableNativeFeedback, View } from 'react-native'; 3 | import Ionicons from 'react-native-vector-icons/Ionicons'; 4 | import { useNavigation } from '@react-navigation/native'; 5 | 6 | class SearchBar extends React.PureComponent { 7 | componentDidMount() { 8 | requestAnimationFrame(() => { 9 | this._textInput.focus(); 10 | }); 11 | } 12 | 13 | state = { 14 | text: '', 15 | }; 16 | 17 | render() { 18 | let searchInputStyle = {}; 19 | if (this.props.textColor) { 20 | searchInputStyle.color = this.props.textColor; 21 | } 22 | if (this.props.textFontFamily) { 23 | searchInputStyle.fontFamily = this.props.textFontFamily; 24 | } 25 | 26 | return ( 27 | 28 | { 30 | this._textInput = view; 31 | }} 32 | placeholder="Search" 33 | placeholderTextColor={this.props.placeholderTextColor || '#ccc'} 34 | value={this.state.text} 35 | autoCapitalize="none" 36 | autoCorrect={false} 37 | selectionColor={this.props.selectionColor} 38 | underlineColorAndroid={this.props.underlineColorAndroid || '#ccc'} 39 | onSubmitEditing={this._handleSubmit} 40 | onChangeText={this._handleChangeText} 41 | style={[styles.searchInput, searchInputStyle]} 42 | /> 43 | 45 | {this.state.text 46 | ? 51 | 56 | 57 | : null} 58 | 59 | 60 | ); 61 | } 62 | 63 | _handleClear = () => { 64 | this._handleChangeText('') 65 | }; 66 | _handleChangeText = text => { 67 | this.setState({ text }); 68 | this.props.onChangeQuery && this.props.onChangeQuery(text); 69 | }; 70 | 71 | _handleSubmit = () => { 72 | let { text } = this.state; 73 | this.props.onSubmit && this.props.onSubmit(text); 74 | this._textInput.blur(); 75 | }; 76 | } 77 | 78 | export default function (props) { 79 | const navigation = useNavigation(); 80 | 81 | return ( 82 | 83 | ); 84 | } 85 | 86 | const styles = StyleSheet.create({ 87 | container: { 88 | flex: 1, 89 | flexDirection: 'row', 90 | }, 91 | searchInput: { 92 | flex: 1, 93 | fontSize: 18, 94 | marginBottom: 2, 95 | paddingLeft: 5, 96 | marginRight: 5, 97 | }, 98 | }); 99 | -------------------------------------------------------------------------------- /lib/src/SearchLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Platform, StyleSheet, View } from 'react-native'; 3 | import SearchBar from './SearchBar'; 4 | import Header from './Header'; 5 | 6 | const DEFAULT_TINT_COLOR = Platform.OS === 'ios' ? '#007AFF' : '#000'; 7 | 8 | export default class SearchLayout extends React.Component { 9 | static SearchBar = SearchBar; 10 | static Header = Header; 11 | static DefaultTintColor = DEFAULT_TINT_COLOR; 12 | 13 | static defaultProps = { 14 | debounce: 500, 15 | headerBackgroundColor: '#fff', 16 | headerTintColor: DEFAULT_TINT_COLOR, 17 | hideBackButton: false 18 | }; 19 | 20 | state = { 21 | q: '', 22 | }; 23 | 24 | _handleSubmit = q => { 25 | this.props.onSubmit && this.props.onSubmit(q); 26 | }; 27 | 28 | // TODO: debounce 29 | _handleChangeQuery = q => { 30 | this.props.onChangeQuery && this.props.onChangeQuery(q); 31 | this.setState({ q }); 32 | }; 33 | 34 | render() { 35 | return ( 36 | 37 |
41 | 56 |
57 | 58 | {this.props.renderResults 59 | ? this.props.renderResults(this.state.q) 60 | : this.props.children} 61 |
62 | ); 63 | } 64 | } 65 | 66 | const styles = StyleSheet.create({ 67 | container: { 68 | flex: 1, 69 | }, 70 | }); 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "postinstall": "expo-yarn-workspaces check-workspace-dependencies" 5 | }, 6 | "workspaces": [ 7 | "example", 8 | "lib" 9 | ], 10 | "devDependencies": { 11 | "expo-yarn-workspaces": "~1.3.0" 12 | } 13 | } 14 | --------------------------------------------------------------------------------