diff --git a/README.md b/README.md index a5441af..a339e46 100644 --- a/README.md +++ b/README.md @@ -42,14 +42,14 @@ A flexible React Native Wheel Picker for iOS and Android without using the nativ ## Features - - Without native side. - - Unified API. - - Only native animations. - - [Support native feedback](#Native-Feedback). - - [Support virtualization](#withVirtualized). - - Compatible with Expo ([Snack][EXPO_SNACK]). - - Deep customization - - Written ```TypeScript```. +- Without native side. +- Unified API. +- Only native animations. +- [Support native feedback](#Native-Feedback). +- [Support virtualization](#withVirtualized). +- Compatible with Expo ([Snack][EXPO_SNACK]). +- Deep customization +- Written ```TypeScript```. ## Installation ```shell @@ -62,9 +62,15 @@ yarn add @quidone/react-native-wheel-picker - [Native Feedback](#Native-Feedback) - [API](#API) - [WheelPicker](#WheelPicker) - - [usePickerItemHeight](#usePickerItemHeight) - - [useScrollContentOffset](#useScrollContentOffset) - - [withVirtualized](#withVirtualized) + - [usePickerItemHeight](#usePickerItemHeight) + - [useScrollContentOffset](#useScrollContentOffset) + - [withVirtualized](#withVirtualized) + - [DatePicker (Beta)](#DatePicker) + - [Picker Control (Beta)](#Picker-Control) + - [usePickerControl](#usePickerControl) + - [withPickerControl](#withPickerControl) + - [useOnPickerValueChangedEffect](#useOnPickerValueChangedEffect) + - [useOnPickerValueChangingEffect](#useOnPickerValueChangingEffect) - [Footer](#-Author) ## Usage @@ -80,7 +86,7 @@ yarn install cd example && yarn install && yarn ios ``` -### Simple case +### WheelPicker usage ```jsx import React, {useState} from 'react'; @@ -106,6 +112,123 @@ const App = () => { export default App; ``` +### DatePicker usage (Beta) + +#### Simple case +```tsx +import React, {useState} from 'react'; +import {DatePicker} from '@quidone/react-native-wheel-picker'; + +const App = () => { + const [date, setDate] = useState('2025-02-02'); + + return ( + setDate(date)} + /> + ); +}; +``` + +#### Customized case +You also have a lot of control over each WheelPicker and the rendering process; +you can add your own components between individual WheelPickers + +> ⚠️ **Warning:** It is recommended to test the component in a release build of your application. +> There is an issue where synchronization of scrolling may occasionally slip during scrolling +> attempts when performance is low. + +```tsx +import React, {useState} from 'react'; +import {useStableCallback} from '@rozhkov/react-useful-hooks'; +import {DatePicker} from '@quidone/react-native-wheel-picker'; + +const CustomizedDatePicker = () => { + const [date, setDate] = useState('2025-02-02'); + + const onDateChanged = useStableCallback(({date}: {date: string}) => { + setDate(date); + }); + + return ( + } + renderMonth={() => } + renderYear={() => } + > + {({dateNodes}) => { + return dateNodes.map((x) => x.node); + }} + + ); +}; +``` + +### PickerControl usage (Beta) + +```tsx +import React, {useState} from 'react'; +import WheelPicker, { + type PickerItem, + useOnPickerValueChangedEffect, + useOnPickerValueChangingEffect, + usePickerControl, + withPickerControl, +} from '@quidone/react-native-wheel-picker'; +import {View} from 'react-native'; + +const ControlPicker = withPickerControl(WheelPicker); + +type ControlPickersMap = { + value1: {item: PickerItem}; + value2: {item: PickerItem}; +}; + +const data = Array.from({length: 100}, (_, index) => ({value: index})); + +const App = () => { + const [value, setValue] = useState({value1: 0, value2: 0}); + + const pickerControl = usePickerControl(); + + useOnPickerValueChangedEffect(pickerControl, (event) => { + setValue({ + value1: event.pickers.value1.item.value, + value2: event.pickers.value2.item.value, + }); + }); + + useOnPickerValueChangingEffect(pickerControl, (event) => { + // some logic + }); + + return ( + + + + + ); +}; +``` + + ## Native Feedback You can trigger native sound and impact with [@quidone/react-native-wheel-picker-feedback][FEEDBACK_GITHUB] @@ -154,13 +277,13 @@ const App = () => { - ```scrollEventThrottle?``` [object | array] - [original](https://reactnative.dev/docs/scrollview#scrolleventthrottle-ios) -### usePickerItemHeight +#### usePickerItemHeight This hook returns the item height which was passed via props. -### useScrollContentOffset +#### useScrollContentOffset This hook returns the animated value of the ScrollView offset. -### withVirtualized +#### withVirtualized This HOC returns virtualized picker ```jsx @@ -175,6 +298,58 @@ const VirtualizedWheelPicker = withVirtualized(WheelPicker); - ```windowSize?``` - [original](https://reactnative.dev/docs/flatlist#windowsize). - ```updateCellsBatchingPeriod?``` (default = 10) - [original](https://reactnative.dev/docs/flatlist#updatecellsbatchingperiod). +### DatePicker + +A specialized picker component for selecting dates. It supports localization and deep customization. + +#### Props +- ```date``` [string] - Current date in 'YYYY-MM-DD' format +- ```onDateChanged``` [function] - Callback fired when date selection is confirmed +- ```minDate?``` [string] - Minimum selectable date in 'YYYY-MM-DD' format +- ```maxDate?``` [string] - Maximum selectable date in 'YYYY-MM-DD' format +- ```locale?``` [string] - Locale for date formatting (default = 'en') +- ```renderDate?``` [function] - Custom renderer for date component +- ```renderMonth?``` [function] - Custom renderer for month component +- ```renderYear?``` [function] - Custom renderer for year component +- ```children?``` [function] - Render prop for customizing component layout + +DatePicker also accepts all the common wheel picker props like `itemHeight`, `visibleItemCount`, `readOnly`, etc. + +#### Subcomponents +DatePicker exposes subcomponents that can be used for custom layouts: +- ```DatePicker.Date``` - Day WheelPicker +- ```DatePicker.Month``` - Month WheelPicker +- ```DatePicker.Year``` - Year WheelPicker + +### Picker Control + +Picker Control provides a way to synchronize multiple WheelPicker components. It is used inside DatePicker. + +Main goals: +1. Synchronize onValueChanged and onValueChanging events. +2. Synchronize the value selection process. If a value changes, all WheelPickers should accept this value, even if they are still spinning. + +#### usePickerControl + +A hook that creates a control object for connecting multiple pickers. See [example](#PickerControl-Usage) + +#### withPickerControl + +A HOC that connects a WheelPicker to a control object. See [example](#PickerControl-Usage) + +##### Adding props +- ```control``` [object] - Control object created with `usePickerControl` +- ```pickerName``` [string] - Unique name for the picker within the control group + +#### useOnPickerValueChangedEffect + +Called when the value has been changed. This occurs during the inactive state of all WheelPickers. See [example](#PickerControl-Usage) + +#### useOnPickerValueChangingEffect + +Called when any of the connected WheelPickers changes. See [example](#PickerControl-Usage) + + ## 👨‍💻 Author [Sergey Rozhkov][AUTHOR] diff --git a/example/package.json b/example/package.json index a8c5fca..579f1d3 100644 --- a/example/package.json +++ b/example/package.json @@ -10,19 +10,26 @@ "dependencies": { "@expo/ngrok": "^4.1.1", "@faker-js/faker": "^8.0.2", + "@gorhom/bottom-sheet": "^5", "@quidone/react-native-wheel-picker-feedback": "^2.0.0", - "@react-native-picker/picker": "2.9.0", + "@react-native-picker/picker": "2.11.1", + "@react-navigation/elements": "^2.6.3", + "@react-navigation/native": "^7.1.17", + "@react-navigation/native-stack": "^7.3.25", "@rozhkov/react-useful-hooks": "^1.0.9", - "expo": "^52.0.41", - "expo-splash-screen": "~0.29.22", - "expo-status-bar": "~2.0.1", - "react": "18.3.1", - "react-dom": "18.3.1", - "react-native": "0.76.7", + "expo": "^53.0.20", + "expo-splash-screen": "~0.30.10", + "expo-status-bar": "~2.2.3", + "react": "19.0.0", + "react-dom": "19.0.0", + "react-native": "0.79.5", "react-native-builder-bob": "^0.38.4", "react-native-elements": "^4.0.0-rc.2", - "react-native-safe-area-context": "4.12.0", - "react-native-web": "~0.19.10" + "react-native-gesture-handler": "~2.24.0", + "react-native-reanimated": "~3.17.4", + "react-native-safe-area-context": "5.4.0", + "react-native-screens": "~4.11.1", + "react-native-web": "^0.20.0" }, "devDependencies": { "@babel/core": "^7.24.0", diff --git a/example/src/App.tsx b/example/src/App.tsx index 2ddc59c..45cd5af 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,38 +1,19 @@ 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'; +import {GestureHandlerRootView} from 'react-native-gesture-handler'; +import {BottomSheetModalProvider} from '@gorhom/bottom-sheet'; 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..6966634 --- /dev/null +++ b/example/src/snack/navigator/RootNavigator.tsx @@ -0,0 +1,24 @@ +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'; +import {WithBottomSheetScreen} from '../screens/with-bottom-sheet'; + +// @ts-ignore +const RootStackNavigator = createNativeStackNavigator({ + screens: { + Main: MainScreen, + SimplePicker: SimplePickerScreen, + SimplePickerAndIOSPicker: SimplePickerAndIOSPickerScreen, + CustomizedPicker: CustomizedPickerScreen, + SimpleDatePicker: SimpleDatePickerScreen, + ControlSimpleUsage: ControlSimpleUsage, + WithBottomSheet: WithBottomSheetScreen, + }, +}); + +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 55% rename from example/src/picker-config/PickerConfigPanel.tsx rename to example/src/snack/props-changer/PickerPropsChangerPanel.tsx index 21a88d7..c534521 100644 --- a/example/src/picker-config/PickerConfigPanel.tsx +++ b/example/src/snack/props-changer/PickerPropsChangerPanel.tsx @@ -1,54 +1,94 @@ 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 { + Linking, + type StyleProp, + StyleSheet, + Text, + TouchableOpacity, + View, + type ViewStyle, +} from 'react-native'; +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 = { + style?: StyleProp; + hideSound?: boolean; + hideImpact?: boolean; + hideVirtualized?: boolean; +}; -const PickerConfigPanel = () => { +const PickerPropsChangerPanel = ({ + style, + 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..d100d2c --- /dev/null +++ b/example/src/snack/props-changer/withPropsChanger.tsx @@ -0,0 +1,82 @@ +import React, {type 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..6d33377 --- /dev/null +++ b/example/src/snack/screens/control-simple-usage/ControlSimpleUsage.tsx @@ -0,0 +1,138 @@ +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); + setValue({ + value1: event.pickers.value1.item.value, + value2: event.pickers.value2.item.value, + }); + }); + + useOnPickerValueChangingEffect(pickerControl, (event) => { + const curValue = { + value1: event.pickers.value1.item.value, + value2: event.pickers.value2.item.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}); + }; + + return ( + + + + + + + value: {value.value1}:{value.value2} + + + +