Skip to content

Instantly share code, notes, and snippets.

@RichardLindhout
Created February 10, 2020 23:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RichardLindhout/81a4b7592e5a53b22f841a3377a823c2 to your computer and use it in GitHub Desktop.
Save RichardLindhout/81a4b7592e5a53b22f841a3377a823c2 to your computer and use it in GitHub Desktop.
Closable modal scroll friendly React Native
import React, { useRef, useCallback } from 'react'
import {
View,
Keyboard,
Animated,
ScrollView,
TouchableWithoutFeedback,
StyleSheet,
} from 'react-native'
import { Portal } from 'react-native-paper'
import widthAndHeightHOC from '../WidthAndHeight/widthAndHeightHOC'
import safeAreaHOC from '../WidthAndHeight/safeAreaHOC'
import keyboardHOC from './keyboardHOC'
function BottomSheet({
keyboardHeight,
isVisible,
onRequestClose,
height,
safe,
children,
}) {
const animatedScrollYValue = useRef(new Animated.Value(0)).current
const scrollView = React.createRef()
const offset = React.createRef(0)
const onAnimatedClose = useCallback(() => {
if (scrollView.current) {
Keyboard.dismiss()
scrollView.current &&
scrollView.current.scrollTo({ y: 0, animated: true })
onRequestClose()
} else {
onRequestClose()
}
}, [scrollView, onRequestClose])
const onScrollViewRendered = useCallback(
node => {
scrollView.current = node
if (scrollView.current) {
setTimeout(() => {
scrollView.current &&
scrollView.current.scrollTo({
y: height - safe.top,
animated: true,
})
}, 100)
}
},
[scrollView, height, safe.top]
)
const onScroll = useCallback(
event => {
var currentOffset = event.nativeEvent.contentOffset.y
let direction = currentOffset > offset.current ? 'down' : 'up'
if (currentOffset === offset.current) {
direction = 'idle'
}
offset.current = currentOffset
if (currentOffset <= 0 && (direction === 'up' || direction === 'idle')) {
if (isVisible) {
onRequestClose()
}
}
},
[offset, isVisible, onRequestClose]
)
const animatedOpacity = animatedScrollYValue.interpolate({
inputRange: [0, height - safe.top, height],
outputRange: [0, 0, 0.4],
extrapolate: 'clamp',
})
return (
<Portal pointerEvents="none">
{isVisible && (
<>
<ScrollView
ref={onScrollViewRendered}
scrollEventThrottle={25}
keyboardShouldPersistTaps="always"
style={styles.scrollView}
contentInsetAdjustmentBehavior={'never'}
alwaysBounceVertical={false}
bounces={false}
onScroll={Animated.event(
[
{
nativeEvent: { contentOffset: { y: animatedScrollYValue } },
},
],
{ listener: onScroll }
)}
>
<TouchableWithoutFeedback onPress={onAnimatedClose}>
<View
style={[
styles.touchableInner,
{
height: height,
},
]}
/>
</TouchableWithoutFeedback>
<View style={[styles.panel, { minHeight: height }]}>
<View style={{ height: safe.top }} />
{children}
<View style={{ height: safe.bottom }} />
{keyboardHeight > 0 && (
<View style={{ height: keyboardHeight }} />
)}
</View>
</ScrollView>
<Animated.View
style={[
styles.statusBar,
{
height: safe.top,
opacity: animatedOpacity,
},
]}
pointerEvents="none"
/>
</>
)}
</Portal>
)
}
const styles = StyleSheet.create({
panel: {
width: '100%',
backgroundColor: '#fff',
borderTopStartRadius: 20,
borderTopEndRadius: 20,
overflow: 'hidden',
},
touchableInner: { width: '100%' },
scrollView: { flex: 1, backgroundColor: 'rgba(0,0,0,0.4)' },
statusBar: {
backgroundColor: '#000',
position: 'absolute',
top: 0,
left: 0,
right: 0,
},
})
export default keyboardHOC(safeAreaHOC(widthAndHeightHOC(BottomSheet)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment