Skip to content

Commit

Permalink
Improve desktop UX + Code cleanup
Browse files Browse the repository at this point in the history
Now when dragging the content inside of the ScrollView will be moved, instead of the whole ScrollView. In desktop it's particularly noticeable since the scroll bar is solid
  • Loading branch information
NiciusB committed Mar 26, 2020
1 parent acf556c commit 4f426fb
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 58 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "react-native-web-refresh-control",
"version": "1.0.0",
"description": "An implementation of React Native's RefreshControl for web, since react-native-web does not provide one ATM",
"description": "An implementation of React Native's RefreshControl for web, since react-native-web currently does not provide one",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
Expand Down
143 changes: 88 additions & 55 deletions src/RefreshControl.web.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,60 +14,69 @@ import PropTypes from 'prop-types'
const arrowIcon =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAJcEhZcwAADdcAAA3XAUIom3gAAAAHdElNRQfgCQYHLCTylhV1AAAAjklEQVQ4y2P8z0AaYCJRPX4NsyNWM5Ok4R/n+/noWhjx+2F20n8HwcTQv0T7IXUe4wFUWwh6Gl0LEaGEqoWoYEXWQmQ8ILQwEh/TkBBjme3HIESkjn+Mv9/vJjlpkOwkom2AxTmRGhBJhCgNyCmKCA2oCZCgBvT0ykSacgIaZiaiKydoA7pykiKOSE+jAwADZUnJjMWwUQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNi0wOS0wNlQwNzo0NDozNiswMjowMAZN3oQAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTYtMDktMDZUMDc6NDQ6MzYrMDI6MDB3EGY4AAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAABJRU5ErkJggg=='

function RefreshControl(props) {
const propsRef = useRef(props)
function RefreshControl({
refreshing,
tintColor,
colors,
style,
progressViewOffset,
children,
size,
title,
titleColor,
onRefresh,
enabled,
}) {
const onRefreshRef = useRef(onRefresh)
useEffect(() => {
propsRef.current = props
}, [props])
onRefreshRef.current = onRefresh
}, [onRefresh])
const enabledRef = useRef(enabled)
useEffect(() => {
enabledRef.current = enabled
}, [enabled])

const contentContainerRef = useRef()
const containerRef = useRef()
const pullPosReachedState = useRef(0)
const pullPosReachedAnimated = useRef(new Animated.Value(0))
const pullDownSwipeMargin = useRef(new Animated.Value(0))

useEffect(() => {
Animated.timing(pullDownSwipeMargin.current, {
toValue: props.refreshing ? 50 : 0,
toValue: refreshing ? 50 : 0,
duration: 350,
}).start()
if (props.refreshing) {
if (refreshing) {
pullPosReachedState.current = 0
pullPosReachedAnimated.current.setValue(0)
}
}, [props.refreshing])
}, [refreshing])

const resetPullVariables = useCallback(() => {
const onPanResponderFinish = useCallback(() => {
if (pullPosReachedState.current && onRefreshRef.current) {
onRefreshRef.current()
}
if (!pullPosReachedState.current) {
Animated.timing(pullDownSwipeMargin.current, {
toValue: 0,
duration: 350,
}).start()
}
}, [])
const resetPullVariablesRef = useRef(resetPullVariables)
useEffect(() => {
resetPullVariablesRef.current = resetPullVariables
}, [resetPullVariables])

const onPanResponderFinish = useCallback((evt, gestureState) => {
if (pullPosReachedState.current && propsRef.current.onRefresh) {
propsRef.current.onRefresh()
}
resetPullVariablesRef.current()
}, [])

const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => false,
onStartShouldSetPanResponderCapture: (evt, gestureState) => false,
onMoveShouldSetPanResponder: (evt, gestureState) => {
if (!contentContainerRef.current) return false
const containerDOM = findNodeHandle(contentContainerRef.current)
return containerDOM ? containerDOM.children[0].scrollTop === 0 : false
onStartShouldSetPanResponder: () => false,
onStartShouldSetPanResponderCapture: () => false,
onMoveShouldSetPanResponder: () => {
if (!containerRef.current) return false
const containerDOM = findNodeHandle(containerRef.current)
if (!containerDOM) return false
return containerDOM.children[0].scrollTop === 0
},
onMoveShouldSetPanResponderCapture: (evt, gestureState) => false,
onPanResponderMove: (evt, gestureState) => {
if (propsRef.current.enabled !== undefined && !propsRef.current.enabled) return
onMoveShouldSetPanResponderCapture: () => false,
onPanResponderMove: (_, gestureState) => {
if (enabledRef.current !== undefined && !enabledRef.current) return

const adjustedDy = gestureState.dy <= 0 ? 0 : (gestureState.dy * 150) / (gestureState.dy + 120) // Diminishing returns function
pullDownSwipeMargin.current.setValue(adjustedDy)
Expand All @@ -80,16 +89,16 @@ function RefreshControl(props) {
}).start()
}
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderTerminationRequest: () => true,
onPanResponderRelease: onPanResponderFinish,
onPanResponderTerminate: onPanResponderFinish,
})
)

const refreshIndicatorColor = useMemo(
() => (props.tintColor ? props.tintColor : props.colors && props.colors.length ? props.colors[0] : null),
[props.colors, props.tintColor]
)
const refreshIndicatorColor = useMemo(() => (tintColor ? tintColor : colors && colors.length ? colors[0] : null), [
colors,
tintColor,
])
const pullDownIconStyle = useMemo(
() => ({
width: 22,
Expand All @@ -107,46 +116,56 @@ function RefreshControl(props) {
[]
)

const containerStyle = useMemo(
() => [style, { overflowY: 'hidden', overflow: 'hidden', paddingTop: progressViewOffset }],
[progressViewOffset, style]
)
const indicatorTransformStyle = useMemo(
() => ({
alignSelf: 'center',
marginTop: -40,
height: 40,
transform: [{ translateY: pullDownSwipeMargin.current }],
}),
[]
)
const contentContainerStyle = useMemo(
() => ({
flex: 1,
transform: [{ translateY: pullDownSwipeMargin.current }],
}),

// This is messing with react-native-web's internal implementation
// Will probably break if anything changes on their end
const AnimatedContentContainer = useMemo(
() => withAnimated(childProps => <children.props.children.type {...childProps} />),
[]
)

return (
<Animated.View
style={[props.style, { overflowY: 'hidden', overflow: 'hidden' }]}
{...panResponder.current.panHandlers}>
<Animated.View style={[indicatorTransformStyle, { marginLeft: 'auto', marginRight: 'auto' }]}>
{props.refreshing ? (
const newContentContainerStyle = useMemo(
() => [children.props.children.style, { transform: [{ translateY: pullDownSwipeMargin.current }] }],
[children.props.children.style]
)
const newChildren = React.cloneElement(
children,
null,
<>
<Animated.View style={indicatorTransformStyle}>
{refreshing ? (
<>
<ActivityIndicator
color={refreshIndicatorColor || undefined}
size={props.size || undefined}
size={size || undefined}
style={{ marginVertical: 10 }}
/>
{props.title && (
<Text style={{ color: props.titleColor, textAlign: 'center', marginTop: 5 }}>{props.title}</Text>
)}
{title && <Text style={{ color: titleColor, textAlign: 'center', marginTop: 5 }}>{title}</Text>}
</>
) : (
<Animated.Image source={{ uri: arrowIcon }} style={pullDownIconStyle} />
)}
</Animated.View>
{props.progressViewOffset && <View style={{ marginTop: props.progressViewOffset }} />}
<Animated.View ref={contentContainerRef} style={contentContainerStyle}>
{props.children}
</Animated.View>
</Animated.View>
<AnimatedContentContainer {...children.props.children.props} style={newContentContainerStyle} />
</>
)

return (
<View ref={containerRef} style={containerStyle} {...panResponder.current.panHandlers}>
{newChildren}
</View>
)
}

Expand All @@ -165,3 +184,17 @@ RefreshControl.propTypes = {
}

export default RefreshControl

function withAnimated(WrappedComponent) {
const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component'

class WithAnimated extends React.Component {
static displayName = `WithAnimated(${displayName})`

render() {
return <WrappedComponent {...this.props} />
}
}

return Animated.createAnimatedComponent(WithAnimated)
}
4 changes: 2 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import { FlatList, Platform, ScrollView } from 'react-native'
import CustomRefreshControl from './RefreshControl'

export const RefreshControl = CustomRefreshControl
export { CustomRefreshControl as RefreshControl }
export function patchFlatListProps() {
try {
if (Platform.OS === 'web') {
Expand All @@ -21,7 +21,7 @@ function setCustomFlatListWeb() {
<ScrollView
{...props}
//eslint-disable-next-line react/prop-types
refreshControl={<RefreshControl refreshing={props.refreshing} onRefresh={props.onRefresh} />}
refreshControl={<CustomRefreshControl refreshing={props.refreshing} onRefresh={props.onRefresh} />}
/>
),
}
Expand Down

0 comments on commit 4f426fb

Please sign in to comment.