import React from 'react' import * as RN from 'react-native' import { useEvent } from '../hooks/useEvent' import useRounding from '../hooks/useRounding' import SliderView from './SliderView' type Props = RN.ViewProps & { minimumValue: number; maximumValue: number; step: number; style?: RN.StyleProp; inverted: boolean; vertical: boolean; enabled: boolean; thumbSize?: number; onMove: (value: number) => void; onPress: (value: number) => void; onRelease: (value: number) => void; children?: React.ReactNode; } // We add a default padding to ensure that the responder view has enough space to recognize the touches const styleSheet = RN.StyleSheet.create({ vertical: { paddingHorizontal: 10 }, horizontal: { paddingVertical: 10 } }) const ResponderView = React.forwardRef((props, ref) => { const { width: windowWidth, height: windowHeight } = RN.useWindowDimensions() const { vertical, inverted, enabled, style, minimumValue, maximumValue, step, onLayout: onLayoutProp, onMove: onMoveProp, onPress: onPressProp, onRelease: onReleaseProp, ...others } = props const containerSize = React.useRef({ width: 0, height: 0 }) const fallbackRef = React.useRef() const forwardRef = React.useCallback((view: RN.View) => { fallbackRef.current = view if (ref) { if (typeof ref === 'function') ref(view) else ref.current = view } }, [ref]) const round = useRounding({ step, minimumValue, maximumValue }) const isVertical = React.useMemo(() => vertical || (style && (RN.StyleSheet.flatten(style).flexDirection || '').startsWith('column')), [vertical, style]) // See below for more details const originPageLocation = React.useRef({ pageX: 0, pageY: 0 }) React.useEffect(() => { // We update the component's origin when the window size changes fallbackRef.current?.measure((_x, _y, _width, _height, pageX = 0, pageY = 0) => (originPageLocation.current = { pageX, pageY })) }, [windowWidth, windowHeight]) /** Convert a touch event into it's position on the slider */ const eventToValue = useEvent((event: RN.GestureResponderEvent) => { // We could simplify this code if this bug was solved: // https://github.com/Sharcoux/slider/issues/18#issuecomment-877411645 const { pageX, pageY, locationX, locationY } = event.nativeEvent const x = RN.Platform.OS === 'web' ? locationX : (pageX - originPageLocation.current.pageX) const y = RN.Platform.OS === 'web' ? locationY : (pageY - originPageLocation.current.pageY) let offset = isVertical ? y : x const size = containerSize.current?.[isVertical ? 'height' : 'width'] || 1 // In RTL mode, coordinates are flipped for horizontal sliders if (RN.I18nManager.isRTL && !isVertical) offset = size - offset const newValue = inverted ? maximumValue - ((maximumValue - minimumValue) * offset) / size : minimumValue + ((maximumValue - minimumValue) * offset) / size return round(newValue) }) const onMove = useEvent((event: RN.GestureResponderEvent) => { onMoveProp(eventToValue(event)) event.preventDefault() event.stopPropagation() }) const onPress = useEvent((event: RN.GestureResponderEvent) => { onPressProp(eventToValue(event)) event.preventDefault() event.stopPropagation() }) const onRelease = useEvent((event: RN.GestureResponderEvent) => { onReleaseProp(eventToValue(event)) event.preventDefault() event.stopPropagation() }) const isEnabled = useEvent((event: RN.GestureResponderEvent) => { event.stopPropagation() return enabled }) const keepResponder = useEvent((event: RN.GestureResponderEvent) => { event.stopPropagation() return false }) const onLayout = useEvent((event: RN.LayoutChangeEvent) => { // For some reason, pageX and pageY might be 'undefined' in some cases fallbackRef.current?.measure((_x, _y, _width, _height, pageX = 0, pageY = 0) => (originPageLocation.current = { pageX, pageY })) onLayoutProp?.(event) containerSize.current = event.nativeEvent.layout }) const containerStyle = React.useMemo(() => [styleSheet[vertical ? 'vertical' : 'horizontal'], style], [vertical, style]) return ( {props.children} ) }) ResponderView.displayName = 'ResponderView' export default React.memo(ResponderView)