├── .babelrc ├── gif ├── scroll.gif ├── toggle.gif ├── example1.gif └── example2.gif ├── LICENSE.md ├── package.json ├── .gitignore ├── index.js └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["module:metro-react-native-babel-preset"] 3 | } -------------------------------------------------------------------------------- /gif/scroll.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoMartella/react-native-simple-bottom-sheet/HEAD/gif/scroll.gif -------------------------------------------------------------------------------- /gif/toggle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoMartella/react-native-simple-bottom-sheet/HEAD/gif/toggle.gif -------------------------------------------------------------------------------- /gif/example1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoMartella/react-native-simple-bottom-sheet/HEAD/gif/example1.gif -------------------------------------------------------------------------------- /gif/example2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StefanoMartella/react-native-simple-bottom-sheet/HEAD/gif/example2.gif -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2020 Google, Inc. http://angularjs.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-simple-bottom-sheet", 3 | "version": "1.0.3", 4 | "description": "A simple react native bottom sheet component", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/StefanoMartella/react-native-simple-bottom-sheet.git" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "react-native", 16 | "react-native-component", 17 | "react native", 18 | "slider", 19 | "panel", 20 | "bottom-sheet", 21 | "sheet", 22 | "simple", 23 | "slider-panel", 24 | "bottom", 25 | "bottom-slider", 26 | "ios", 27 | "android", 28 | "mobile" 29 | ], 30 | "author": "Stefano Martella ", 31 | "license": "ISC", 32 | "bugs": { 33 | "url": "https://github.com/StefanoMartella/react-native-simple-bottom-sheet/issues" 34 | }, 35 | "homepage": "https://github.com/StefanoMartella/react-native-simple-bottom-sheet#readme", 36 | "dependencies": { 37 | "prop-types": "^15.7.2" 38 | }, 39 | "devDependencies": { 40 | "metro-react-native-babel-preset": "^0.59.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | .parcel-cache 79 | 80 | # Next.js build output 81 | .next 82 | out 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and not Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | # Stores VSCode versions used for testing VSCode extensions 110 | .vscode-test 111 | 112 | # yarn v2 113 | 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .pnp.* 118 | 119 | package-lock.json 120 | 121 | testcomponent/ 122 | .idea 123 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { 3 | Animated, 4 | BackHandler, 5 | Dimensions, 6 | Easing, 7 | Keyboard, 8 | PanResponder, 9 | TouchableOpacity, 10 | View, 11 | } from 'react-native'; 12 | import PropTypes from 'prop-types'; 13 | 14 | class BottomSheet extends Component { 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | isPanelVisible: props.isOpen, 19 | isPanelOpened: props.isOpen, 20 | contentHeight: undefined, 21 | }; 22 | this.panelHeightValue = new Animated.Value(0); 23 | this._setPanResponders(); 24 | } 25 | 26 | togglePanel = () => { 27 | const {contentHeight, isPanelOpened} = this.state; 28 | const { 29 | animationDuration, 30 | animation, 31 | sliderMinHeight, 32 | onOpen, 33 | onClose, 34 | } = this.props; 35 | 36 | Animated.timing(this.panelHeightValue, { 37 | duration: animationDuration, 38 | easing: animation, 39 | toValue: this.panelHeightValue._value === 0 40 | ? contentHeight - sliderMinHeight 41 | : 0, 42 | useNativeDriver: false, 43 | }).start(() => { 44 | this.setState({isPanelOpened: !isPanelOpened}, () => { 45 | if (this.state.isPanelOpened) { 46 | onOpen(); 47 | } else { 48 | onClose(); 49 | Keyboard.dismiss(); 50 | } 51 | }); 52 | }); 53 | }; 54 | 55 | _onBackPress = () => { 56 | this.state.isPanelOpened && this.togglePanel(); 57 | return this.state.isPanelOpened; 58 | }; 59 | 60 | componentDidMount() { 61 | BackHandler.addEventListener('hardwareBackPress', this._onBackPress); 62 | } 63 | 64 | componentWillUnmount() { 65 | BackHandler.removeEventListener('hardwareBackPress', this._onBackPress); 66 | } 67 | 68 | _setPanResponders() { 69 | this._parentPanResponder = PanResponder.create({ 70 | onMoveShouldSetPanResponderCapture: () => !this.state.isPanelOpened, 71 | onPanResponderRelease: () => this.togglePanel(), 72 | }); 73 | 74 | this._childPanResponder = PanResponder.create({ 75 | onMoveShouldSetPanResponderCapture: (_, gestureState) => 76 | gestureState.dy > 15, 77 | onPanResponderRelease: (_, gestureState) => 78 | gestureState.dy > 0 && this.togglePanel(), 79 | }); 80 | } 81 | 82 | _handleScrollEndDrag = ({nativeEvent}) => { 83 | nativeEvent.contentOffset.y === 0 && this.togglePanel(); 84 | }; 85 | 86 | _setSize = ({nativeEvent}) => { 87 | this.setState({contentHeight: nativeEvent.layout.height}, () => { 88 | const {isOpen, sliderMinHeight} = this.props; 89 | const {contentHeight} = this.state; 90 | if (!isOpen && contentHeight) { 91 | this.panelHeightValue.setValue(contentHeight - sliderMinHeight); 92 | this.setState({isPanelVisible: true}); 93 | } 94 | }); 95 | }; 96 | 97 | render() { 98 | const {isPanelVisible} = this.state; 99 | const { 100 | sliderMaxHeight, 101 | wrapperStyle, 102 | outerContentStyle, 103 | innerContentStyle, 104 | lineContainerStyle, 105 | lineStyle, 106 | children, 107 | } = this.props; 108 | 109 | return ( 110 | 122 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | {typeof children === 'function' 132 | ? children(this._handleScrollEndDrag) 133 | : children} 134 | 135 | 136 | 137 | ); 138 | } 139 | } 140 | 141 | BottomSheet.propTypes = { 142 | children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), 143 | isOpen: PropTypes.bool, 144 | sliderMaxHeight: PropTypes.number, 145 | sliderMinHeight: (props, propName, _) => { 146 | if (props[propName] > props.sliderMaxHeight) { 147 | return new Error( 148 | 'sliderMinHeight value cannot be greater than sliderMaxHeight', 149 | ); 150 | } 151 | }, 152 | animation: PropTypes.func, 153 | animationDuration: PropTypes.number, 154 | onOpen: PropTypes.func, 155 | onClose: PropTypes.func, 156 | wrapperStyle: PropTypes.object, 157 | outerContentStyle: PropTypes.object, 158 | innerContentStyle: PropTypes.object, 159 | lineContainerStyle: PropTypes.object, 160 | lineStyle: PropTypes.object, 161 | }; 162 | 163 | BottomSheet.defaultProps = { 164 | children: , 165 | isOpen: true, 166 | sliderMaxHeight: Dimensions.get('window').height * 0.5, 167 | sliderMinHeight: 50, 168 | animation: Easing.quad, 169 | animationDuration: 200, 170 | onOpen: () => null, 171 | onClose: () => null, 172 | wrapperStyle: {}, 173 | outerContentStyle: {}, 174 | innerContentStyle: {}, 175 | lineContainerStyle: {}, 176 | lineStyle: {}, 177 | }; 178 | 179 | const styles = { 180 | container: { 181 | flex: 1, 182 | shadowColor: '#000000', 183 | shadowOffset: { 184 | width: 0, 185 | height: 6, 186 | }, 187 | shadowOpacity: 0.37, 188 | shadowRadius: 7.49, 189 | elevation: 12, 190 | paddingHorizontal: 21, 191 | borderTopLeftRadius: 30, 192 | borderTopRightRadius: 30, 193 | position: 'absolute', 194 | bottom: 0, 195 | right: 0, 196 | left: 0, 197 | backgroundColor: '#ffffff', 198 | }, 199 | lineContainer: { 200 | borderTopLeftRadius: 30, 201 | borderTopRightRadius: 30, 202 | justifyContent: 'center', 203 | alignItems: 'center', 204 | }, 205 | line: { 206 | width: 35, 207 | height: 4, 208 | borderRadius: 2, 209 | marginTop: 18, 210 | marginBottom: 30, 211 | backgroundColor: '#D5DDE0', 212 | }, 213 | outerContent: { 214 | flex: -1, 215 | }, 216 | innerContent: { 217 | flex: -1, 218 | }, 219 | }; 220 | 221 | export default BottomSheet; 222 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-simple-bottom-sheet 2 | 3 | [![CodeFactor](https://www.codefactor.io/repository/github/stefanomartella/react-native-simple-bottom-sheet/badge)](https://www.codefactor.io/repository/github/stefanomartella/react-native-simple-bottom-sheet) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/8db64b8b3d2347469aac4fe2032053f4)](https://www.codacy.com/gh/StefanoMartella/react-native-simple-bottom-sheet/dashboard?utm_source=github.com&utm_medium=referral&utm_content=StefanoMartella/react-native-simple-bottom-sheet&utm_campaign=Badge_Grade) ![GithubStart](https://badgen.net/github/stars/StefanoMartella/react-native-simple-bottom-sheet) ![GithubLicense](https://badgen.net/github/license/StefanoMartella/react-native-simple-bottom-sheet) ![NpmVersion](https://badgen.net/npm/v/react-native-simple-bottom-sheet) ![NpmMonthlyDownloads](https://badgen.net/npm/dm/react-native-simple-bottom-sheet) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/donate/?hosted_button_id=HXMYHURK4YX3E) 4 | 5 | A simple react native bottom sheet component 6 | 7 | Example 1 | Example 2 | Example 3 8 | :-------------------------:|:-------------------------:|:-------------------------: 9 | ![](./gif/example1.gif) | ![](./gif/example2.gif) | ![](./gif/toggle.gif) 10 | 11 | ## Table of Contents 12 | 13 | * [Installation](#installation) 14 | * [Usage](#usage) 15 | * [Props](#props) 16 | * [Methods](#methods) 17 | 18 | ## Installation 19 | 20 | `npm i --save react-native-simple-bottom-sheet` 21 | 22 | ## Usage 23 | 24 | ```javascript 25 | import BottomSheet from 'react-native-simple-bottom-sheet'; 26 | 27 | function App() { 28 | return ( 29 | 30 | Your content 31 | 32 | // The component to render inside the panel 33 | 34 | 35 | 36 | ); 37 | } 38 | ``` 39 | 40 | By default the height of the panel tries to adapt to the content height till the `sliderMaxHeight` value is reached.
41 | If you want the content to scroll inside the panel use `ScrollView`/`FlatList` like this: 42 | 43 | ```javascript 44 | function App() { 45 | return ( 46 | 47 | Your content 48 | 49 | {(onScrollEndDrag) => ( 50 | 51 | {[...Array(10)].map((_, index) => ( 52 | 53 | {`List Item ${index + 1}`} 54 | 55 | ))} 56 | 57 | )} 58 | 59 | 60 | ); 61 | } 62 | ``` 63 | 64 | This allows the panel to close when the user reaches the top of the scrollable content and drags the panel down again. Example: 65 | 66 |
67 |

68 | scroll 69 |

70 |

71 | 72 | By default when the panel is closed you can drag it up again thanks to the part of the panel that remains 73 | on the bottom side of the screen. If you want to completely hide it you can set the `sliderMinHeight` prop 74 | to `0` and use the `togglePanel` method to bring it up. 75 | 76 | ```javascript 77 | function App() { 78 | const panelRef = useRef(null); 79 | 80 | return ( 81 | 82 | Your content 83 | panelRef.current.togglePanel()}> 84 | Toggle 85 | 86 | panelRef.current = ref}> 87 | 88 | Some random content 89 | 90 | 91 | 92 | ); 93 | } 94 | ``` 95 | 96 |
97 |

98 | toggle 99 |

100 |
101 | 102 | ## Props 103 | 104 | | Prop Name | Type | Default | Description | 105 | |--------------------|-------------|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 106 | | children | func or node | `` | A component or a render function. Use `toggleSlider` function instead | 107 | | isOpen | boolean | `true` | Initial state of the panel; true to render it opened, false otherwise. **Important: Do not try to open/close the panel througth this prop, see** `togglePanel` **method instead** | 108 | | sliderMinHeight | number | `50` | Min height of the panel | 109 | | sliderMaxHeight | number | `Dimensions.get('window').height * 0.5` | Max height of the panel | 110 | | animation | func | `Easing.quad` | The close/open animation of the panel | 111 | | animationDuration | number | `200` | How long the panel takes to open/close | 112 | | onOpen | function | `() => null` | Function to execute when the panel is opened | 113 | | onClose | function | `() => null` | Function to execute when the panel is closed | 114 | | wrapperStyle | object | `{}` | Custom style for the panel wrapper | 115 | | outerContentStyle | object | `{}` | Custom style for the outer content | 116 | | innerContentStyle | object | `{}` | Custom style for the inner content | 117 | | lineContainerStyle | object | `{}` | Custom style for the line container | 118 | | lineStyle | object | `{}` | Custom style for the line | 119 | 120 | ## Methods 121 | 122 | | Name | Description | 123 | |--------------|----------------------------------| 124 | | togglePanel | Function to close/open the panel | 125 | 126 | ## License 127 | 128 | MIT 129 | 130 | ## Support 131 | 132 | If you enjoyed this project — or just feeling generous, consider buying me a beer. Cheers! 133 | 134 | 135 | 136 | ## Author 137 | 138 | Made by Stefano Martella 139 | --------------------------------------------------------------------------------