@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
JavaScript
;
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