From fa6666625daec23900d4f67c75ee0f0f22f90d29 Mon Sep 17 00:00:00 2001 From: rozhkovs Date: Wed, 23 Jul 2025 19:58:34 +0700 Subject: [PATCH 1/5] fix: `onValueChanged` is called between scrolling and index synchronization --- src/base/list/List.tsx | 1 + src/hoc/virtualized/VirtualizedList.tsx | 1 + src/utils/scrolling/withScrollEndEvent.tsx | 19 ++++++++++++++++--- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/base/list/List.tsx b/src/base/list/List.tsx index b302cf2..2907c02 100644 --- a/src/base/list/List.tsx +++ b/src/base/list/List.tsx @@ -112,6 +112,7 @@ const List = >( ref={listRef} contentOffset={initialOffset} onScroll={onScroll} + scrollOffset={scrollOffset} snapToOffsets={snapToOffsets} style={styles.list} contentContainerStyle={contentContainerStyle} diff --git a/src/hoc/virtualized/VirtualizedList.tsx b/src/hoc/virtualized/VirtualizedList.tsx index b367794..0c233ea 100644 --- a/src/hoc/virtualized/VirtualizedList.tsx +++ b/src/hoc/virtualized/VirtualizedList.tsx @@ -119,6 +119,7 @@ const VirtualizedList = >( getItemLayout={getItemLayout} initialScrollIndex={initialIndex} onScroll={onScroll} + scrollOffset={scrollOffset} snapToOffsets={snapToOffsets} style={styles.list} contentContainerStyle={contentContainerStyle} diff --git a/src/utils/scrolling/withScrollEndEvent.tsx b/src/utils/scrolling/withScrollEndEvent.tsx index f3b0b1f..84febb5 100644 --- a/src/utils/scrolling/withScrollEndEvent.tsx +++ b/src/utils/scrolling/withScrollEndEvent.tsx @@ -5,12 +5,14 @@ import React, { forwardRef, memo, useCallback, + useEffect, useMemo, } from 'react'; import type { - ScrollViewProps, - NativeSyntheticEvent, + Animated, NativeScrollEvent, + NativeSyntheticEvent, + ScrollViewProps, } from 'react-native'; import debounce from '@utils/debounce'; @@ -23,6 +25,7 @@ type ComponentProps = Pick< >; type ExtendProps = PropsT & { + scrollOffset: Animated.Value; onScrollEnd?: () => void; }; @@ -35,12 +38,13 @@ const withScrollEndEvent = ( onScrollEndDrag: onScrollEndDragProp, onMomentumScrollBegin: onMomentumScrollBeginProp, onMomentumScrollEnd: onMomentumScrollEndProp, + scrollOffset, ...rest }: ExtendProps, forwardedRef: ForwardedRef>>, ) => { const onScrollEnd = useMemo( - () => debounce(onScrollEndProp, 0), // This works well with onScrollEndDrag -> onMomentumScrollBegin transitions + () => debounce(onScrollEndProp, 0), // A small delay is needed so that onScrollEnd doesn't trigger prematurely. [onScrollEndProp], ); @@ -68,6 +72,15 @@ const withScrollEndEvent = ( [onScrollEnd, onMomentumScrollEndProp], ); + useEffect(() => { + const sub = scrollOffset.addListener(() => { + onScrollEnd.clear(); + }); + return () => { + scrollOffset.removeListener(sub); + }; + }, [onScrollEnd, scrollOffset]); + return ( Date: Wed, 23 Jul 2025 20:08:09 +0700 Subject: [PATCH 2/5] fix: `WheelPicker` doesn't synchronize scrolling, if `value` prop doesn't change after scrolling --- src/base/picker/Picker.tsx | 29 ++++++++++++-------- src/base/picker/hooks/useSyncScrollEffect.ts | 23 ++++++++++++++-- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/base/picker/Picker.tsx b/src/base/picker/Picker.tsx index 8e77706..b874818 100644 --- a/src/base/picker/Picker.tsx +++ b/src/base/picker/Picker.tsx @@ -22,7 +22,7 @@ import Overlay from '../overlay/Overlay'; import {calcPickerHeight, createFaces} from '../item/faces'; import PickerItemContainer from '../item/PickerItemContainer'; import {useBoolean} from '@utils/react'; -import {useInit} from '@rozhkov/react-useful-hooks'; +import {useInit, useStableCallback} from '@rozhkov/react-useful-hooks'; import List from '../list/List'; export type PickerProps> = { @@ -138,22 +138,29 @@ const Picker = >({ ], ); - const {activeIndexRef, onScrollEnd} = useValueEventsEffect( - { - data, - valueIndex, - itemHeight, - offsetYAv: offsetY, - }, - {onValueChanging, onValueChanged}, - ); - useSyncScrollEffect({ + const {activeIndexRef, onScrollEnd: onScrollEndForValueEvents} = + useValueEventsEffect( + { + data, + valueIndex, + itemHeight, + offsetYAv: offsetY, + }, + {onValueChanging, onValueChanged}, + ); + const {onScrollEnd: onScrollEndForSyncScroll} = useSyncScrollEffect({ listRef, + value, valueIndex, activeIndexRef, touching: touching.value, }); + const onScrollEnd = useStableCallback(() => { + onScrollEndForValueEvents(); + onScrollEndForSyncScroll(); + }); + return ( diff --git a/src/base/picker/hooks/useSyncScrollEffect.ts b/src/base/picker/hooks/useSyncScrollEffect.ts index 6983fee..bdd3358 100644 --- a/src/base/picker/hooks/useSyncScrollEffect.ts +++ b/src/base/picker/hooks/useSyncScrollEffect.ts @@ -1,18 +1,21 @@ -import {type RefObject, useEffect} from 'react'; +import {type RefObject, useEffect, useRef} from 'react'; +import {useStableCallback} from '@rozhkov/react-useful-hooks'; import type {ListMethods} from '../../types'; const useSyncScrollEffect = ({ listRef, + value, valueIndex, activeIndexRef, touching, }: { listRef: RefObject; + value: unknown; valueIndex: number; activeIndexRef: RefObject; touching: boolean; }) => { - useEffect(() => { + const syncScroll = useStableCallback(() => { if ( listRef.current == null || touching || @@ -22,7 +25,23 @@ const useSyncScrollEffect = ({ } listRef.current.scrollToIndex({index: valueIndex, animated: true}); + }); + + useEffect(() => { + syncScroll(); }, [valueIndex]); // eslint-disable-line react-hooks/exhaustive-deps + + const timeoutId = useRef(undefined); + const onScrollEnd = useStableCallback(() => { + if (value !== undefined) { + clearTimeout(timeoutId.current); + timeoutId.current = setTimeout(syncScroll, 0); + } + }); + + return { + onScrollEnd, + }; }; export default useSyncScrollEffect; From 2b0bb87c06b20ed89a9e8463d3639fac8acc716d Mon Sep 17 00:00:00 2001 From: rozhkovs Date: Wed, 23 Jul 2025 20:31:55 +0700 Subject: [PATCH 3/5] feat: add `extraValues` prop for additional synchronization --- src/base/picker/Picker.tsx | 3 ++ src/base/picker/hooks/useSyncScrollEffect.ts | 7 ++++ src/utils/react/index.ts | 1 + .../react/useEffectWithDynamicDepsLength.ts | 35 +++++++++++++++++++ 4 files changed, 46 insertions(+) create mode 100644 src/utils/react/useEffectWithDynamicDepsLength.ts diff --git a/src/base/picker/Picker.tsx b/src/base/picker/Picker.tsx index b874818..772b579 100644 --- a/src/base/picker/Picker.tsx +++ b/src/base/picker/Picker.tsx @@ -28,6 +28,7 @@ import List from '../list/List'; export type PickerProps> = { data: ReadonlyArray; value?: ItemT['value']; + extraValues?: unknown[]; itemHeight?: number; visibleItemCount?: number; width?: number | 'auto' | `${number}%`; @@ -82,6 +83,7 @@ const useValueIndex = (data: ReadonlyArray>, value: any) => { const Picker = >({ data, value, + extraValues = [], width = 'auto', itemHeight = 48, visibleItemCount = 5, @@ -152,6 +154,7 @@ const Picker = >({ listRef, value, valueIndex, + extraValues, activeIndexRef, touching: touching.value, }); diff --git a/src/base/picker/hooks/useSyncScrollEffect.ts b/src/base/picker/hooks/useSyncScrollEffect.ts index bdd3358..0f89c0a 100644 --- a/src/base/picker/hooks/useSyncScrollEffect.ts +++ b/src/base/picker/hooks/useSyncScrollEffect.ts @@ -1,17 +1,20 @@ import {type RefObject, useEffect, useRef} from 'react'; import {useStableCallback} from '@rozhkov/react-useful-hooks'; +import {useEffectWithDynamicDepsLength} from '@utils/react'; import type {ListMethods} from '../../types'; const useSyncScrollEffect = ({ listRef, value, valueIndex, + extraValues = [], activeIndexRef, touching, }: { listRef: RefObject; value: unknown; valueIndex: number; + extraValues: unknown[] | undefined; activeIndexRef: RefObject; touching: boolean; }) => { @@ -31,6 +34,10 @@ const useSyncScrollEffect = ({ syncScroll(); }, [valueIndex]); // eslint-disable-line react-hooks/exhaustive-deps + useEffectWithDynamicDepsLength(() => { + syncScroll(); + }, extraValues); + const timeoutId = useRef(undefined); const onScrollEnd = useStableCallback(() => { if (value !== undefined) { diff --git a/src/utils/react/index.ts b/src/utils/react/index.ts index f30ff1e..993a546 100644 --- a/src/utils/react/index.ts +++ b/src/utils/react/index.ts @@ -1,2 +1,3 @@ export {default as typedMemo} from './typedMemo'; export {default as useBoolean} from './useBoolean'; +export {default as useEffectWithDynamicDepsLength} from './useEffectWithDynamicDepsLength'; diff --git a/src/utils/react/useEffectWithDynamicDepsLength.ts b/src/utils/react/useEffectWithDynamicDepsLength.ts new file mode 100644 index 0000000..6a98fec --- /dev/null +++ b/src/utils/react/useEffectWithDynamicDepsLength.ts @@ -0,0 +1,35 @@ +import {useEffect} from 'react'; +import {usePrevious, useStableCallback} from '@rozhkov/react-useful-hooks'; + +const areArraysShallowEqual = ( + arr1: unknown[] | undefined, + arr2: unknown[] | undefined, +): boolean => { + if (arr1 === arr2) return true; + if (!arr1 || !arr2) return false; + + if (arr1.length !== arr2.length) return false; + + for (let i = 0; i < arr1.length; i++) { + if (arr1[i] !== arr2[i]) return false; + } + + return true; +}; + +const useEffectWithDynamicDepsLength = ( + callback: () => void, + deps: unknown[], +) => { + const prevDeps = usePrevious(deps); + + const callbackStable = useStableCallback(callback); + + useEffect(() => { + if (!areArraysShallowEqual(prevDeps, deps)) { + callbackStable(); + } + }, [deps]); // eslint-disable-line react-hooks/exhaustive-deps +}; + +export default useEffectWithDynamicDepsLength; From 89c5db56bdbc7e4acdc09a1aac39452cdc7b7454 Mon Sep 17 00:00:00 2001 From: rozhkovs Date: Mon, 28 Jul 2025 22:21:53 +0700 Subject: [PATCH 4/5] feat: add `onScrollStart`, `onScrollStartEnd` events --- src/base/list/List.tsx | 7 ++- src/base/picker/Picker.tsx | 9 +++ src/base/types.ts | 1 + src/hoc/virtualized/VirtualizedList.tsx | 9 ++- src/utils/scrolling/index.ts | 2 +- ...dEvent.tsx => withScrollStartEndEvent.tsx} | 58 ++++++++++++++----- 6 files changed, 67 insertions(+), 19 deletions(-) rename src/utils/scrolling/{withScrollEndEvent.tsx => withScrollStartEndEvent.tsx} (54%) diff --git a/src/base/list/List.tsx b/src/base/list/List.tsx index 2907c02..188f30c 100644 --- a/src/base/list/List.tsx +++ b/src/base/list/List.tsx @@ -20,9 +20,9 @@ import { ViewStyle, } from 'react-native'; import {useInit} from '@rozhkov/react-useful-hooks'; -import {withScrollEndEvent} from '@utils/scrolling'; +import {withScrollStartEndEvent} from '@utils/scrolling'; -const ExtendedAnimatedScrollView = withScrollEndEvent(Animated.ScrollView); +const ExtendedAnimatedScrollView = withScrollStartEndEvent(Animated.ScrollView); const OFFSET_X = 0; const getOffsetY = (index: number, itemHeight: number) => index * itemHeight; @@ -39,6 +39,7 @@ export type ListProps> = { onTouchStart: () => void; onTouchEnd: () => void; onTouchCancel: () => void; + onScrollStart: (() => void) | undefined; onScrollEnd: () => void; contentContainerStyle: StyleProp | undefined; }; @@ -56,6 +57,7 @@ const List = >( onTouchEnd, onTouchStart, onTouchCancel, + onScrollStart, onScrollEnd, contentContainerStyle: contentContainerStyleProp, ...restProps @@ -121,6 +123,7 @@ const List = >( onTouchCancel={onTouchCancel} nestedScrollEnabled={true} removeClippedSubviews={false} + onScrollStart={onScrollStart} onScrollEnd={onScrollEnd} > {data.map((item, index) => diff --git a/src/base/picker/Picker.tsx b/src/base/picker/Picker.tsx index 772b579..8c54c9b 100644 --- a/src/base/picker/Picker.tsx +++ b/src/base/picker/Picker.tsx @@ -51,6 +51,9 @@ export type PickerProps> = { contentContainerStyle?: StyleProp; scrollEventThrottle?: number; + + _onScrollStart?: () => void; + _onScrollEnd?: () => void; }; const defaultKeyExtractor: KeyExtractor = (_, index) => index.toString(); @@ -104,6 +107,9 @@ const Picker = >({ itemTextStyle, overlayItemStyle, contentContainerStyle, + + _onScrollStart, + _onScrollEnd, ...restProps }: PickerProps) => { const valueIndex = useValueIndex(data, value); @@ -160,6 +166,8 @@ const Picker = >({ }); const onScrollEnd = useStableCallback(() => { + // consistency matters + _onScrollEnd?.(); onScrollEndForValueEvents(); onScrollEndForSyncScroll(); }); @@ -186,6 +194,7 @@ const Picker = >({ onTouchStart: touching.setTrue, onTouchEnd: touching.setFalse, onTouchCancel: touching.setFalse, + onScrollStart: _onScrollStart, onScrollEnd, contentContainerStyle, })} diff --git a/src/base/types.ts b/src/base/types.ts index 29463eb..dbe56de 100644 --- a/src/base/types.ts +++ b/src/base/types.ts @@ -59,6 +59,7 @@ export type RenderListProps> = { onTouchStart: () => void; onTouchEnd: () => void; onTouchCancel: () => void; + onScrollStart: (() => void) | undefined; onScrollEnd: () => void; contentContainerStyle: StyleProp | undefined; } & Record; diff --git a/src/hoc/virtualized/VirtualizedList.tsx b/src/hoc/virtualized/VirtualizedList.tsx index 0c233ea..f254f23 100644 --- a/src/hoc/virtualized/VirtualizedList.tsx +++ b/src/hoc/virtualized/VirtualizedList.tsx @@ -14,7 +14,7 @@ import { StyleSheet, type ViewStyle, } from 'react-native'; -import {withScrollEndEvent} from '@utils/scrolling'; +import {withScrollStartEndEvent} from '@utils/scrolling'; import type { KeyExtractor, ListMethods, @@ -23,7 +23,9 @@ import type { } from '../../base/types'; // TODO "any" is not an exact type. How to pass the generic type? -const ExtendedAnimatedFlatList = withScrollEndEvent(Animated.FlatList); +const ExtendedAnimatedFlatList = withScrollStartEndEvent( + Animated.FlatList, +); export type AdditionalProps = Pick< FlatListProps, @@ -46,6 +48,7 @@ type VirtualizedListProps> = { onTouchStart: () => void; onTouchEnd: () => void; onTouchCancel: () => void; + onScrollStart: (() => void) | undefined; onScrollEnd: () => void; contentContainerStyle: StyleProp | undefined; } & AdditionalProps; @@ -64,6 +67,7 @@ const VirtualizedList = >( onTouchEnd, onTouchStart, onTouchCancel, + onScrollStart, onScrollEnd, contentContainerStyle: contentContainerStyleProp, @@ -126,6 +130,7 @@ const VirtualizedList = >( onTouchStart={onTouchStart} onTouchEnd={onTouchEnd} onTouchCancel={onTouchCancel} + onScrollStart={onScrollStart} onScrollEnd={onScrollEnd} initialNumToRender={initialNumToRender ?? Math.ceil(visibleItemCount / 2)} maxToRenderPerBatch={ diff --git a/src/utils/scrolling/index.ts b/src/utils/scrolling/index.ts index 5095174..17ad697 100644 --- a/src/utils/scrolling/index.ts +++ b/src/utils/scrolling/index.ts @@ -1,2 +1,2 @@ -export {default as withScrollEndEvent} from './withScrollEndEvent'; +export {default as withScrollStartEndEvent} from './withScrollStartEndEvent'; export {getPageIndex} from './getPageIndex'; diff --git a/src/utils/scrolling/withScrollEndEvent.tsx b/src/utils/scrolling/withScrollStartEndEvent.tsx similarity index 54% rename from src/utils/scrolling/withScrollEndEvent.tsx rename to src/utils/scrolling/withScrollStartEndEvent.tsx index 84febb5..26f6254 100644 --- a/src/utils/scrolling/withScrollEndEvent.tsx +++ b/src/utils/scrolling/withScrollStartEndEvent.tsx @@ -4,9 +4,9 @@ import React, { type ForwardedRef, forwardRef, memo, - useCallback, useEffect, useMemo, + useRef, } from 'react'; import type { Animated, @@ -14,6 +14,7 @@ import type { NativeSyntheticEvent, ScrollViewProps, } from 'react-native'; +import {useStableCallback} from '@rozhkov/react-useful-hooks'; import debounce from '@utils/debounce'; type ComponentProps = Pick< @@ -26,15 +27,18 @@ type ComponentProps = Pick< type ExtendProps = PropsT & { scrollOffset: Animated.Value; + onScrollStart?: () => void; onScrollEnd?: () => void; }; -const withScrollEndEvent = ( +const withScrollStartEndEvent = ( Component: ComponentType, ) => { const Wrapper = ( { - onScrollEnd: onScrollEndProp = () => {}, + onScrollStart: onScrollStartProp, + onScrollEnd: onScrollEndProp, + onScrollBeginDrag: onScrollBeginDragProp, onScrollEndDrag: onScrollEndDragProp, onMomentumScrollBegin: onMomentumScrollBeginProp, onMomentumScrollEnd: onMomentumScrollEndProp, @@ -43,48 +47,74 @@ const withScrollEndEvent = ( }: ExtendProps, forwardedRef: ForwardedRef>>, ) => { + const onScrollStartStable = useStableCallback(onScrollStartProp); + + const isOnScrollStartCalledRef = useRef(false); + const deactivateOnScrollStart = useStableCallback(() => { + isOnScrollStartCalledRef.current = false; + }); + const maybeCallOnScrollStart = useStableCallback(() => { + if (!isOnScrollStartCalledRef.current) { + onScrollStartStable(); + isOnScrollStartCalledRef.current = true; + } + }); + + const onScrollEndStable = useStableCallback(() => { + maybeCallOnScrollStart(); + onScrollEndProp?.(); + deactivateOnScrollStart(); + }); + const onScrollEnd = useMemo( - () => debounce(onScrollEndProp, 0), // A small delay is needed so that onScrollEnd doesn't trigger prematurely. - [onScrollEndProp], + () => debounce(onScrollEndStable, 0), // A small delay is needed so that onScrollEnd doesn't trigger prematurely. + [onScrollEndStable], + ); + + const onScrollBeginDrag = useStableCallback( + (args: NativeSyntheticEvent) => { + maybeCallOnScrollStart(); + onScrollBeginDragProp?.(args); + }, ); - const onScrollEndDrag = useCallback( + const onScrollEndDrag = useStableCallback( (args: NativeSyntheticEvent) => { onScrollEndDragProp?.(args); onScrollEnd(); }, - [onScrollEnd, onScrollEndDragProp], ); - const onMomentumScrollBegin = useCallback( + const onMomentumScrollBegin = useStableCallback( (args: NativeSyntheticEvent) => { + maybeCallOnScrollStart(); onScrollEnd.clear(); onMomentumScrollBeginProp?.(args); }, - [onScrollEnd, onMomentumScrollBeginProp], ); - const onMomentumScrollEnd = useCallback( + const onMomentumScrollEnd = useStableCallback( (args: NativeSyntheticEvent) => { onMomentumScrollEndProp?.(args); onScrollEnd(); }, - [onScrollEnd, onMomentumScrollEndProp], ); useEffect(() => { const sub = scrollOffset.addListener(() => { + maybeCallOnScrollStart(); onScrollEnd.clear(); }); return () => { scrollOffset.removeListener(sub); }; - }, [onScrollEnd, scrollOffset]); + }, [maybeCallOnScrollStart, onScrollEnd, scrollOffset]); return ( ( ); }; - Wrapper.displayName = `withScrollEndEvent(${ + Wrapper.displayName = `withScrollStartEndEvent(${ Component.displayName || 'Component' })`; @@ -103,4 +133,4 @@ const withScrollEndEvent = ( ); }; -export default withScrollEndEvent; +export default withScrollStartEndEvent; From 6b7e22caa851ffa08ad01974a20a6c134ed26fbc Mon Sep 17 00:00:00 2001 From: rozhkovs Date: Fri, 18 Jul 2025 22:40:03 +0700 Subject: [PATCH 5/5] feat: base implementation --- example/package.json | 6 + example/src/App.tsx | 37 +- .../SimplePickerBlockExample.tsx | 42 -- .../src/components/example-blocks/index.ts | 3 - example/src/picker-config/index.ts | 3 - .../picker-config/withExamplePickerConfig.tsx | 63 -- example/src/snack/contants.ts | 2 + example/src/snack/get-random-number.ts | 7 + example/src/snack/index.ts | 2 + .../NativeFeedbackProvider.tsx | 38 + .../src/snack/native-api-provider/index.ts | 4 + example/src/snack/navigator/RootNavigator.tsx | 22 + example/src/snack/navigator/index.ts | 1 + .../PickerPropsChangerPanel.tsx} | 80 ++- .../PickerPropsChangerProvider.tsx} | 28 +- example/src/snack/props-changer/index.ts | 3 + .../snack/props-changer/withPropsChanger.tsx | 82 +++ .../ControlSimpleUsage.tsx | 145 ++++ .../screens/control-simple-usage/index.ts | 1 + .../CustomizedPickerScreen.tsx} | 58 +- .../screens/customized-picker}/Overlay.tsx | 0 .../screens/customized-picker}/PickerItem.tsx | 0 .../PickerItemContainer.tsx | 0 .../snack/screens/customized-picker/index.ts | 1 + .../screens/customized-picker}/types.ts | 0 example/src/snack/screens/index.ts | 0 example/src/snack/screens/main/MainScreen.tsx | 46 ++ example/src/snack/screens/main/index.ts | 1 + .../SimpleDatePickerScreen.tsx | 77 ++ .../snack/screens/simple-date-picker/index.ts | 1 + .../SimplePickerAndIOSPickerScreen.tsx} | 31 +- .../simple-picker-and-ios-picker/index.ts | 1 + .../simple-picker/SimplePickerScreen.tsx | 63 ++ .../src/snack/screens/simple-picker/index.ts | 1 + .../base => snack/ui-base}/Box.tsx | 0 .../base => snack/ui-base}/Divider.tsx | 0 .../base => snack/ui-base}/Header.tsx | 0 .../ui-base}/ListItemCheckBox.tsx | 2 +- .../ui-base/PickerScreenViewContainer.tsx | 21 + .../base => snack/ui-base}/index.ts | 1 + example/yarn.lock | 667 +++++++++++++++++- local-namespace-config.js | 1 + package.json | 4 +- src/base/index.ts | 2 +- src/base/picker/Picker.tsx | 17 +- src/base/picker/hooks/useSyncScrollEffect.ts | 12 +- src/base/types.ts | 8 +- src/date/DatePicker.tsx | 100 +++ src/date/DatePickerCommonPropsProvider.tsx | 113 +++ src/date/DatePickerContainer.tsx | 45 ++ src/date/DatePickerDate.tsx | 51 ++ src/date/DatePickerLocaleProvider.tsx | 51 ++ src/date/DatePickerMonth.tsx | 51 ++ src/date/DatePickerValueProvider.tsx | 121 ++++ src/date/DatePickerYear.tsx | 51 ++ src/date/date.ts | 115 +++ src/date/index.ts | 5 + src/date/useOverlayItemStyle.ts | 37 + src/index.tsx | 14 + src/picker-control/create-control.ts | 262 +++++++ src/picker-control/index.ts | 9 + src/picker-control/usePickerControl.ts | 48 ++ .../usePickerControlSubscriber.ts | 92 +++ src/picker-control/withPickerControl.tsx | 121 ++++ .../scrolling/withScrollStartEndEvent.tsx | 7 +- tsconfig.json | 1 + yarn.lock | 16 + 67 files changed, 2662 insertions(+), 232 deletions(-) delete mode 100644 example/src/components/example-blocks/SimplePickerBlockExample.tsx delete mode 100644 example/src/components/example-blocks/index.ts delete mode 100644 example/src/picker-config/index.ts delete mode 100644 example/src/picker-config/withExamplePickerConfig.tsx create mode 100644 example/src/snack/contants.ts create mode 100644 example/src/snack/get-random-number.ts create mode 100644 example/src/snack/index.ts create mode 100644 example/src/snack/native-api-provider/NativeFeedbackProvider.tsx create mode 100644 example/src/snack/native-api-provider/index.ts create mode 100644 example/src/snack/navigator/RootNavigator.tsx create mode 100644 example/src/snack/navigator/index.ts rename example/src/{picker-config/PickerConfigPanel.tsx => snack/props-changer/PickerPropsChangerPanel.tsx} (60%) rename example/src/{picker-config/PickerConfigProvider.tsx => snack/props-changer/PickerPropsChangerProvider.tsx} (74%) create mode 100644 example/src/snack/props-changer/index.ts create mode 100644 example/src/snack/props-changer/withPropsChanger.tsx create mode 100644 example/src/snack/screens/control-simple-usage/ControlSimpleUsage.tsx create mode 100644 example/src/snack/screens/control-simple-usage/index.ts rename example/src/{components/example-blocks/AvatarCustomizedPickerBlockExample/index.tsx => snack/screens/customized-picker/CustomizedPickerScreen.tsx} (60%) rename example/src/{components/example-blocks/AvatarCustomizedPickerBlockExample => snack/screens/customized-picker}/Overlay.tsx (100%) rename example/src/{components/example-blocks/AvatarCustomizedPickerBlockExample => snack/screens/customized-picker}/PickerItem.tsx (100%) rename example/src/{components/example-blocks/AvatarCustomizedPickerBlockExample => snack/screens/customized-picker}/PickerItemContainer.tsx (100%) create mode 100644 example/src/snack/screens/customized-picker/index.ts rename example/src/{components/example-blocks/AvatarCustomizedPickerBlockExample => snack/screens/customized-picker}/types.ts (100%) create mode 100644 example/src/snack/screens/index.ts create mode 100644 example/src/snack/screens/main/MainScreen.tsx create mode 100644 example/src/snack/screens/main/index.ts create mode 100644 example/src/snack/screens/simple-date-picker/SimpleDatePickerScreen.tsx create mode 100644 example/src/snack/screens/simple-date-picker/index.ts rename example/src/{components/example-blocks/CompareWithNativeIOSBlockExample.tsx => snack/screens/simple-picker-and-ios-picker/SimplePickerAndIOSPickerScreen.tsx} (82%) create mode 100644 example/src/snack/screens/simple-picker-and-ios-picker/index.ts create mode 100644 example/src/snack/screens/simple-picker/SimplePickerScreen.tsx create mode 100644 example/src/snack/screens/simple-picker/index.ts rename example/src/{components/base => snack/ui-base}/Box.tsx (100%) rename example/src/{components/base => snack/ui-base}/Divider.tsx (100%) rename example/src/{components/base => snack/ui-base}/Header.tsx (100%) rename example/src/{components/base => snack/ui-base}/ListItemCheckBox.tsx (98%) create mode 100644 example/src/snack/ui-base/PickerScreenViewContainer.tsx rename example/src/{components/base => snack/ui-base}/index.ts (70%) create mode 100644 src/date/DatePicker.tsx create mode 100644 src/date/DatePickerCommonPropsProvider.tsx create mode 100644 src/date/DatePickerContainer.tsx create mode 100644 src/date/DatePickerDate.tsx create mode 100644 src/date/DatePickerLocaleProvider.tsx create mode 100644 src/date/DatePickerMonth.tsx create mode 100644 src/date/DatePickerValueProvider.tsx create mode 100644 src/date/DatePickerYear.tsx create mode 100644 src/date/date.ts create mode 100644 src/date/index.ts create mode 100644 src/date/useOverlayItemStyle.ts create mode 100644 src/picker-control/create-control.ts create mode 100644 src/picker-control/index.ts create mode 100644 src/picker-control/usePickerControl.ts create mode 100644 src/picker-control/usePickerControlSubscriber.ts create mode 100644 src/picker-control/withPickerControl.tsx diff --git a/example/package.json b/example/package.json index a8c5fca..cea5fac 100644 --- a/example/package.json +++ b/example/package.json @@ -12,6 +12,9 @@ "@faker-js/faker": "^8.0.2", "@quidone/react-native-wheel-picker-feedback": "^2.0.0", "@react-native-picker/picker": "2.9.0", + "@react-navigation/elements": "^2.5.2", + "@react-navigation/native": "^7.1.14", + "@react-navigation/native-stack": "^7.3.21", "@rozhkov/react-useful-hooks": "^1.0.9", "expo": "^52.0.41", "expo-splash-screen": "~0.29.22", @@ -21,7 +24,10 @@ "react-native": "0.76.7", "react-native-builder-bob": "^0.38.4", "react-native-elements": "^4.0.0-rc.2", + "react-native-gesture-handler": "~2.20.2", + "react-native-reanimated": "~3.16.1", "react-native-safe-area-context": "4.12.0", + "react-native-screens": "~4.4.0", "react-native-web": "~0.19.10" }, "devDependencies": { diff --git a/example/src/App.tsx b/example/src/App.tsx index 2ddc59c..1c88765 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,38 +1,13 @@ import * as React from 'react'; -import {ScrollView, StyleSheet} from 'react-native'; -import {PickerConfigProvider} from './picker-config'; -import {Box} from './components/base'; -import { - AvatarCustomizedPickerBlockExample, - CompareWithNativeIOSBlockExample, - SimplePickerBlockExample, -} from './components/example-blocks'; +import WheelPickerFeedback from '@quidone/react-native-wheel-picker-feedback'; +import {NativeFeedbackProvider, RootNavigation} from './snack'; const App = () => { return ( - - - - - - + + + ); }; -const styles = StyleSheet.create({ - contentContainer: { - paddingVertical: 60, - paddingHorizontal: 20, - alignItems: 'center', - }, -}); - -const Providers = () => { - return ( - - - - ); -}; - -export default Providers; +export default App; diff --git a/example/src/components/example-blocks/SimplePickerBlockExample.tsx b/example/src/components/example-blocks/SimplePickerBlockExample.tsx deleted file mode 100644 index ec1d066..0000000 --- a/example/src/components/example-blocks/SimplePickerBlockExample.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React, {useCallback, useState} from 'react'; -import WheelPicker, { - PickerItem, - type ValueChangedEvent, -} from '@quidone/react-native-wheel-picker'; -import {useInit} from '@rozhkov/react-useful-hooks'; -import {withExamplePickerConfig, PickerConfigPanel} from '../../picker-config'; -import {Header} from '../base'; - -const ExampleWheelPicker = withExamplePickerConfig(WheelPicker); -const createPickerItem = (index: number): PickerItem => ({ - value: index, - label: index.toString(), -}); - -const SimplePicker = () => { - const data = useInit(() => [...Array(100).keys()].map(createPickerItem)); - const [value, setValue] = useState(0); - - const onValueChanged = useCallback( - ({item: {value: val}}: ValueChangedEvent>) => { - setValue(val); - }, - [], - ); - - return ( - <> -
- - - - ); -}; - -export default SimplePicker; diff --git a/example/src/components/example-blocks/index.ts b/example/src/components/example-blocks/index.ts deleted file mode 100644 index f60bb39..0000000 --- a/example/src/components/example-blocks/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export {default as SimplePickerBlockExample} from './SimplePickerBlockExample'; -export {default as AvatarCustomizedPickerBlockExample} from './AvatarCustomizedPickerBlockExample'; -export {default as CompareWithNativeIOSBlockExample} from './CompareWithNativeIOSBlockExample'; diff --git a/example/src/picker-config/index.ts b/example/src/picker-config/index.ts deleted file mode 100644 index 95cd215..0000000 --- a/example/src/picker-config/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export {default as PickerConfigProvider} from './PickerConfigProvider'; -export {default as withExamplePickerConfig} from './withExamplePickerConfig'; -export {default as PickerConfigPanel} from './PickerConfigPanel'; diff --git a/example/src/picker-config/withExamplePickerConfig.tsx b/example/src/picker-config/withExamplePickerConfig.tsx deleted file mode 100644 index 1dfd91c..0000000 --- a/example/src/picker-config/withExamplePickerConfig.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, {FC, memo, useCallback, useMemo} from 'react'; -import type { - default as WheelPicker, - OnValueChanging, - WheelPickerProps, -} from '@quidone/react-native-wheel-picker'; -import {usePickerConfig} from './PickerConfigProvider'; -import WheelPickerFeedback from '@quidone/react-native-wheel-picker-feedback'; -import {withVirtualized} from '@quidone/react-native-wheel-picker'; - -const useCallFeedback = () => { - const {enabledSound, enabledImpact} = usePickerConfig(); - return useMemo(() => { - switch (true) { - case enabledSound && enabledImpact: return WheelPickerFeedback.triggerSoundAndImpact; // eslint-disable-line prettier/prettier - case enabledSound: return WheelPickerFeedback.triggerSound; // eslint-disable-line prettier/prettier - case enabledImpact: return WheelPickerFeedback.triggerImpact; // eslint-disable-line prettier/prettier - default: return () => {}; // eslint-disable-line prettier/prettier - } - }, [enabledImpact, enabledSound]); -}; - -const withExamplePickerConfig = ( - WrappedComponent: FC>, -) => { - const Wrapper = ({ - onValueChanging: onValueChangingProp, - ...restProps - }: WheelPickerProps) => { - const {enabledVirtualized, readOnly, visibleItemCount} = usePickerConfig(); - const callFeedback = useCallFeedback(); - - const onValueChanging = useCallback>( - (...args) => { - callFeedback(); - onValueChangingProp?.(...args); - }, - [callFeedback, onValueChangingProp], - ); - - const ResultComponent = useMemo(() => { - if (!enabledVirtualized) { - return WrappedComponent; - } - return withVirtualized(WrappedComponent as any); - }, [enabledVirtualized]); - - return ( - - ); - }; - - Wrapper.displayName = `withExamplePickerConfig(${WrappedComponent.displayName})`; - - return memo(Wrapper) as typeof WheelPicker; -}; - -export default withExamplePickerConfig; diff --git a/example/src/snack/contants.ts b/example/src/snack/contants.ts new file mode 100644 index 0000000..77b671a --- /dev/null +++ b/example/src/snack/contants.ts @@ -0,0 +1,2 @@ +export const WP_FEEDBACK_GITHUB_URL = + 'https://github.com/quidone/react-native-wheel-picker-feedback'; diff --git a/example/src/snack/get-random-number.ts b/example/src/snack/get-random-number.ts new file mode 100644 index 0000000..7424cd6 --- /dev/null +++ b/example/src/snack/get-random-number.ts @@ -0,0 +1,7 @@ +export const getRandomNumber = (currentValue: number, dataCount: number) => { + let randomValue; + do { + randomValue = Math.floor(Math.random() * dataCount); + } while (randomValue === currentValue); + return randomValue; +}; diff --git a/example/src/snack/index.ts b/example/src/snack/index.ts new file mode 100644 index 0000000..4836a61 --- /dev/null +++ b/example/src/snack/index.ts @@ -0,0 +1,2 @@ +export {RootNavigation} from './navigator'; +export {NativeFeedbackProvider} from './native-api-provider'; diff --git a/example/src/snack/native-api-provider/NativeFeedbackProvider.tsx b/example/src/snack/native-api-provider/NativeFeedbackProvider.tsx new file mode 100644 index 0000000..5de27ac --- /dev/null +++ b/example/src/snack/native-api-provider/NativeFeedbackProvider.tsx @@ -0,0 +1,38 @@ +import React, {createContext, type PropsWithChildren, useContext} from 'react'; + +const NativeFeedbackModuleContext = createContext< + NativeFeedbackModule | 'NOT_SUPPORT_IN_SNACK' +>('NOT_SUPPORT_IN_SNACK'); + +type NativeFeedbackModule = { + triggerImpact: () => void; + triggerSound: () => void; + triggerSoundAndImpact: () => void; +}; + +type NativeFeedbackProviderProps = PropsWithChildren<{ + module: NativeFeedbackModule | 'NOT_SUPPORT_IN_SNACK'; +}>; + +const NativeFeedbackProvider = ({ + module, + children, +}: NativeFeedbackProviderProps) => { + return ( + + {children} + + ); +}; + +export default NativeFeedbackProvider; + +export const useNativeFeedbackModule = () => { + const value = useContext(NativeFeedbackModuleContext); + if (value === undefined) { + throw new Error( + 'useNativeFeedbackModule must be called from within NativeFeedbackProvider!', + ); + } + return useContext(NativeFeedbackModuleContext)!; +}; diff --git a/example/src/snack/native-api-provider/index.ts b/example/src/snack/native-api-provider/index.ts new file mode 100644 index 0000000..9ba76a6 --- /dev/null +++ b/example/src/snack/native-api-provider/index.ts @@ -0,0 +1,4 @@ +export { + default as NativeFeedbackProvider, + useNativeFeedbackModule, +} from './NativeFeedbackProvider'; diff --git a/example/src/snack/navigator/RootNavigator.tsx b/example/src/snack/navigator/RootNavigator.tsx new file mode 100644 index 0000000..dfddde2 --- /dev/null +++ b/example/src/snack/navigator/RootNavigator.tsx @@ -0,0 +1,22 @@ +import {createStaticNavigation} from '@react-navigation/native'; +import {createNativeStackNavigator} from '@react-navigation/native-stack'; +import {SimplePickerScreen} from '../screens/simple-picker'; +import {MainScreen} from '../screens/main'; +import {SimplePickerAndIOSPickerScreen} from '../screens/simple-picker-and-ios-picker'; +import {CustomizedPickerScreen} from '../screens/customized-picker'; +import {SimpleDatePickerScreen} from '../screens/simple-date-picker'; +import {ControlSimpleUsage} from '../screens/control-simple-usage'; + +// @ts-ignore +const RootStackNavigator = createNativeStackNavigator({ + screens: { + Main: MainScreen, + SimplePicker: SimplePickerScreen, + SimplePickerAndIOSPicker: SimplePickerAndIOSPickerScreen, + CustomizedPicker: CustomizedPickerScreen, + SimpleDatePicker: SimpleDatePickerScreen, + ControlSimpleUsage: ControlSimpleUsage, + }, +}); + +export const RootNavigation = createStaticNavigation(RootStackNavigator); diff --git a/example/src/snack/navigator/index.ts b/example/src/snack/navigator/index.ts new file mode 100644 index 0000000..7d199b8 --- /dev/null +++ b/example/src/snack/navigator/index.ts @@ -0,0 +1 @@ +export {RootNavigation} from './RootNavigator'; diff --git a/example/src/picker-config/PickerConfigPanel.tsx b/example/src/snack/props-changer/PickerPropsChangerPanel.tsx similarity index 60% rename from example/src/picker-config/PickerConfigPanel.tsx rename to example/src/snack/props-changer/PickerPropsChangerPanel.tsx index 21a88d7..35fdf6f 100644 --- a/example/src/picker-config/PickerConfigPanel.tsx +++ b/example/src/snack/props-changer/PickerPropsChangerPanel.tsx @@ -1,54 +1,84 @@ import React, {memo} from 'react'; import {Linking, StyleSheet, Text, TouchableOpacity, View} from 'react-native'; -import {ListItemCheckBox, Divider} from '../components/base'; -import {usePickerConfig} from './PickerConfigProvider'; -import {WP_FEEDBACK_GITHUB_URL} from '../contants'; +import {ListItemCheckBox, Divider} from '../ui-base'; import {ButtonGroup} from 'react-native-elements'; import {useInit} from '@rozhkov/react-useful-hooks'; +import {usePickerPropsChanger} from './PickerPropsChangerProvider'; +import {WP_FEEDBACK_GITHUB_URL} from '../contants'; + +type PickerPropsChangerPanelProps = { + hideSound?: boolean; + hideImpact?: boolean; + hideVirtualized?: boolean; +}; -const PickerConfigPanel = () => { +const PickerPropsChangerPanel = ({ + hideImpact, + hideSound, + hideVirtualized, +}: PickerPropsChangerPanelProps) => { const { enabledVirtualized, enabledSound, enabledImpact, readOnly, + enableScrollByTapOnItem, visibleItemCount, toggleVirtualized, toggleSound, toggleImpact, toggleReadOnly, + toggleScrollByTapOnItem, changeVisibleItemCount, - } = usePickerConfig(); + } = usePickerPropsChanger(); const visibleItemCounts = useInit(() => ['1', '3', '5', '7', '9']); return ( - - - - - - + {!hideSound && ( + <> + + + + )} + {!hideImpact && ( + <> + + + + )} + {!hideVirtualized && ( + <> + + + + )} + + Visible Count: void; toggleImpact: () => void; toggleVirtualized: () => void; + toggleScrollByTapOnItem: () => void; toggleReadOnly: () => void; changeVisibleItemCount: (count: number) => void; } & PickerConfig; @@ -34,23 +36,26 @@ const alertNotAvailableFeedback = () => { ); }; -const PickerConfigProvider = ({children}: PropsWithChildren) => { +const PickerPropsChangerProvider = ({children}: PropsWithChildren) => { + const nativeFeedbackModule = useNativeFeedbackModule(); + const [config, setConfig] = useState(() => ({ enabledSound: false, enabledImpact: false, enabledVirtualized: false, + enableScrollByTapOnItem: true, readOnly: false, visibleItemCount: 5, })); const toggleSound = useStableCallback(() => { - if (IS_EXPO_SNACK) { + if (nativeFeedbackModule === 'NOT_SUPPORT_IN_SNACK') { alertNotAvailableFeedback(); return; } setConfig((prev) => ({...prev, enabledSound: !prev.enabledSound})); }); const toggleImpact = useStableCallback(() => { - if (IS_EXPO_SNACK) { + if (nativeFeedbackModule === 'NOT_SUPPORT_IN_SNACK') { alertNotAvailableFeedback(); return; } @@ -68,6 +73,12 @@ const PickerConfigProvider = ({children}: PropsWithChildren) => { readOnly: !prev.readOnly, })); }); + const toggleScrollByTapOnItem = useStableCallback(() => { + setConfig((prev) => ({ + ...prev, + enableScrollByTapOnItem: !prev.enableScrollByTapOnItem, + })); + }); const changeVisibleItemCount = useStableCallback( (count: 1 | 3 | 5 | 7 | 9 | number) => { setConfig((prev) => ({ @@ -82,6 +93,7 @@ const PickerConfigProvider = ({children}: PropsWithChildren) => { toggleSound, toggleImpact, toggleVirtualized, + toggleScrollByTapOnItem, toggleReadOnly, changeVisibleItemCount, }); @@ -89,13 +101,13 @@ const PickerConfigProvider = ({children}: PropsWithChildren) => { return {children}; }; -export default PickerConfigProvider; +export default PickerPropsChangerProvider; -export const usePickerConfig = () => { +export const usePickerPropsChanger = () => { const value = useContext(Context); if (value === undefined) { throw new Error( - `usePickerConfig must be called from within PickerConfigProvider!`, + `usePickerPropsChanger must be called from within PickerPropsChangerProvider!`, ); } return value; diff --git a/example/src/snack/props-changer/index.ts b/example/src/snack/props-changer/index.ts new file mode 100644 index 0000000..bad27ae --- /dev/null +++ b/example/src/snack/props-changer/index.ts @@ -0,0 +1,3 @@ +export {default as withPropsChanger} from './withPropsChanger'; +export {default as PickerPropsChangerPanel} from './PickerPropsChangerPanel'; +export {default as PickerPropsChangerProvider} from './PickerPropsChangerProvider'; diff --git a/example/src/snack/props-changer/withPropsChanger.tsx b/example/src/snack/props-changer/withPropsChanger.tsx new file mode 100644 index 0000000..cb8c538 --- /dev/null +++ b/example/src/snack/props-changer/withPropsChanger.tsx @@ -0,0 +1,82 @@ +import React, {FC, memo, useCallback, useMemo} from 'react'; +import type { + OnValueChanging, + WheelPickerProps, +} from '@quidone/react-native-wheel-picker'; +import {withVirtualized} from '@quidone/react-native-wheel-picker'; +import {usePickerPropsChanger} from './PickerPropsChangerProvider'; +import {useNativeFeedbackModule} from '../native-api-provider'; + +const useCallFeedback = () => { + const nativeFeedbackModule = useNativeFeedbackModule(); + const {enabledSound, enabledImpact} = usePickerPropsChanger(); + return useMemo(() => { + if (nativeFeedbackModule === 'NOT_SUPPORT_IN_SNACK') { + return () => {}; + } + switch (true) { + case enabledSound && enabledImpact: return nativeFeedbackModule.triggerSoundAndImpact; // eslint-disable-line prettier/prettier + case enabledSound: return nativeFeedbackModule.triggerSound; // eslint-disable-line prettier/prettier + case enabledImpact: return nativeFeedbackModule.triggerImpact; // eslint-disable-line prettier/prettier + default: return () => {}; // eslint-disable-line prettier/prettier + } + }, [enabledImpact, enabledSound, nativeFeedbackModule]); +}; + +const withPropsChanger = < + PropsT extends Pick< + WheelPickerProps, + | 'enableScrollByTapOnItem' + | 'visibleItemCount' + | 'readOnly' + | 'onValueChanged' + | 'onValueChanging' + >, +>( + AnyPicker: FC, +) => { + const WrappedPicker = ({ + onValueChanging: onValueChangingProp, + ...restProps + }: PropsT) => { + const { + enabledVirtualized, + readOnly, + visibleItemCount, + enableScrollByTapOnItem, + } = usePickerPropsChanger(); + const callFeedback = useCallFeedback(); + + const onValueChanging = useCallback>( + (...args) => { + callFeedback(); + onValueChangingProp?.(...args); + }, + [callFeedback, onValueChangingProp], + ); + + const ResultComponent = useMemo(() => { + if (!enabledVirtualized) { + return AnyPicker; + } + return withVirtualized(AnyPicker as any); + }, [enabledVirtualized]); + + return ( + // @ts-ignore + + ); + }; + + WrappedPicker.displayName = `withPropsChanger(${AnyPicker.displayName})`; + + return memo(WrappedPicker) as unknown as typeof AnyPicker; +}; + +export default withPropsChanger; diff --git a/example/src/snack/screens/control-simple-usage/ControlSimpleUsage.tsx b/example/src/snack/screens/control-simple-usage/ControlSimpleUsage.tsx new file mode 100644 index 0000000..31ede61 --- /dev/null +++ b/example/src/snack/screens/control-simple-usage/ControlSimpleUsage.tsx @@ -0,0 +1,145 @@ +import React, {memo, useMemo, useState} from 'react'; +import {StyleSheet, View} from 'react-native'; +import WheelPicker, { + type PickerItem, + useOnPickerValueChangedEffect, + useOnPickerValueChangingEffect, + usePickerControl, + withPickerControl, +} from '@quidone/react-native-wheel-picker'; +import {Button, Text} from 'react-native-elements'; +import {getRandomNumber} from '../../get-random-number'; + +const PickerWithControl = withPickerControl(WheelPicker); + +const generateNumbersArray = (length: number): {value: number}[] => { + return Array.from({length: length}, (_, index) => ({value: index})); +}; + +type ControlPickersMap = { + value1: {item: PickerItem}; + value2: {item: PickerItem}; +}; + +const ControlSimpleUsage = () => { + const [dataCount, setDataCount] = useState(100); + const data = useMemo(() => generateNumbersArray(dataCount), [dataCount]); + const [value, setValue] = useState({value1: 0, value2: 0}); + + const pickerControl = usePickerControl(); + + useOnPickerValueChangedEffect(pickerControl, (event) => { + console.log('[useOnPickerValueChangedEffect]', event); + + const nextValue = event.pickers.reduce((r, n) => { + r[n.name] = n.item.value; + return r; + }, {} as typeof value); + + setValue(nextValue); + }); + + useOnPickerValueChangingEffect(pickerControl, (event) => { + const curValue = event.pickers.reduce((r, n) => { + r[n.name] = n.item.value; + return r; + }, {} as typeof value); + + console.log('[useOnPickerValueChangingEffect]', curValue, event); + }); + + // Обработчики нажатия на кнопки + const handleRandomValue1 = () => { + const randomValue1 = getRandomNumber(value.value1, dataCount); + setValue({...value, value1: randomValue1}); + }; + + const handleRandomValue2 = () => { + const randomValue2 = getRandomNumber(value.value2, dataCount); + setValue({...value, value2: randomValue2}); + }; + + const handleRandomBothValues = () => { + const randomValue1 = getRandomNumber(value.value1, dataCount); + const randomValue2 = getRandomNumber(value.value2, dataCount); + setValue({value1: randomValue1, value2: randomValue2}); + }; + + const changeDataCount = (count: number) => { + setDataCount(count); + setValue({value1: 0, value2: 0}); + }; + + // console.log('--- [ControlSimpleUsage] [value] --- ', value); + + return ( + + + + + + + value: {value.value1}:{value.value2} + + + {/* Добавленные кнопки для случайных значений */} + +