├── .gitattributes ├── .gitignore ├── .npmignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── examples ├── demo.js ├── demo1.js ├── demo2.js └── demo3.js ├── index.d.ts ├── index.js ├── package.json └── src ├── SelectDropdown.js ├── components ├── DropdownModal.js ├── DropdownOverlay.js ├── DropdownWindow.js └── Input.js ├── helpers ├── deepSearchInArr.js ├── findIndexInArr.js ├── getDropdownHeight.js └── isExist.js └── hooks ├── useKeyboardHeight.js ├── useLayoutDropdown.js ├── useRefs.js └── useSelectDropdown.js /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # OSX 3 | # 4 | .DS_Store 5 | 6 | # node.js 7 | # 8 | node_modules/ 9 | npm-debug.log 10 | yarn-error.log 11 | 12 | 13 | # Xcode 14 | # 15 | build/ 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | xcuserdata 25 | *.xccheckout 26 | *.moved-aside 27 | DerivedData 28 | *.hmap 29 | *.ipa 30 | *.xcuserstate 31 | project.xcworkspace 32 | 33 | 34 | # Android/IntelliJ 35 | # 36 | build/ 37 | .idea 38 | .gradle 39 | local.properties 40 | *.iml 41 | 42 | # BUCK 43 | buck-out/ 44 | \.buckd/ 45 | *.keystore 46 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: false, 3 | jsxBracketSameLine: true, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | arrowParens: 'avoid', 7 | printWidth: 120, 8 | }; 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Adel Reda 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the 4 | Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 5 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 10 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 11 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH 12 | THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-select-dropdown 2 | 3 | react-native-select-dropdown is a highly customized dropdown | select | picker | menu for react native that works for android and iOS platforms. 4 | 5 | ## Installation 6 | 7 | #### # Using npm 8 | 9 | ```bash 10 | npm install react-native-select-dropdown 11 | ``` 12 | 13 | #### # Using yarn 14 | 15 | ```bash 16 | yarn add react-native-select-dropdown 17 | ``` 18 | 19 | ## Demo 20 | 21 | #### Code provided in Examples folder. 22 | 23 |

24 | 25 | 26 | 27 |

28 | 29 | #### Search Functionality (Code provided in Examples folder). 30 | 31 |

32 | 33 |

34 | 35 | ## 🚀 Major Changes 36 | 37 | ### Version 4.0 38 | 39 | - (defaultButtonText, buttonTextAfterSelection, buttonStyle, buttonTextStyle, renderCustomizedButtonChild, renderDropdownIcon, dropdownIconPosition) have been removed and (renderButton) has been added to customize dropdown button 40 | - (rowTextForSelection, rowStyle, rowTextStyle, selectedRowStyle, selectedRowTextStyle, renderCustomizedRowChild) have been removed and (renderItem) has been added to customize each dropdown item 41 | - testID added to scroll the dropdown menu in e2e tests. 42 | - Most of issues have been fixed. 43 | - Updated readme.md file 44 | - More examples in examples folder. 45 | 46 | ## Usage 47 | 48 | ``` 49 | import SelectDropdown from 'react-native-select-dropdown' 50 | import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; 51 | ... 52 | const emojisWithIcons = [ 53 | {title: 'happy', icon: 'emoticon-happy-outline'}, 54 | {title: 'cool', icon: 'emoticon-cool-outline'}, 55 | {title: 'lol', icon: 'emoticon-lol-outline'}, 56 | {title: 'sad', icon: 'emoticon-sad-outline'}, 57 | {title: 'cry', icon: 'emoticon-cry-outline'}, 58 | {title: 'angry', icon: 'emoticon-angry-outline'}, 59 | {title: 'confused', icon: 'emoticon-confused-outline'}, 60 | {title: 'excited', icon: 'emoticon-excited-outline'}, 61 | {title: 'kiss', icon: 'emoticon-kiss-outline'}, 62 | {title: 'devil', icon: 'emoticon-devil-outline'}, 63 | {title: 'dead', icon: 'emoticon-dead-outline'}, 64 | {title: 'wink', icon: 'emoticon-wink-outline'}, 65 | {title: 'sick', icon: 'emoticon-sick-outline'}, 66 | {title: 'frown', icon: 'emoticon-frown-outline'}, 67 | ]; 68 | ... 69 | { 72 | console.log(selectedItem, index); 73 | }} 74 | renderButton={(selectedItem, isOpened) => { 75 | return ( 76 | 77 | {selectedItem && ( 78 | 79 | )} 80 | 81 | {(selectedItem && selectedItem.title) || 'Select your mood'} 82 | 83 | 84 | 85 | ); 86 | }} 87 | renderItem={(item, index, isSelected) => { 88 | return ( 89 | 90 | 91 | {item.title} 92 | 93 | ); 94 | }} 95 | showsVerticalScrollIndicator={false} 96 | dropdownStyle={styles.dropdownMenuStyle} 97 | /> 98 | ... 99 | const styles = StyleSheet.create({ 100 | dropdownButtonStyle: { 101 | width: 200, 102 | height: 50, 103 | backgroundColor: '#E9ECEF', 104 | borderRadius: 12, 105 | flexDirection: 'row', 106 | justifyContent: 'center', 107 | alignItems: 'center', 108 | paddingHorizontal: 12, 109 | }, 110 | dropdownButtonTxtStyle: { 111 | flex: 1, 112 | fontSize: 18, 113 | fontWeight: '500', 114 | color: '#151E26', 115 | }, 116 | dropdownButtonArrowStyle: { 117 | fontSize: 28, 118 | }, 119 | dropdownButtonIconStyle: { 120 | fontSize: 28, 121 | marginRight: 8, 122 | }, 123 | dropdownMenuStyle: { 124 | backgroundColor: '#E9ECEF', 125 | borderRadius: 8, 126 | }, 127 | dropdownItemStyle: { 128 | width: '100%', 129 | flexDirection: 'row', 130 | paddingHorizontal: 12, 131 | justifyContent: 'center', 132 | alignItems: 'center', 133 | paddingVertical: 8, 134 | }, 135 | dropdownItemTxtStyle: { 136 | flex: 1, 137 | fontSize: 18, 138 | fontWeight: '500', 139 | color: '#151E26', 140 | }, 141 | dropdownItemIconStyle: { 142 | fontSize: 28, 143 | marginRight: 8, 144 | }, 145 | }); 146 | ``` 147 | 148 | ### Props 149 | 150 | - [`data`](#data) 151 | 152 | - [`onSelect`](#onSelect) 153 | 154 | - [`renderButton`](#renderButton) 155 | 156 | - [`renderItem`](#renderItem) 157 | 158 | - [`defaultValue`](#defaultValue) 159 | 160 | - [`defaultValueByIndex`](#defaultValueByIndex) 161 | 162 | - [`disabled`](#disabled) 163 | 164 | - [`disabledIndexes`](#disabledIndexes) 165 | 166 | - [`disableAutoScroll`](#disableAutoScroll) 167 | 168 | - [`testID`](#testID) 169 | 170 | - [`onFocus`](#onFocus) 171 | 172 | - [`onBlur`](#onBlur) 173 | 174 | - [`onScrollEndReached`](#onScrollEndReached) 175 | 176 | - [`statusBarTranslucent`](#statusBarTranslucent) 177 | 178 | - [`dropdownStyle`](#dropdownStyle) 179 | 180 | - [`dropdownOverlayColor`](#dropdownOverlayColor) 181 | 182 | - [`showsVerticalScrollIndicator`](#showsVerticalScrollIndicator) 183 | 184 | - [`search`](#search) 185 | 186 | - [`searchInputStyle`](#searchInputStyle) 187 | 188 | - [`searchInputTxtColor`](#searchInputTxtColor) 189 | 190 | - [`searchInputTxtStyle`](#searchInputTxtStyle) 191 | 192 | - [`searchPlaceHolder`](#searchPlaceHolder) 193 | 194 | - [`searchPlaceHolderColor`](#searchPlaceHolderColor) 195 | 196 | - [`renderSearchInputLeftIcon`](#renderSearchInputLeftIcon) 197 | 198 | - [`renderSearchInputRightIcon`](#renderSearchInputRightIcon) 199 | 200 | - [`onChangeSearchInputText`](#onChangeSearchInputText) 201 | 202 | ### Methods 203 | 204 | - [`reset`](#License) 205 | - [`openDropdown`](#License) 206 | - [`closeDropdown`](#License) 207 | - [`selectIndex`](#License) 208 | 209 | --- 210 | 211 | ### data 212 | 213 | array of data that will be represented in dropdown 'can be array of objects 214 | 215 | | Type | Required | 216 | | ----- | -------- | 217 | | array | Yes | 218 | 219 | --- 220 | 221 | ### onSelect 222 | 223 | function recieves selected item and its index in data array 224 | 225 | | Type | Required | 226 | | -------- | -------- | 227 | | function | Yes | 228 | 229 | --- 230 | 231 | ### renderButton 232 | 233 | function returns React component for the dropdown button 234 | 235 | | Type | Required | 236 | | -------- | -------- | 237 | | function | Yes | 238 | 239 | --- 240 | 241 | ### renderItem 242 | 243 | function returns React component for each dropdown item 244 | 245 | | Type | Required | 246 | | -------- | -------- | 247 | | function | Yes | 248 | 249 | --- 250 | 251 | ### defaultValue 252 | 253 | default selected item in dropdown ( check examples in Demo1) 254 | 255 | | Type | Required | 256 | | ---- | -------- | 257 | | any | No | 258 | 259 | --- 260 | 261 | ### defaultValueByIndex 262 | 263 | default selected item index 264 | 265 | | Type | Required | 266 | | ------- | -------- | 267 | | integer | No | 268 | 269 | --- 270 | 271 | ### disabled 272 | 273 | disable dropdown 274 | 275 | | Type | Required | 276 | | ------- | -------- | 277 | | boolean | No | 278 | 279 | --- 280 | 281 | ### disabledIndexes 282 | 283 | array of disabled items index 284 | 285 | | Type | Required | 286 | | ----- | -------- | 287 | | array | No | 288 | 289 | --- 290 | 291 | ### disableAutoScroll 292 | 293 | disable auto scroll to selected value 294 | 295 | | Type | Required | 296 | | ------- | -------- | 297 | | boolean | No | 298 | 299 | --- 300 | 301 | ### testID 302 | 303 | dropdown menu testID 304 | 305 | | Type | Required | 306 | | ------ | -------- | 307 | | string | No | 308 | 309 | --- 310 | 311 | ### onFocus 312 | 313 | function fires when dropdown is opened 314 | 315 | | Type | Required | 316 | | -------- | -------- | 317 | | function | No | 318 | 319 | --- 320 | 321 | ### onBlur 322 | 323 | function fires when dropdown is closed 324 | 325 | | Type | Required | 326 | | -------- | -------- | 327 | | function | No | 328 | 329 | --- 330 | 331 | ### onScrollEndReached 332 | 333 | function fires when dropdown scrolls to the end (for paginations) 334 | 335 | | Type | Required | 336 | | -------- | -------- | 337 | | function | No | 338 | 339 | --- 340 | 341 | ### statusBarTranslucent 342 | 343 | required to set true when statusbar is translucent `(android only)` 344 | 345 | | Type | Required | 346 | | ------- | -------- | 347 | | boolean | No | 348 | 349 | --- 350 | 351 | ### dropdownStyle 352 | 353 | style object for dropdown view 354 | 355 | | Type | Required | 356 | | ------ | -------- | 357 | | object | No | 358 | 359 | --- 360 | 361 | ### dropdownOverlayColor 362 | 363 | backdrop color when dropdown is opened 364 | 365 | | Type | Required | 366 | | ------ | -------- | 367 | | string | No | 368 | 369 | --- 370 | 371 | ### showsVerticalScrollIndicator 372 | 373 | When true, shows a vertical scroll indicator. 374 | 375 | | Type | Required | 376 | | ------- | -------- | 377 | | boolean | No | 378 | 379 | --- 380 | 381 | ### search 382 | 383 | enable search functionality 384 | 385 | | Type | Required | 386 | | ------- | -------- | 387 | | boolean | No | 388 | 389 | --- 390 | 391 | ### searchInputStyle 392 | 393 | style object for search input 394 | 395 | | Type | Required | 396 | | ------ | -------- | 397 | | object | No | 398 | 399 | --- 400 | 401 | ### searchInputTxtColor 402 | 403 | text color for search input 404 | 405 | | Type | Required | 406 | | ------ | -------- | 407 | | string | No | 408 | 409 | --- 410 | 411 | ### searchInputTxtStyle 412 | 413 | style object for search input text 414 | 415 | | Type | Required | 416 | | ------ | -------- | 417 | | object | No | 418 | 419 | --- 420 | 421 | ### searchPlaceHolder 422 | 423 | placeholder text for search input 424 | 425 | | Type | Required | 426 | | ------ | -------- | 427 | | string | No | 428 | 429 | --- 430 | 431 | ### searchPlaceHolderColor 432 | 433 | text color for search input placeholder 434 | 435 | | Type | Required | 436 | | ------ | -------- | 437 | | string | No | 438 | 439 | --- 440 | 441 | ### renderSearchInputLeftIcon 442 | 443 | function returns React component for search input icon 444 | 445 | | Type | Required | 446 | | -------- | -------- | 447 | | function | No | 448 | 449 | --- 450 | 451 | ### renderSearchInputRightIcon 452 | 453 | function returns React component for search input icon 454 | 455 | | Type | Required | 456 | | -------- | -------- | 457 | | function | No | 458 | 459 | --- 460 | 461 | ### onChangeSearchInputText 462 | 463 | function callback when the search input text changes, this will automatically disable the dropdown's internal search to be implemented manually outside the component 464 | 465 | | Type | Required | 466 | | -------- | -------- | 467 | | function | No | 468 | 469 | --- 470 | 471 | | Method | Description | 472 | | -------------------- | -------------------------------- | 473 | | `reset()` | Remove selection & reset it | 474 | | `openDropdown()` | Open the dropdown. | 475 | | `closeDropdown()` | Close the dropdown. | 476 | | `selectIndex(index)` | Select a specific item by index. | 477 | 478 | --- 479 | 480 | ## License 481 | 482 | [MIT](https://choosealicense.com/licenses/mit/) 483 | -------------------------------------------------------------------------------- /examples/demo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, Text, View} from 'react-native'; 3 | import SelectDropdown from 'react-native-select-dropdown'; 4 | import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; 5 | 6 | const Demo = () => { 7 | const emojisWithIcons = [ 8 | {title: 'happy', icon: 'emoticon-happy-outline'}, 9 | {title: 'cool', icon: 'emoticon-cool-outline'}, 10 | {title: 'lol', icon: 'emoticon-lol-outline'}, 11 | {title: 'sad', icon: 'emoticon-sad-outline'}, 12 | {title: 'cry', icon: 'emoticon-cry-outline'}, 13 | {title: 'angry', icon: 'emoticon-angry-outline'}, 14 | {title: 'confused', icon: 'emoticon-confused-outline'}, 15 | {title: 'excited', icon: 'emoticon-excited-outline'}, 16 | {title: 'kiss', icon: 'emoticon-kiss-outline'}, 17 | {title: 'devil', icon: 'emoticon-devil-outline'}, 18 | {title: 'dead', icon: 'emoticon-dead-outline'}, 19 | {title: 'wink', icon: 'emoticon-wink-outline'}, 20 | {title: 'sick', icon: 'emoticon-sick-outline'}, 21 | {title: 'frown', icon: 'emoticon-frown-outline'}, 22 | ]; 23 | 24 | return ( 25 | 26 | 27 | Demo 28 | 29 | { 32 | console.log(selectedItem, index); 33 | }} 34 | renderButton={(selectedItem, isOpen) => { 35 | return ( 36 | 37 | {selectedItem && } 38 | 39 | {(selectedItem && selectedItem.title) || 'Select your mood'} 40 | 41 | 42 | 43 | ); 44 | }} 45 | renderItem={(item, index, isSelected) => { 46 | return ( 47 | 52 | 53 | {item.title} 54 | 55 | ); 56 | }} 57 | showsVerticalScrollIndicator={false} 58 | dropdownStyle={styles.dropdownMenuStyle} 59 | /> 60 | 61 | ); 62 | }; 63 | 64 | export default Demo; 65 | 66 | const styles = StyleSheet.create({ 67 | container: { 68 | flex: 1, 69 | paddingVertical: 100, 70 | alignItems: 'center', 71 | justifyContent: 'space-evenly', 72 | paddingTop: 90, 73 | }, 74 | header: { 75 | position: 'absolute', 76 | top: 0, 77 | width: '100%', 78 | height: 90, 79 | backgroundColor: '#E9ECEF', 80 | justifyContent: 'flex-end', 81 | alignItems: 'center', 82 | paddingBottom: 16, 83 | }, 84 | headerTxt: { 85 | fontSize: 18, 86 | fontWeight: 'bold', 87 | color: '#151E26', 88 | }, 89 | dropdownButtonStyle: { 90 | width: 200, 91 | height: 50, 92 | backgroundColor: '#E9ECEF', 93 | borderRadius: 12, 94 | flexDirection: 'row', 95 | justifyContent: 'center', 96 | alignItems: 'center', 97 | paddingHorizontal: 12, 98 | }, 99 | dropdownButtonTxtStyle: { 100 | flex: 1, 101 | fontSize: 18, 102 | fontWeight: '500', 103 | color: '#151E26', 104 | }, 105 | dropdownButtonArrowStyle: { 106 | fontSize: 28, 107 | }, 108 | dropdownButtonIconStyle: { 109 | fontSize: 28, 110 | marginRight: 8, 111 | }, 112 | dropdownMenuStyle: { 113 | backgroundColor: '#E9ECEF', 114 | borderRadius: 8, 115 | }, 116 | dropdownItemStyle: { 117 | width: '100%', 118 | flexDirection: 'row', 119 | paddingHorizontal: 12, 120 | justifyContent: 'center', 121 | alignItems: 'center', 122 | paddingVertical: 8, 123 | }, 124 | dropdownItemTxtStyle: { 125 | flex: 1, 126 | fontSize: 18, 127 | fontWeight: '500', 128 | color: '#151E26', 129 | }, 130 | dropdownItemIconStyle: { 131 | fontSize: 28, 132 | marginRight: 8, 133 | }, 134 | }); 135 | -------------------------------------------------------------------------------- /examples/demo1.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, Text, View} from 'react-native'; 3 | import SelectDropdown from 'react-native-select-dropdown'; 4 | import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; 5 | 6 | const Demo1 = () => { 7 | const emojis = [ 8 | 'happy', 9 | 'cool', 10 | 'lol', 11 | 'sad', 12 | 'cry', 13 | 'angry', 14 | 'confused', 15 | 'excited', 16 | 'kiss', 17 | 'devil', 18 | 'dead', 19 | 'wink', 20 | 'sick', 21 | 'frown', 22 | ]; 23 | const emojisWithIcons = [ 24 | {title: 'happy', icon: 'emoticon-happy-outline'}, 25 | {title: 'cool', icon: 'emoticon-cool-outline'}, 26 | {title: 'lol', icon: 'emoticon-lol-outline'}, 27 | {title: 'sad', icon: 'emoticon-sad-outline'}, 28 | {title: 'cry', icon: 'emoticon-cry-outline'}, 29 | {title: 'angry', icon: 'emoticon-angry-outline'}, 30 | {title: 'confused', icon: 'emoticon-confused-outline'}, 31 | {title: 'excited', icon: 'emoticon-excited-outline'}, 32 | {title: 'kiss', icon: 'emoticon-kiss-outline'}, 33 | {title: 'devil', icon: 'emoticon-devil-outline'}, 34 | {title: 'dead', icon: 'emoticon-dead-outline'}, 35 | {title: 'wink', icon: 'emoticon-wink-outline'}, 36 | {title: 'sick', icon: 'emoticon-sick-outline'}, 37 | {title: 'frown', icon: 'emoticon-frown-outline'}, 38 | ]; 39 | 40 | return ( 41 | 42 | 43 | Demo 1 44 | 45 | { 48 | console.log(selectedItem, index); 49 | }} 50 | // defaultValueByIndex={8} // use default value by index or default value 51 | // defaultValue={'kiss'} // use default value by index or default value 52 | renderButton={(selectedItem, isOpen) => { 53 | return ( 54 | 55 | {selectedItem || 'Select your mood'} 56 | 57 | ); 58 | }} 59 | renderItem={(item, index, isSelected) => { 60 | return ( 61 | 66 | {item} 67 | 68 | ); 69 | }} 70 | dropdownStyle={styles.dropdownMenuStyle} 71 | /> 72 | 73 | { 76 | console.log(selectedItem, index); 77 | }} 78 | // defaultValueByIndex={8} // use default value by index or default value 79 | // defaultValue={{title: 'kiss', icon: 'emoticon-kiss-outline'}} // use default value by index or default value 80 | renderButton={(selectedItem, isOpen) => { 81 | return ( 82 | 83 | 84 | 85 | {(selectedItem && selectedItem.title) || 'Select your mood'} 86 | 87 | 88 | 89 | ); 90 | }} 91 | renderItem={(item, index, isSelected) => { 92 | return ( 93 | 98 | 99 | {item.title} 100 | 101 | ); 102 | }} 103 | dropdownStyle={styles.dropdown1MenuStyle} 104 | showsVerticalScrollIndicator={false} 105 | /> 106 | 107 | { 110 | console.log(selectedItem, index); 111 | }} 112 | // defaultValueByIndex={8} // use default value by index or default value 113 | // defaultValue={{title: 'kiss', icon: 'emoticon-kiss-outline'}} // use default value by index or default value 114 | renderButton={(selectedItem, isOpen) => { 115 | return ( 116 | 117 | {selectedItem && } 118 | 119 | {(selectedItem && selectedItem.title) || 'Select your mood'} 120 | 121 | 122 | 123 | ); 124 | }} 125 | renderItem={(item, index, isSelected) => { 126 | return ( 127 | 132 | 133 | {item.title} 134 | 135 | ); 136 | }} 137 | dropdownStyle={styles.dropdown2MenuStyle} 138 | showsVerticalScrollIndicator={false} 139 | /> 140 | 141 | ); 142 | }; 143 | 144 | export default Demo1; 145 | 146 | const styles = StyleSheet.create({ 147 | container: { 148 | flex: 1, 149 | paddingVertical: 50, 150 | alignItems: 'center', 151 | justifyContent: 'space-between', 152 | paddingTop: 116, 153 | }, 154 | header: { 155 | position: 'absolute', 156 | top: 0, 157 | width: '100%', 158 | height: 90, 159 | backgroundColor: '#E9ECEF', 160 | justifyContent: 'flex-end', 161 | alignItems: 'center', 162 | paddingBottom: 16, 163 | }, 164 | headerTxt: { 165 | fontSize: 18, 166 | fontWeight: 'bold', 167 | color: '#151E26', 168 | }, 169 | dropdownButtonStyle: { 170 | width: 200, 171 | height: 50, 172 | backgroundColor: '#E9ECEF', 173 | borderRadius: 12, 174 | flexDirection: 'row', 175 | justifyContent: 'center', 176 | alignItems: 'center', 177 | paddingHorizontal: 12, 178 | }, 179 | dropdownButtonTxtStyle: { 180 | flex: 1, 181 | fontSize: 18, 182 | fontWeight: '500', 183 | color: '#151E26', 184 | textAlign: 'center', 185 | }, 186 | dropdownMenuStyle: { 187 | backgroundColor: '#E9ECEF', 188 | borderRadius: 8, 189 | height: 150, 190 | }, 191 | dropdownItemStyle: { 192 | width: '100%', 193 | flexDirection: 'row', 194 | paddingHorizontal: 12, 195 | justifyContent: 'center', 196 | alignItems: 'center', 197 | paddingVertical: 12, 198 | borderBottomWidth: 1, 199 | borderBottomColor: '#B1BDC8', 200 | }, 201 | dropdownItemTxtStyle: { 202 | flex: 1, 203 | fontSize: 18, 204 | fontWeight: '500', 205 | color: '#151E26', 206 | textAlign: 'center', 207 | }, 208 | dropdownItemIconStyle: { 209 | fontSize: 28, 210 | marginRight: 8, 211 | }, 212 | ////////////// dropdown1 213 | dropdown1ButtonStyle: { 214 | width: '80%', 215 | height: 50, 216 | borderRadius: 12, 217 | flexDirection: 'row', 218 | justifyContent: 'center', 219 | alignItems: 'center', 220 | paddingHorizontal: 12, 221 | backgroundColor: '#444444', 222 | }, 223 | dropdown1ButtonTxtStyle: { 224 | flex: 1, 225 | fontSize: 18, 226 | fontWeight: '500', 227 | color: '#FFFFFF', 228 | textAlign: 'center', 229 | }, 230 | dropdown1ButtonArrowStyle: { 231 | fontSize: 28, 232 | color: '#FFFFFF', 233 | }, 234 | dropdown1ButtonIconStyle: { 235 | fontSize: 28, 236 | marginRight: 8, 237 | color: '#FFFFFF', 238 | }, 239 | dropdown1MenuStyle: { 240 | backgroundColor: '#444444', 241 | borderRadius: 8, 242 | }, 243 | dropdown1ItemStyle: { 244 | width: '100%', 245 | flexDirection: 'row', 246 | paddingHorizontal: 12, 247 | justifyContent: 'center', 248 | alignItems: 'center', 249 | paddingVertical: 12, 250 | borderBottomWidth: 1, 251 | borderBottomColor: '#B1BDC8', 252 | }, 253 | dropdown1ItemTxtStyle: { 254 | flex: 1, 255 | fontSize: 18, 256 | fontWeight: '500', 257 | color: '#FFFFFF', 258 | }, 259 | dropdown1ItemIconStyle: { 260 | fontSize: 28, 261 | marginRight: 8, 262 | color: '#FFFFFF', 263 | }, 264 | ////////////// dropdown2 265 | dropdown2ButtonStyle: { 266 | width: '80%', 267 | height: 50, 268 | borderRadius: 12, 269 | flexDirection: 'row', 270 | justifyContent: 'center', 271 | alignItems: 'center', 272 | paddingHorizontal: 12, 273 | borderWidth: 1, 274 | borderColor: '#B1BDC8', 275 | }, 276 | dropdown2ButtonTxtStyle: { 277 | flex: 1, 278 | fontSize: 18, 279 | fontWeight: '500', 280 | color: '#151E26', 281 | }, 282 | dropdown2ButtonArrowStyle: { 283 | fontSize: 28, 284 | }, 285 | dropdown2ButtonIconStyle: { 286 | fontSize: 28, 287 | marginRight: 8, 288 | }, 289 | dropdown2MenuStyle: { 290 | backgroundColor: '#FFF', 291 | borderRadius: 8, 292 | }, 293 | dropdown2ItemStyle: { 294 | width: '100%', 295 | flexDirection: 'row', 296 | paddingHorizontal: 12, 297 | justifyContent: 'center', 298 | alignItems: 'center', 299 | paddingVertical: 12, 300 | borderBottomWidth: 1, 301 | borderBottomColor: '#B1BDC8', 302 | }, 303 | dropdown2ItemTxtStyle: { 304 | flex: 1, 305 | fontSize: 18, 306 | fontWeight: '500', 307 | color: '#151E26', 308 | }, 309 | dropdown2ItemIconStyle: { 310 | fontSize: 28, 311 | marginRight: 8, 312 | }, 313 | }); 314 | -------------------------------------------------------------------------------- /examples/demo2.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useRef, useState} from 'react'; 2 | import {StyleSheet, Text, View} from 'react-native'; 3 | import SelectDropdown from 'react-native-select-dropdown'; 4 | 5 | const Demo2 = () => { 6 | const [feelings, setFeelings] = useState([]); 7 | const [moods, setMoods] = useState([]); 8 | 9 | const moodDropdownRef = useRef(); 10 | 11 | useEffect(() => { 12 | setTimeout(() => { 13 | setFeelings([ 14 | {title: 'positive', moods: [{title: 'Happy'}, {title: 'lol'}]}, 15 | {title: 'negative', moods: [{title: 'Sad'}, {title: 'Angry'}]}, 16 | ]); 17 | }, 1000); 18 | }, []); 19 | 20 | return ( 21 | 22 | 23 | Demo 2 24 | 25 | 26 | { 29 | console.log(selectedItem, index); 30 | moodDropdownRef.current.reset(); 31 | setMoods([]); 32 | setMoods(selectedItem.moods); 33 | // setTimeout(() => { 34 | // moodDropdownRef.current.selectIndex(1); 35 | // }, 250); 36 | }} 37 | renderButton={(selectedItem, isOpen) => { 38 | return ( 39 | 40 | {selectedItem?.title || 'Select a feeling'} 41 | 42 | ); 43 | }} 44 | renderItem={(item, index, isSelected) => { 45 | return ( 46 | 51 | {item.title} 52 | 53 | ); 54 | }} 55 | dropdownStyle={styles.dropdownMenuStyle} 56 | /> 57 | 58 | { 62 | console.log(selectedItem, index); 63 | }} 64 | renderButton={(selectedItem, isOpen) => { 65 | return ( 66 | 67 | {selectedItem?.title || 'Select a mood'} 68 | 69 | ); 70 | }} 71 | renderItem={(item, index, isSelected) => { 72 | return ( 73 | 78 | {item.title} 79 | 80 | ); 81 | }} 82 | dropdownStyle={styles.dropdownMenuStyle} 83 | /> 84 | 85 | 86 | ); 87 | }; 88 | 89 | export default Demo2; 90 | 91 | const styles = StyleSheet.create({ 92 | container: { 93 | flex: 1, 94 | flexDirection: 'row', 95 | alignItems: 'center', 96 | justifyContent: 'space-between', 97 | }, 98 | header: { 99 | position: 'absolute', 100 | top: 0, 101 | width: '100%', 102 | height: 90, 103 | backgroundColor: '#E9ECEF', 104 | justifyContent: 'flex-end', 105 | alignItems: 'center', 106 | paddingBottom: 16, 107 | }, 108 | headerTxt: { 109 | fontSize: 18, 110 | fontWeight: 'bold', 111 | color: '#151E26', 112 | }, 113 | dropdownButtonStyle: { 114 | flex: 1, 115 | height: 50, 116 | backgroundColor: '#E9ECEF', 117 | borderRadius: 12, 118 | flexDirection: 'row', 119 | justifyContent: 'center', 120 | alignItems: 'center', 121 | paddingHorizontal: 12, 122 | }, 123 | dropdownButtonTxtStyle: { 124 | flex: 1, 125 | fontSize: 18, 126 | fontWeight: '500', 127 | color: '#151E26', 128 | textAlign: 'center', 129 | }, 130 | dropdownMenuStyle: { 131 | backgroundColor: '#E9ECEF', 132 | borderRadius: 8, 133 | height: 100, 134 | }, 135 | dropdownItemStyle: { 136 | width: '100%', 137 | height: 50, 138 | flexDirection: 'row', 139 | paddingHorizontal: 12, 140 | justifyContent: 'center', 141 | alignItems: 'center', 142 | borderBottomWidth: 1, 143 | borderBottomColor: '#B1BDC8', 144 | }, 145 | dropdownItemTxtStyle: { 146 | flex: 1, 147 | fontSize: 18, 148 | fontWeight: '500', 149 | color: '#151E26', 150 | textAlign: 'center', 151 | }, 152 | dropdownItemIconStyle: { 153 | fontSize: 28, 154 | marginRight: 8, 155 | }, 156 | }); 157 | -------------------------------------------------------------------------------- /examples/demo3.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {StyleSheet, Text, View} from 'react-native'; 3 | import SelectDropdown from 'react-native-select-dropdown'; 4 | import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; 5 | import FontAwesome from 'react-native-vector-icons/FontAwesome'; 6 | 7 | const Demo3 = () => { 8 | const emojis = [ 9 | 'happy', 10 | 'cool', 11 | 'lol', 12 | 'sad', 13 | 'cry', 14 | 'angry', 15 | 'confused', 16 | 'excited', 17 | 'kiss', 18 | 'devil', 19 | 'dead', 20 | 'wink', 21 | 'sick', 22 | 'frown', 23 | ]; 24 | const emojisWithIcons = [ 25 | {title: 'happy', icon: 'emoticon-happy-outline'}, 26 | {title: 'cool', icon: 'emoticon-cool-outline'}, 27 | {title: 'lol', icon: 'emoticon-lol-outline'}, 28 | {title: 'sad', icon: 'emoticon-sad-outline'}, 29 | {title: 'cry', icon: 'emoticon-cry-outline'}, 30 | {title: 'angry', icon: 'emoticon-angry-outline'}, 31 | {title: 'confused', icon: 'emoticon-confused-outline'}, 32 | {title: 'excited', icon: 'emoticon-excited-outline'}, 33 | {title: 'kiss', icon: 'emoticon-kiss-outline'}, 34 | {title: 'devil', icon: 'emoticon-devil-outline'}, 35 | {title: 'dead', icon: 'emoticon-dead-outline'}, 36 | {title: 'wink', icon: 'emoticon-wink-outline'}, 37 | {title: 'sick', icon: 'emoticon-sick-outline'}, 38 | {title: 'frown', icon: 'emoticon-frown-outline'}, 39 | ]; 40 | 41 | return ( 42 | 43 | 44 | Demo 3 45 | 46 | { 51 | console.log(selectedItem, index); 52 | }} 53 | renderButton={(selectedItem, isOpen) => { 54 | return ( 55 | 56 | {selectedItem || 'Select your mood'} 57 | 58 | ); 59 | }} 60 | renderItem={(item, index, isSelected) => { 61 | return ( 62 | 67 | {item} 68 | 69 | ); 70 | }} 71 | dropdownStyle={styles.dropdownMenuStyle} 72 | search 73 | searchInputStyle={styles.dropdownSearchInputStyle} 74 | searchInputTxtColor={'#151E26'} 75 | searchPlaceHolder={'Search here'} 76 | searchPlaceHolderColor={'#72808D'} 77 | renderSearchInputLeftIcon={() => { 78 | return ; 79 | }} 80 | /> 81 | 82 | { 87 | console.log(selectedItem, index); 88 | }} 89 | renderButton={(selectedItem, isOpen) => { 90 | return ( 91 | 92 | 93 | 94 | {(selectedItem && selectedItem.title) || 'Select your mood'} 95 | 96 | 97 | 98 | ); 99 | }} 100 | renderItem={(item, index, isSelected) => { 101 | return ( 102 | 107 | 108 | {item.title} 109 | 110 | ); 111 | }} 112 | dropdownStyle={styles.dropdown1MenuStyle} 113 | showsVerticalScrollIndicator={false} 114 | search 115 | searchInputStyle={styles.dropdown1SearchInputStyle} 116 | searchInputTxtColor={'#FFFFFF'} 117 | searchPlaceHolder={'Search here'} 118 | searchPlaceHolderColor={'#F8F8F8'} 119 | renderSearchInputLeftIcon={() => { 120 | return ; 121 | }} 122 | /> 123 | 124 | { 129 | console.log(selectedItem, index); 130 | }} 131 | renderButton={(selectedItem, isOpen) => { 132 | return ( 133 | 134 | 135 | 136 | {(selectedItem && selectedItem.title) || 'Select your mood'} 137 | 138 | 139 | 140 | ); 141 | }} 142 | renderItem={(item, index, isSelected) => { 143 | return ( 144 | 149 | 150 | {item.title} 151 | 152 | ); 153 | }} 154 | dropdownStyle={styles.dropdown2MenuStyle} 155 | showsVerticalScrollIndicator={false} 156 | search 157 | searchInputStyle={styles.dropdown2SearchInputStyle} 158 | searchInputTxtColor={'#151E26'} 159 | searchPlaceHolder={'Search here'} 160 | searchPlaceHolderColor={'#72808D'} 161 | renderSearchInputLeftIcon={() => { 162 | return ; 163 | }} 164 | /> 165 | 166 | ); 167 | }; 168 | 169 | export default Demo3; 170 | 171 | const styles = StyleSheet.create({ 172 | container: { 173 | flex: 1, 174 | paddingVertical: 50, 175 | alignItems: 'center', 176 | justifyContent: 'space-between', 177 | paddingTop: 116, 178 | }, 179 | header: { 180 | position: 'absolute', 181 | top: 0, 182 | width: '100%', 183 | height: 90, 184 | backgroundColor: '#E9ECEF', 185 | justifyContent: 'flex-end', 186 | alignItems: 'center', 187 | paddingBottom: 16, 188 | }, 189 | headerTxt: { 190 | fontSize: 18, 191 | fontWeight: 'bold', 192 | color: '#151E26', 193 | }, 194 | dropdownButtonStyle: { 195 | width: 350, 196 | height: 50, 197 | backgroundColor: '#E9ECEF', 198 | borderRadius: 12, 199 | flexDirection: 'row', 200 | justifyContent: 'center', 201 | alignItems: 'center', 202 | paddingHorizontal: 12, 203 | }, 204 | dropdownButtonTxtStyle: { 205 | flex: 1, 206 | fontSize: 18, 207 | fontWeight: '500', 208 | color: '#151E26', 209 | textAlign: 'center', 210 | }, 211 | dropdownMenuStyle: { 212 | backgroundColor: '#E9ECEF', 213 | borderRadius: 8, 214 | }, 215 | dropdownSearchInputStyle: { 216 | backgroundColor: '#E9ECEF', 217 | borderRadius: 8, 218 | borderBottomWidth: 1, 219 | borderBottomColor: '#B1BDC8', 220 | }, 221 | dropdownItemStyle: { 222 | width: '100%', 223 | flexDirection: 'row', 224 | paddingHorizontal: 12, 225 | justifyContent: 'center', 226 | alignItems: 'center', 227 | paddingVertical: 12, 228 | borderBottomWidth: 1, 229 | borderBottomColor: '#B1BDC8', 230 | }, 231 | dropdownItemTxtStyle: { 232 | flex: 1, 233 | fontSize: 18, 234 | fontWeight: '500', 235 | color: '#151E26', 236 | textAlign: 'center', 237 | }, 238 | dropdownItemIconStyle: { 239 | fontSize: 28, 240 | marginRight: 8, 241 | }, 242 | ////////////// dropdown1 243 | dropdown1ButtonStyle: { 244 | width: '80%', 245 | height: 50, 246 | borderRadius: 12, 247 | flexDirection: 'row', 248 | justifyContent: 'center', 249 | alignItems: 'center', 250 | paddingHorizontal: 12, 251 | backgroundColor: '#444444', 252 | }, 253 | dropdown1ButtonTxtStyle: { 254 | flex: 1, 255 | fontSize: 18, 256 | fontWeight: '500', 257 | color: '#FFFFFF', 258 | textAlign: 'center', 259 | }, 260 | dropdown1ButtonArrowStyle: { 261 | fontSize: 28, 262 | color: '#FFFFFF', 263 | }, 264 | dropdown1ButtonIconStyle: { 265 | fontSize: 28, 266 | marginRight: 8, 267 | color: '#FFFFFF', 268 | }, 269 | dropdown1MenuStyle: { 270 | backgroundColor: '#444444', 271 | borderRadius: 8, 272 | }, 273 | dropdown1SearchInputStyle: { 274 | backgroundColor: '#444444', 275 | borderBottomWidth: 1, 276 | borderBottomColor: '#FFFFFF', 277 | }, 278 | dropdown1ItemStyle: { 279 | width: '100%', 280 | flexDirection: 'row', 281 | paddingHorizontal: 12, 282 | justifyContent: 'center', 283 | alignItems: 'center', 284 | paddingVertical: 12, 285 | borderBottomWidth: 1, 286 | borderBottomColor: '#B1BDC8', 287 | }, 288 | dropdown1ItemTxtStyle: { 289 | flex: 1, 290 | fontSize: 18, 291 | fontWeight: '500', 292 | color: '#FFFFFF', 293 | }, 294 | dropdown1ItemIconStyle: { 295 | fontSize: 28, 296 | marginRight: 8, 297 | color: '#FFFFFF', 298 | }, 299 | ////////////// dropdown2 300 | dropdown2ButtonStyle: { 301 | width: '80%', 302 | height: 50, 303 | borderRadius: 12, 304 | flexDirection: 'row', 305 | justifyContent: 'center', 306 | alignItems: 'center', 307 | paddingHorizontal: 12, 308 | borderWidth: 1, 309 | borderColor: '#B1BDC8', 310 | }, 311 | dropdown2ButtonTxtStyle: { 312 | flex: 1, 313 | fontSize: 18, 314 | fontWeight: '500', 315 | color: '#151E26', 316 | }, 317 | dropdown2ButtonArrowStyle: { 318 | fontSize: 28, 319 | }, 320 | dropdown2ButtonIconStyle: { 321 | fontSize: 28, 322 | marginRight: 8, 323 | }, 324 | dropdown2MenuStyle: { 325 | backgroundColor: '#FFF', 326 | borderRadius: 8, 327 | }, 328 | dropdown2SearchInputStyle: { 329 | backgroundColor: '#FFFFFF', 330 | borderRadius: 8, 331 | borderBottomWidth: 1, 332 | borderBottomColor: '#B1BDC8', 333 | }, 334 | dropdown2ItemStyle: { 335 | width: '100%', 336 | flexDirection: 'row', 337 | paddingHorizontal: 12, 338 | justifyContent: 'center', 339 | alignItems: 'center', 340 | paddingVertical: 12, 341 | borderBottomWidth: 1, 342 | borderBottomColor: '#B1BDC8', 343 | }, 344 | dropdown2ItemTxtStyle: { 345 | flex: 1, 346 | fontSize: 18, 347 | fontWeight: '500', 348 | color: '#151E26', 349 | }, 350 | dropdown2ItemIconStyle: { 351 | fontSize: 28, 352 | marginRight: 8, 353 | }, 354 | }); 355 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import type * as React from 'react'; 2 | import {StyleProp, ViewStyle, TextStyle} from 'react-native'; 3 | 4 | declare module 'react-native-select-dropdown' { 5 | export type SelectDropdownProps = { 6 | /** 7 | * array of data that will be represented in dropdown, can be array of objects 8 | */ 9 | data: Array; 10 | /** 11 | * function recieves selected item and its index in data array 12 | */ 13 | onSelect: (selectedItem: any, index: number) => void; 14 | /** 15 | * function returns React component for the dropdown button 16 | */ 17 | renderButton: (selectedItem: any, isOpened: boolean) => React.ReactNode; 18 | /** 19 | * function returns React component for each dropdown item 20 | */ 21 | renderItem: (selectedItem: any, index: number, isSelected: boolean) => React.ReactNode; 22 | /** 23 | * default selected item in dropdown 24 | */ 25 | defaultValue?: any; 26 | /** 27 | * default selected item index 28 | */ 29 | defaultValueByIndex?: number; 30 | /** 31 | * disable dropdown 32 | */ 33 | disabled?: boolean; 34 | /** 35 | * array of disabled items index 36 | */ 37 | disabledIndexes?: number[]; 38 | /** 39 | * disable auto scroll to selected value 40 | */ 41 | disableAutoScroll?: boolean; 42 | /** 43 | * dropdown menu testID 44 | */ 45 | testID?: string; 46 | /** 47 | * function fires when dropdown is opened 48 | */ 49 | onFocus?: () => void; 50 | /** 51 | * function fires when dropdown is closed 52 | */ 53 | onBlur?: () => void; 54 | /** 55 | * function fires when dropdown reaches the end 56 | */ 57 | onScrollEndReached?: () => void; 58 | /** 59 | * required to set true when statusbar is translucent (android only) 60 | */ 61 | statusBarTranslucent?: boolean; 62 | /** 63 | * style object for dropdown view 64 | */ 65 | dropdownStyle?: StyleProp; 66 | /** 67 | * backdrop color when dropdown is opened 68 | */ 69 | dropdownOverlayColor?: string; 70 | /** 71 | * When true, shows a vertical scroll indicator in the dropdown. 72 | */ 73 | showsVerticalScrollIndicator?: boolean; 74 | /** 75 | * enable search functionality 76 | */ 77 | search?: boolean; 78 | /** 79 | * style object for search input 80 | */ 81 | searchInputStyle?: StyleProp; 82 | /** 83 | * text color for search input 84 | */ 85 | searchInputTxtColor?: string; 86 | /** 87 | * text style for search input 88 | */ 89 | searchInputTxtStyle?: StyleProp; 90 | /** 91 | * placeholder text for search input 92 | */ 93 | searchPlaceHolder?: string; 94 | /** 95 | * text color for search input placeholder 96 | */ 97 | searchPlaceHolderColor?: string; 98 | /** 99 | * function callback when the search input text changes, this will automatically disable the dropdown's internal search to be implemented manually outside the component 100 | */ 101 | onChangeSearchInputText?: (searchText: string) => void; 102 | /** 103 | * function returns React component for search input icon 104 | */ 105 | renderSearchInputLeftIcon?: (selectedItem: any, index: number) => React.ReactNode; 106 | /** 107 | * function returns React component for search input icon 108 | */ 109 | renderSearchInputRightIcon?: (selectedItem: any, index: number) => React.ReactNode; 110 | }; 111 | 112 | export default class SelectDropdown extends React.Component { 113 | /** 114 | * Remove selection & reset it 115 | */ 116 | reset(): void; 117 | /** 118 | * Open the dropdown. 119 | */ 120 | openDropdown(): void; 121 | /** 122 | * Close the dropdown. 123 | */ 124 | closeDropdown(): void; 125 | /** 126 | * Select index. 127 | */ 128 | selectIndex(index: number): void; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import SelectDropdown from './src/SelectDropdown'; 2 | 3 | export default SelectDropdown; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-select-dropdown", 3 | "version": "4.0.1", 4 | "description": "react-native-select-dropdown is a highly customized dropdown | select | picker | menu for react native that works for andriod and iOS platforms.", 5 | "main": "index.js", 6 | "typings": "index.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [ 11 | "react-native", 12 | "android", 13 | "ios", 14 | "dropdown", 15 | "select", 16 | "option", 17 | "selector", 18 | "picker", 19 | "menu", 20 | "modal" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/AdelRedaa97/react-native-select-dropdown" 25 | }, 26 | "author": "Adel Reda (AdelRedaa97)", 27 | "license": "MIT" 28 | } -------------------------------------------------------------------------------- /src/SelectDropdown.js: -------------------------------------------------------------------------------- 1 | import React, {forwardRef, useImperativeHandle, useCallback} from 'react'; 2 | import {View, TouchableOpacity, FlatList} from 'react-native'; 3 | import {isExist} from './helpers/isExist'; 4 | import Input from './components/Input'; 5 | import DropdownOverlay from './components/DropdownOverlay'; 6 | import DropdownModal from './components/DropdownModal'; 7 | import DropdownWindow from './components/DropdownWindow'; 8 | import {useSelectDropdown} from './hooks/useSelectDropdown'; 9 | import {useLayoutDropdown} from './hooks/useLayoutDropdown'; 10 | import {useRefs} from './hooks/useRefs'; 11 | import {findIndexInArr} from './helpers/findIndexInArr'; 12 | 13 | const SelectDropdown = ( 14 | { 15 | data /* array */, 16 | onSelect /* function */, 17 | renderButton /* function returns React component for the dropdown button */, 18 | renderItem /* function returns React component for each dropdown Item */, 19 | defaultValue /* any */, 20 | defaultValueByIndex /* integer */, 21 | disabled /* boolean */, 22 | disabledIndexes /* array of disabled items index */, 23 | disableAutoScroll /* boolean */, 24 | testID /* dropdown menu testID */, 25 | onFocus /* function */, 26 | onBlur /* function */, 27 | onScrollEndReached /* function */, 28 | ///////////////////////////// 29 | statusBarTranslucent /* boolean */, 30 | dropdownStyle /* style object for search input */, 31 | dropdownOverlayColor /* string */, 32 | showsVerticalScrollIndicator /* boolean */, 33 | ///////////////////////////// 34 | search /* boolean */, 35 | searchInputStyle /* style object for search input */, 36 | searchInputTxtColor /* text color for search input */, 37 | searchInputTxtStyle /* text style for search input */, 38 | searchPlaceHolder /* placeholder text for search input */, 39 | searchPlaceHolderColor /* text color for search input placeholder */, 40 | renderSearchInputLeftIcon /* function returns React component for search input icon */, 41 | renderSearchInputRightIcon /* function returns React component for search input icon */, 42 | onChangeSearchInputText /* function callback when the search input text changes, this will automatically disable the dropdown's interna search to be implemented manually outside the component */, 43 | }, 44 | ref, 45 | ) => { 46 | const disabledInternalSearch = !!onChangeSearchInputText; 47 | /* ******************* hooks ******************* */ 48 | const {dropdownButtonRef, dropDownFlatlistRef} = useRefs(); 49 | const { 50 | dataArr, // 51 | selectedItem, 52 | selectItem, 53 | reset, 54 | searchTxt, 55 | setSearchTxt, 56 | } = useSelectDropdown(data, defaultValueByIndex, defaultValue, disabledInternalSearch); 57 | const { 58 | isVisible, // 59 | setIsVisible, 60 | buttonLayout, 61 | onDropdownButtonLayout, 62 | dropdownWindowStyle, 63 | onRequestClose, 64 | } = useLayoutDropdown(data, dropdownStyle); 65 | useImperativeHandle(ref, () => ({ 66 | reset: () => { 67 | reset(); 68 | }, 69 | openDropdown: () => { 70 | openDropdown(); 71 | }, 72 | closeDropdown: () => { 73 | closeDropdown(); 74 | }, 75 | selectIndex: index => { 76 | selectItem(index); 77 | }, 78 | })); 79 | /* ******************* Methods ******************* */ 80 | const openDropdown = () => { 81 | dropdownButtonRef.current.measure((fx, fy, w, h, px, py) => { 82 | onDropdownButtonLayout(w, h, px, py); 83 | setIsVisible(true); 84 | onFocus && onFocus(); 85 | scrollToSelectedItem(); 86 | }); 87 | }; 88 | const closeDropdown = () => { 89 | setIsVisible(false); 90 | setSearchTxt(''); 91 | onBlur && onBlur(); 92 | }; 93 | const scrollToSelectedItem = () => { 94 | const indexInCurrArr = findIndexInArr(selectedItem, dataArr); 95 | setTimeout(() => { 96 | if (disableAutoScroll) { 97 | return; 98 | } 99 | if (indexInCurrArr > 1) { 100 | dropDownFlatlistRef?.current?.scrollToIndex({ 101 | index: search ? indexInCurrArr - 1 : indexInCurrArr, 102 | animated: true, 103 | }); 104 | } 105 | }, 200); 106 | }; 107 | const onSelectItem = (item, index) => { 108 | const indexInOriginalArr = findIndexInArr(item, data); 109 | closeDropdown(); 110 | onSelect && onSelect(item, indexInOriginalArr); 111 | selectItem(indexInOriginalArr); 112 | }; 113 | const onScrollToIndexFailed = error => { 114 | dropDownFlatlistRef.current.scrollToOffset({ 115 | offset: error.averageItemLength * error.index, 116 | animated: true, 117 | }); 118 | setTimeout(() => { 119 | if (dataArr.length !== 0 && dropDownFlatlistRef) { 120 | dropDownFlatlistRef.current.scrollToIndex({index: error.index, animated: true}); 121 | } 122 | }, 100); 123 | }; 124 | /* ******************** Render Methods ******************** */ 125 | const renderSearchView = () => { 126 | return ( 127 | search && ( 128 | { 135 | setSearchTxt(txt); 136 | disabledInternalSearch && onChangeSearchInputText(txt); 137 | }} 138 | inputStyle={searchInputStyle} 139 | inputTextStyle={searchInputTxtStyle} 140 | renderLeft={renderSearchInputLeftIcon} 141 | renderRight={renderSearchInputRightIcon} 142 | /> 143 | ) 144 | ); 145 | }; 146 | const renderFlatlistItem = ({item, index}) => { 147 | const indexInCurrArr = findIndexInArr(selectedItem, dataArr); 148 | const isSelected = index == indexInCurrArr; 149 | 150 | let clonedElement = renderItem ? renderItem(item, index, isSelected) : ; 151 | let props = {...clonedElement.props}; 152 | return ( 153 | isExist(item) && ( 154 | onSelectItem(item, index)}> 159 | {props?.children} 160 | 161 | ) 162 | ); 163 | }; 164 | const renderDropdown = () => { 165 | return ( 166 | isVisible && ( 167 | 168 | 169 | 170 | index.toString()} 174 | ref={dropDownFlatlistRef} 175 | renderItem={renderFlatlistItem} 176 | ListHeaderComponent={renderSearchView()} 177 | stickyHeaderIndices={search && [0]} 178 | keyboardShouldPersistTaps="always" 179 | onEndReached={() => onScrollEndReached && onScrollEndReached()} 180 | onEndReachedThreshold={0.5} 181 | showsVerticalScrollIndicator={showsVerticalScrollIndicator} 182 | onScrollToIndexFailed={onScrollToIndexFailed} 183 | /> 184 | 185 | 186 | ) 187 | ); 188 | }; 189 | /////////////////////////////////////////////////////// 190 | let clonedElement = renderButton ? renderButton(selectedItem, isVisible) : ; 191 | let props = {...clonedElement.props}; 192 | return ( 193 | 194 | {renderDropdown()} 195 | {props?.children} 196 | 197 | ); 198 | }; 199 | 200 | export default forwardRef((props, ref) => SelectDropdown(props, ref)); 201 | -------------------------------------------------------------------------------- /src/components/DropdownModal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Modal} from 'react-native'; 3 | 4 | const DropdownModal = ({visible, statusBarTranslucent, onRequestClose, children}) => { 5 | const defaults = { 6 | statusBarTranslucent: statusBarTranslucent || false, 7 | }; 8 | return ( 9 | 16 | {children} 17 | 18 | ); 19 | }; 20 | 21 | export default DropdownModal; 22 | -------------------------------------------------------------------------------- /src/components/DropdownOverlay.js: -------------------------------------------------------------------------------- 1 | import {StyleSheet, TouchableOpacity} from 'react-native'; 2 | import React from 'react'; 3 | 4 | const DropdownOverlay = ({onPress, backgroundColor}) => { 5 | const defaults = { 6 | backgroundColor: backgroundColor || 'rgba(0,0,0,0.4)', 7 | }; 8 | return ( 9 | 19 | ); 20 | }; 21 | 22 | export default DropdownOverlay; 23 | 24 | const styles = StyleSheet.create({ 25 | dropdownOverlay: { 26 | width: '100%', 27 | height: '100%', 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /src/components/DropdownWindow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, StyleSheet} from 'react-native'; 3 | 4 | const DropdownWindow = ({layoutStyle, children}) => { 5 | return {children}; 6 | }; 7 | 8 | export default DropdownWindow; 9 | 10 | const styles = StyleSheet.create({ 11 | dropdownOverlayView: { 12 | backgroundColor: '#EFEFEF', 13 | }, 14 | shadow: { 15 | shadowColor: '#000', 16 | shadowOffset: {width: 0, height: 6}, 17 | shadowOpacity: 0.1, 18 | shadowRadius: 10, 19 | elevation: 10, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/Input.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {forwardRef} from 'react'; 3 | import {View, TextInput, StyleSheet, I18nManager} from 'react-native'; 4 | 5 | const voidFunction = () => {}; 6 | 7 | const Input = ( 8 | { 9 | searchViewWidth, 10 | inputStyle, 11 | inputTextStyle, 12 | value, 13 | valueColor, 14 | placeholder, 15 | placeholderTextColor, 16 | textAlign, 17 | onChangeText, 18 | onEndEditing, 19 | onSubmitEditing, 20 | renderLeft, 21 | renderRight, 22 | testID, 23 | }, 24 | ref, 25 | ) => { 26 | const defaults = { 27 | inputStyle: inputStyle, 28 | inputTextStyle: inputTextStyle, 29 | value: value ?? '', 30 | valueColor: valueColor ?? '#000000', 31 | placeholder: placeholder ?? '', 32 | placeholderTextColor: placeholderTextColor ?? '#CACACA', 33 | textAlign: textAlign || (I18nManager.isRTL ? 'right' : 'left'), 34 | onChangeText: onChangeText ?? voidFunction, 35 | onEndEditing: onEndEditing ?? voidFunction, 36 | onSubmitEditing: onSubmitEditing ?? voidFunction, 37 | renderLeft: renderLeft, 38 | renderRight: renderRight, 39 | testID: testID, 40 | }; 41 | 42 | const onChangeTextValidator = txt => { 43 | if (txt.length == 1 && txt == ' ') { 44 | return; 45 | } 46 | if (txt.length > 1 && txt.slice(-2) == ' ') { 47 | return; 48 | } 49 | defaults.onChangeText(txt); 50 | }; 51 | 52 | return ( 53 | 54 | 59 | {defaults.renderLeft && {defaults.renderLeft()}} 60 | 80 | {defaults.renderRight && {defaults.renderRight()}} 81 | 82 | 83 | ); 84 | }; 85 | 86 | export default forwardRef((props, ref) => Input(props, ref)); 87 | 88 | const styles = StyleSheet.create({ 89 | searchViewStyle: { 90 | height: 50, 91 | paddingHorizontal: 0, 92 | }, 93 | defaultInputStyle: { 94 | width: '100%', 95 | height: '100%', 96 | backgroundColor: '#FFFFFF', 97 | flexDirection: 'row', 98 | justifyContent: 'center', 99 | paddingHorizontal: '4%', 100 | }, 101 | inputField: { 102 | flex: 1, 103 | height: '100%', 104 | backgroundColor: '#0000', 105 | textAlignVertical: 'center', 106 | paddingVertical: 0, 107 | }, 108 | pressableLeft: { 109 | height: '100%', 110 | marginRight: '4%', 111 | justifyContent: 'center', 112 | }, 113 | pressableRight: { 114 | height: '100%', 115 | marginLeft: '4%', 116 | justifyContent: 'center', 117 | }, 118 | }); 119 | -------------------------------------------------------------------------------- /src/helpers/deepSearchInArr.js: -------------------------------------------------------------------------------- 1 | const contains = (item, searchTxt) => { 2 | // item is an object 3 | if (typeof item == 'object' && item != null) { 4 | for (let key in item) { 5 | const value = item[key]; 6 | if (contains(value, searchTxt)) { 7 | return true; 8 | } 9 | } 10 | } 11 | // string, number or boolean 12 | if (typeof item != 'object' && item != null && item != undefined) { 13 | const itemStringfied = item.toString().toLowerCase(); 14 | const searchTxtStringfied = searchTxt.toString().toLowerCase(); 15 | if (itemStringfied.includes(searchTxtStringfied)) { 16 | return true; 17 | } 18 | } 19 | return false; 20 | }; 21 | 22 | export const deepSearchInArr = (query, arr) => { 23 | let array = []; 24 | for (let i = 0; i <= arr.length - 1; i++) { 25 | if (contains(arr[i], query)) { 26 | array.push(arr[i]); 27 | } 28 | if (i == arr.length - 1) { 29 | return array; 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/helpers/findIndexInArr.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | export const findIndexInArr = (obj, arr) => { 4 | var defaultValueIndex = -1; 5 | if (typeof obj == 'object') { 6 | for (let index = 0; index < arr.length; index++) { 7 | const element = arr[index]; 8 | if (_.isEqual(element, obj)) { 9 | defaultValueIndex = index; 10 | } 11 | if (index == arr.length - 1) { 12 | return defaultValueIndex; 13 | } 14 | } 15 | } else { 16 | for (let index = 0; index < arr.length; index++) { 17 | const element = arr[index]; 18 | if (element == obj) { 19 | defaultValueIndex = index; 20 | } 21 | if (index == arr.length - 1) { 22 | if (index == arr.length - 1) { 23 | return defaultValueIndex; 24 | } 25 | } 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/helpers/getDropdownHeight.js: -------------------------------------------------------------------------------- 1 | export const getDropdownHeight = (dropdownStyle, dataLength) => { 2 | if (dropdownStyle && dropdownStyle.height) { 3 | return dropdownStyle.height; 4 | } else { 5 | if (dataLength == 0) { 6 | return 150; 7 | } 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/helpers/isExist.js: -------------------------------------------------------------------------------- 1 | export const isExist = value => { 2 | if (value != undefined && value != null) { 3 | return true; 4 | } 5 | return false; 6 | }; 7 | -------------------------------------------------------------------------------- /src/hooks/useKeyboardHeight.js: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from 'react'; 2 | import {Keyboard} from 'react-native'; 3 | 4 | export const useKeyboardHeight = () => { 5 | const [keyboardHeight, setKeyboardHeight] = useState(0); 6 | 7 | useEffect(() => { 8 | const showSubscription = Keyboard.addListener('keyboardDidShow', e => { 9 | setKeyboardHeight(e.endCoordinates.height); 10 | }); 11 | const hideSubscription = Keyboard.addListener('keyboardDidHide', () => { 12 | setKeyboardHeight(0); 13 | }); 14 | return () => { 15 | showSubscription.remove(); 16 | hideSubscription.remove(); 17 | }; 18 | }, [setKeyboardHeight]); 19 | 20 | return {keyboardHeight}; 21 | }; 22 | -------------------------------------------------------------------------------- /src/hooks/useLayoutDropdown.js: -------------------------------------------------------------------------------- 1 | import {useEffect, useState, useMemo} from 'react'; 2 | import {I18nManager, Dimensions} from 'react-native'; 3 | import {getDropdownHeight} from '../helpers/getDropdownHeight'; 4 | import {useKeyboardHeight} from './useKeyboardHeight'; 5 | const {height} = Dimensions.get('window'); 6 | const DROPDOWN_MAX_HEIGHT = height * 0.4; 7 | 8 | export const useLayoutDropdown = (data, dropdownStyle) => { 9 | const [isVisible, setIsVisible] = useState(false); // dropdown visible ? 10 | const [buttonLayout, setButtonLayout] = useState(null); 11 | const [dropdownCalculatedStyle, setDropdownCalculatedStyle] = useState({}); 12 | 13 | const [dropdownHEIGHT, setDropdownHEIGHT] = useState(() => { 14 | return getDropdownHeight(dropdownStyle, data?.length || 0); 15 | }); // dropdown height 16 | 17 | const {keyboardHeight} = useKeyboardHeight(); 18 | 19 | useEffect(() => { 20 | setDropdownHEIGHT(getDropdownHeight(dropdownStyle, data?.length || 0)); 21 | }, [JSON.stringify(dropdownStyle), JSON.stringify(data)]); 22 | 23 | const onDropdownButtonLayout = (w, h, px, py) => { 24 | setButtonLayout({w, h, px, py}); 25 | 26 | const remainingHeight = dropdownStyle?.height || height / 4; 27 | 28 | if (py + h > height - remainingHeight) { 29 | return setDropdownCalculatedStyle({ 30 | bottom: height - (py + h) + h, 31 | width: dropdownStyle?.width || w, 32 | ...(I18nManager.isRTL ? {right: dropdownStyle?.right || px} : {left: dropdownStyle?.left || px}), 33 | }); 34 | } 35 | 36 | return setDropdownCalculatedStyle({ 37 | top: py + h + 2, 38 | width: dropdownStyle?.width || w, 39 | ...(I18nManager.isRTL ? {right: dropdownStyle?.right || px} : {left: dropdownStyle?.left || px}), 40 | }); 41 | }; 42 | 43 | const dropdownWindowStyle = useMemo(() => { 44 | // minimum dropdownheight to show while keyboard is opened 45 | const minDropdownHeight = 200; 46 | const getPositionIfKeyboardIsOpened = () => { 47 | if (keyboardHeight) { 48 | if (dropdownCalculatedStyle.top && height - dropdownCalculatedStyle.top < keyboardHeight + minDropdownHeight) { 49 | return {top: height - (keyboardHeight + minDropdownHeight), minHeight: minDropdownHeight}; 50 | } 51 | if (dropdownCalculatedStyle.bottom && dropdownCalculatedStyle.bottom < keyboardHeight - minDropdownHeight) { 52 | return {top: height - (keyboardHeight + minDropdownHeight), bottom: undefined, minHeight: minDropdownHeight}; 53 | } 54 | return {minHeight: minDropdownHeight}; 55 | } 56 | return {}; 57 | }; 58 | 59 | return { 60 | ...{ 61 | borderTopWidth: 0, 62 | overflow: 'hidden', 63 | }, 64 | ...dropdownStyle, 65 | ...dropdownCalculatedStyle, 66 | ...{ 67 | position: 'absolute', 68 | height: dropdownHEIGHT, 69 | maxHeight: DROPDOWN_MAX_HEIGHT, 70 | }, 71 | ...getPositionIfKeyboardIsOpened(), 72 | }; 73 | }, [JSON.stringify(dropdownStyle), JSON.stringify(dropdownCalculatedStyle), keyboardHeight, dropdownHEIGHT]); 74 | 75 | const onRequestClose = () => { 76 | setIsVisible(false); 77 | }; 78 | 79 | return { 80 | isVisible, 81 | setIsVisible, 82 | buttonLayout, 83 | onDropdownButtonLayout, 84 | dropdownWindowStyle, 85 | onRequestClose, 86 | }; 87 | }; 88 | -------------------------------------------------------------------------------- /src/hooks/useRefs.js: -------------------------------------------------------------------------------- 1 | import {useRef} from 'react'; 2 | 3 | export const useRefs = () => { 4 | const dropdownButtonRef = useRef(); // button ref to get positions 5 | const dropDownFlatlistRef = useRef(null); // ref to the drop down flatlist 6 | 7 | return { 8 | dropdownButtonRef, 9 | dropDownFlatlistRef, 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/hooks/useSelectDropdown.js: -------------------------------------------------------------------------------- 1 | import {useState, useEffect, useMemo} from 'react'; 2 | import {deepSearchInArr} from '../helpers/deepSearchInArr'; 3 | import {findIndexInArr} from '../helpers/findIndexInArr'; 4 | import {isExist} from '../helpers/isExist'; 5 | 6 | export const useSelectDropdown = (data, defaultValueByIndex, defaultValue, disabledInternalSearch) => { 7 | const [selectedItem, setSelectedItem] = useState(null); // selected item from dropdown 8 | const [selectedIndex, setSelectedIndex] = useState(-1); // index of selected item from dropdown 9 | const [searchTxt, setSearchTxt] = useState(''); 10 | 11 | // data array changes 12 | useEffect(() => { 13 | if (!data || data.length == 0) { 14 | reset(); 15 | } 16 | }, [JSON.stringify(data)]); 17 | 18 | // default value by index added or changed 19 | useEffect(() => { 20 | // defaultValueByIndex may be equals zero 21 | if (isExist(defaultValueByIndex)) { 22 | if (data && isExist(data[defaultValueByIndex])) { 23 | selectItem(defaultValueByIndex); 24 | } 25 | } 26 | }, [JSON.stringify(defaultValueByIndex)]); 27 | // default value added or changed 28 | useEffect(() => { 29 | // defaultValue may be equals zero 30 | if (isExist(defaultValue)) { 31 | if (data && findIndexInArr(defaultValue, data) >= 0) { 32 | selectItem(findIndexInArr(defaultValue, data)); 33 | } 34 | } 35 | }, [JSON.stringify(defaultValue)]); 36 | 37 | const dataArr = useMemo(() => { 38 | if (disabledInternalSearch) { 39 | return data; 40 | } 41 | return searchTxt ? deepSearchInArr(searchTxt, data) : data; 42 | }, [JSON.stringify(data), searchTxt]); 43 | 44 | const selectItem = index => { 45 | setSelectedItem(data[index]); 46 | setSelectedIndex(index); 47 | }; 48 | 49 | const reset = () => { 50 | setSelectedItem(null); 51 | setSelectedIndex(-1); 52 | }; 53 | 54 | return { 55 | dataArr, 56 | selectedItem, 57 | selectedIndex, 58 | selectItem, 59 | reset, 60 | searchTxt, 61 | setSearchTxt, 62 | }; 63 | }; 64 | --------------------------------------------------------------------------------