import React, { useRef } from 'react'; import { PanResponder, ScrollView, View } from 'react-native'; import type { NativeScrollEvent, NativeSyntheticEvent, ScrollViewProps, } from 'react-native'; import type { ReactElement } from 'react'; import { useDragableDrawerContext } from './DragableDrawerContext'; export type DragableScrollViewProps = ScrollViewProps; // scrollY tolerance: treat anything within this many pixels of the top as // "at scroll top" to handle fractional pixels and throttled scroll events. const SCROLL_TOP_THRESHOLD_PX = 5; // Minimum downward finger movement before the pull-down collapse is triggered. const PULL_DOWN_THRESHOLD_PX = 10; const DEFAULT_SCROLL_EVENT_THROTTLE = 16; /** * A ScrollView that coordinates with a parent DragableDrawer. * * - Drawer below max height: all gestures move the drawer; scroll is locked. * - Drawer at max height: native scrolling works normally. * - Drawer at max + scrolled to top + pull down: drawer collapses. * * The native ScrollView with bounces={false}/overScrollMode="never" will not * intercept a downward gesture at scrollY=0 (nothing left to scroll), so * onMoveShouldSetPanResponderCapture fires cleanly on both platforms. * onScrollEndDrag + onMomentumScrollEnd ensure scrollYRef is up-to-date * before the next gesture starts. */ const DragableScrollView = ({ onScroll, onScrollEndDrag: onScrollEndDragProp, onMomentumScrollEnd: onMomentumScrollEndProp, scrollEventThrottle = DEFAULT_SCROLL_EVENT_THROTTLE, children, ...props }: DragableScrollViewProps): ReactElement => { const { isAtMaxHeight, scrollYRef, onScrollY, beginPan, movePan, releasePan, } = useDragableDrawerContext(); // Mirror context value into a ref so PanResponder closures always read // the latest value (PanResponder is created once and closures are frozen). const isAtMaxHeightRef = useRef(isAtMaxHeight); isAtMaxHeightRef.current = isAtMaxHeight; const panResponder = useRef( PanResponder.create({ // Drawer below max: capture on start so the native ScrollView never // gets the touch and scroll is completely locked. onStartShouldSetPanResponderCapture: () => !isAtMaxHeightRef.current, // Drawer at max + scroll at top + pull down: recapture for collapse. // bounces={false} / overScrollMode="never" means the native ScrollView // has nothing to do here, so it will not intercept the gesture and this // capture fires reliably before any scroll activity begins. onMoveShouldSetPanResponderCapture: (_, gesture) => isAtMaxHeightRef.current && scrollYRef.current <= SCROLL_TOP_THRESHOLD_PX && gesture.dy > PULL_DOWN_THRESHOLD_PX, onPanResponderGrant: () => beginPan(), onPanResponderMove: (_, gesture) => movePan(gesture.dy), onPanResponderRelease: (_, gesture) => releasePan(gesture.dy, gesture.vy), }) ).current; const handleScroll = (e: NativeSyntheticEvent) => { onScrollY(e.nativeEvent.contentOffset.y); onScroll?.(e); }; // Keep scrollYRef accurate at every scroll boundary so the next gesture // checks the correct position even when onScroll is throttled. const handleScrollEndDrag = (e: NativeSyntheticEvent) => { onScrollY(e.nativeEvent.contentOffset.y); onScrollEndDragProp?.(e); }; const handleMomentumScrollEnd = ( e: NativeSyntheticEvent ) => { onScrollY(e.nativeEvent.contentOffset.y); onMomentumScrollEndProp?.(e); }; return ( {children} ); }; export default DragableScrollView;