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} + + + {/* Добавленные кнопки для случайных значений */} + +