UNPKG

@quidone/react-native-wheel-picker

Version:

Picker is a UI component for selecting an item from a list of options.

145 lines (144 loc) 5.6 kB
"use strict"; import React, { forwardRef, memo, useEffect, useMemo, useRef } from 'react'; import { useStableCallback } from '@rozhkov/react-useful-hooks'; import debounce from '../debounce'; import { jsx as _jsx } from "react/jsx-runtime"; const withScrollStartEndEvent = Component => { const Wrapper = ({ onScrollStart: onScrollStartProp, onScrollEnd: onScrollEndProp, onScrollBeginDrag: onScrollBeginDragProp, onScrollEndDrag: onScrollEndDragProp, onMomentumScrollBegin: onMomentumScrollBeginProp, onMomentumScrollEnd: onMomentumScrollEndProp, scrollOffset, ...rest }, forwardedRef) => { const onScrollStartStable = useStableCallback(onScrollStartProp); const isOnScrollStartCalledRef = useRef(false); /* * `isImplicitScrollRef` marks a scroll session that was detected from * `contentOffset` updates only, without a native start callback. * * Why this exists: * Android may perform a programmatic animated scroll * (`scrollTo` / `scrollToIndex`) and emit several offset updates while * skipping the normal callback pair that we usually rely on: * `onScrollBeginDrag` / `onScrollEndDrag` and * `onMomentumScrollBegin` / `onMomentumScrollEnd`. * * Relevant React Native issues: * https://github.com/facebook/react-native/issues/11693 * https://github.com/facebook/react-native/issues/19246 * https://github.com/facebook/react-native/issues/25672 * https://github.com/facebook/react-native/issues/26661 * * In this library it shows up when one wheel triggers a synchronized * animated scroll in another wheel. Example sequence on Android: * - offset changes: 48 -> 47.6 -> 46.0 -> 43.4 -> ... -> 0 * - but no native "begin/end" events are dispatched for that movement * * Old failure mode: * - the first offset update inferred scroll start and scheduled a debounced * scroll end * - the next offset updates only cleared that debounced end * - because Android never sent a native end event, the session stayed * "started" forever * - `PickerControl` then kept one picker in `isStopped = false`, so the * aggregated DatePicker `onDateChanged` stopped firing * * We still emit `onScrollStart` for such a session, but we must finish it * differently: if the session is implicit, every offset update should * re-arm the debounced end so that the last offset update wins and the * scroll eventually ends even when Android never gives us a native end * callback. * * Related library issues: * https://github.com/quidone/react-native-wheel-picker/issues/56 * https://github.com/quidone/react-native-wheel-picker/issues/71 */ const isImplicitScrollRef = useRef(false); const deactivateOnScrollStart = useStableCallback(() => { isOnScrollStartCalledRef.current = false; isImplicitScrollRef.current = false; }); const maybeCallOnScrollStart = useStableCallback(({ implicit }) => { const shouldActivate = !isOnScrollStartCalledRef.current; if (shouldActivate) { onScrollStartStable(); isOnScrollStartCalledRef.current = true; isImplicitScrollRef.current = implicit; return; } if (!implicit) { isImplicitScrollRef.current = false; } }); const maybeCallOnNativeScrollStart = useStableCallback(() => { maybeCallOnScrollStart({ implicit: false }); }); const maybeCallOnImplicitScrollStart = useStableCallback(() => { maybeCallOnScrollStart({ implicit: true }); }); const onScrollEndStable = useStableCallback(() => { maybeCallOnNativeScrollStart(); onScrollEndProp?.(); deactivateOnScrollStart(); }); const onScrollEnd = useMemo(() => debounce(onScrollEndStable, 100), // A small delay is needed so that onScrollEnd doesn't trigger prematurely. [onScrollEndStable]); const onScrollBeginDrag = useStableCallback(args => { maybeCallOnNativeScrollStart(); onScrollBeginDragProp?.(args); }); const onScrollEndDrag = useStableCallback(args => { onScrollEndDragProp?.(args); onScrollEnd(); }); const onMomentumScrollBegin = useStableCallback(args => { maybeCallOnNativeScrollStart(); onScrollEnd.clear(); onMomentumScrollBeginProp?.(args); }); const onMomentumScrollEnd = useStableCallback(args => { onMomentumScrollEndProp?.(args); onScrollEnd(); }); useEffect(() => { const sub = scrollOffset.addListener(() => { if (!isOnScrollStartCalledRef.current) { maybeCallOnImplicitScrollStart(); onScrollEnd(); return; } if (isImplicitScrollRef.current) { onScrollEnd(); } else { onScrollEnd.clear(); } }); return () => { scrollOffset.removeListener(sub); }; }, [maybeCallOnImplicitScrollStart, onScrollEnd, scrollOffset]); return /*#__PURE__*/_jsx(Component, { ...rest, ref: forwardedRef, onScrollBeginDrag: onScrollBeginDrag, onScrollEndDrag: onScrollEndDrag, onMomentumScrollBegin: onMomentumScrollBegin, onMomentumScrollEnd: onMomentumScrollEnd }); }; Wrapper.displayName = `withScrollStartEndEvent(${Component.displayName || 'Component'})`; return /*#__PURE__*/memo(/*#__PURE__*/forwardRef(Wrapper)); }; export default withScrollStartEndEvent; //# sourceMappingURL=withScrollStartEndEvent.js.map