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}
+
+
+ {/* Добавленные кнопки для случайных значений */}
+
+
+
+
+
+ Data count: {dataCount}
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ buttonsContainer: {
+ marginTop: 20,
+ paddingHorizontal: 10,
+ gap: 8,
+ },
+});
+
+export default memo(ControlSimpleUsage);
diff --git a/example/src/snack/screens/control-simple-usage/index.ts b/example/src/snack/screens/control-simple-usage/index.ts
new file mode 100644
index 0000000..845185d
--- /dev/null
+++ b/example/src/snack/screens/control-simple-usage/index.ts
@@ -0,0 +1 @@
+export {default as ControlSimpleUsage} from './ControlSimpleUsage';
diff --git a/example/src/components/example-blocks/AvatarCustomizedPickerBlockExample/index.tsx b/example/src/snack/screens/customized-picker/CustomizedPickerScreen.tsx
similarity index 60%
rename from example/src/components/example-blocks/AvatarCustomizedPickerBlockExample/index.tsx
rename to example/src/snack/screens/customized-picker/CustomizedPickerScreen.tsx
index 8d4a1d6..5b73cb8 100644
--- a/example/src/components/example-blocks/AvatarCustomizedPickerBlockExample/index.tsx
+++ b/example/src/snack/screens/customized-picker/CustomizedPickerScreen.tsx
@@ -9,14 +9,18 @@ import WheelPicker, {
} from '@quidone/react-native-wheel-picker';
import {useInit} from '@rozhkov/react-useful-hooks';
import {faker} from '@faker-js/faker';
-import {withExamplePickerConfig} from '../../../picker-config';
-import {Header} from '../../base';
+import {
+ PickerPropsChangerPanel,
+ PickerPropsChangerProvider,
+ withPropsChanger,
+} from '../../props-changer';
+import {Divider, PickerScreenViewContainer} from '../../ui-base';
import PickerItemContainer from './PickerItemContainer';
import PickerItemComponent from './PickerItem';
import Overlay from './Overlay';
import type {CusPickerItem} from './types';
-const ExampleWheelPicker = withExamplePickerConfig(WheelPicker);
+const ExampleWheelPicker = withPropsChanger(WheelPicker);
const createPickerItem = (index: number): CusPickerItem => {
const sex = index % 2 === 0 ? 'male' : 'female';
@@ -33,12 +37,13 @@ const createPickerItem = (index: number): CusPickerItem => {
const renderItem: RenderItem> = (props) => (
);
-const renderItemContainer: RenderItemContainer> = (props) => (
-
-);
+const renderItemContainer: RenderItemContainer> = ({
+ key,
+ ...restProps
+}) => ;
const renderOverlay: RenderOverlay = (props) => ;
-const CustomizedPicker = () => {
+const CustomizedPickerScreen = () => {
const data = useInit(() => [...Array(100).keys()].map(createPickerItem));
const [value, setValue] = useState(0);
@@ -50,23 +55,26 @@ const CustomizedPicker = () => {
);
return (
- <>
-
-
-
-
- >
+
+
+
+
+
+
+
+
+
);
};
@@ -76,4 +84,4 @@ const styles = StyleSheet.create({
contentContainerStyle: {paddingLeft: 32},
});
-export default memo(CustomizedPicker);
+export default memo(CustomizedPickerScreen);
diff --git a/example/src/components/example-blocks/AvatarCustomizedPickerBlockExample/Overlay.tsx b/example/src/snack/screens/customized-picker/Overlay.tsx
similarity index 100%
rename from example/src/components/example-blocks/AvatarCustomizedPickerBlockExample/Overlay.tsx
rename to example/src/snack/screens/customized-picker/Overlay.tsx
diff --git a/example/src/components/example-blocks/AvatarCustomizedPickerBlockExample/PickerItem.tsx b/example/src/snack/screens/customized-picker/PickerItem.tsx
similarity index 100%
rename from example/src/components/example-blocks/AvatarCustomizedPickerBlockExample/PickerItem.tsx
rename to example/src/snack/screens/customized-picker/PickerItem.tsx
diff --git a/example/src/components/example-blocks/AvatarCustomizedPickerBlockExample/PickerItemContainer.tsx b/example/src/snack/screens/customized-picker/PickerItemContainer.tsx
similarity index 100%
rename from example/src/components/example-blocks/AvatarCustomizedPickerBlockExample/PickerItemContainer.tsx
rename to example/src/snack/screens/customized-picker/PickerItemContainer.tsx
diff --git a/example/src/snack/screens/customized-picker/index.ts b/example/src/snack/screens/customized-picker/index.ts
new file mode 100644
index 0000000..06072c9
--- /dev/null
+++ b/example/src/snack/screens/customized-picker/index.ts
@@ -0,0 +1 @@
+export {default as CustomizedPickerScreen} from './CustomizedPickerScreen';
diff --git a/example/src/components/example-blocks/AvatarCustomizedPickerBlockExample/types.ts b/example/src/snack/screens/customized-picker/types.ts
similarity index 100%
rename from example/src/components/example-blocks/AvatarCustomizedPickerBlockExample/types.ts
rename to example/src/snack/screens/customized-picker/types.ts
diff --git a/example/src/snack/screens/index.ts b/example/src/snack/screens/index.ts
new file mode 100644
index 0000000..e69de29
diff --git a/example/src/snack/screens/main/MainScreen.tsx b/example/src/snack/screens/main/MainScreen.tsx
new file mode 100644
index 0000000..0c3f72f
--- /dev/null
+++ b/example/src/snack/screens/main/MainScreen.tsx
@@ -0,0 +1,46 @@
+import {useNavigation} from '@react-navigation/native';
+import React from 'react';
+import {ListItem} from 'react-native-elements';
+import {Divider} from '../../ui-base';
+
+const screens: {title: string; screenName: string}[] = [
+ {screenName: 'SimplePicker', title: 'Picker'},
+ {
+ screenName: 'SimplePickerAndIOSPicker',
+ title: 'Picker and iOS Picker',
+ },
+ {
+ screenName: 'CustomizedPicker',
+ title: 'Customized Picker',
+ },
+ {
+ screenName: 'SimpleDatePicker',
+ title: 'Date Picker',
+ },
+ {
+ screenName: 'ControlSimpleUsage',
+ title: 'Control / Simple usage',
+ },
+];
+
+const MainScreen = () => {
+ const navigation = useNavigation();
+
+ return (
+ <>
+ {screens.map((screen) => (
+
+ navigation.navigate(screen.screenName)}>
+
+ {screen.title}
+
+
+
+
+
+ ))}
+ >
+ );
+};
+
+export default MainScreen;
diff --git a/example/src/snack/screens/main/index.ts b/example/src/snack/screens/main/index.ts
new file mode 100644
index 0000000..1efa8f4
--- /dev/null
+++ b/example/src/snack/screens/main/index.ts
@@ -0,0 +1 @@
+export {default as MainScreen} from './MainScreen';
diff --git a/example/src/snack/screens/simple-date-picker/SimpleDatePickerScreen.tsx b/example/src/snack/screens/simple-date-picker/SimpleDatePickerScreen.tsx
new file mode 100644
index 0000000..0880829
--- /dev/null
+++ b/example/src/snack/screens/simple-date-picker/SimpleDatePickerScreen.tsx
@@ -0,0 +1,77 @@
+import React, {useState} from 'react';
+import {Text, View} from 'react-native';
+import {DatePicker} from '@quidone/react-native-wheel-picker';
+import {useStableCallback} from '@rozhkov/react-useful-hooks';
+import {Divider, PickerScreenViewContainer} from '../../ui-base';
+import {
+ PickerPropsChangerPanel,
+ PickerPropsChangerProvider,
+} from '../../props-changer';
+import withPropsChanger from '../../props-changer/withPropsChanger';
+import {Button} from 'react-native-elements';
+
+const generateRandomDate = (): string => {
+ const currentDate = new Date();
+ const tenYearsInMilliseconds = 10 * 365 * 24 * 60 * 60 * 1000;
+
+ const randomTimestamp =
+ currentDate.getTime() + Math.floor(Math.random() * tenYearsInMilliseconds);
+
+ const randomDate = new Date(randomTimestamp);
+
+ const year = randomDate.getFullYear();
+ const month = String(randomDate.getMonth() + 1).padStart(2, '0');
+ const day = String(randomDate.getDate()).padStart(2, '0');
+
+ return `${year}-${month}-${day}`;
+};
+
+const ExampleDatePicker = withPropsChanger(DatePicker);
+
+const SimpleDatePickerScreen = () => {
+ const [date, setDate] = useState('2025-02-02');
+
+ const onDateChanged = useStableCallback(({date}: {date: string}) => {
+ setDate(date);
+ });
+
+ return (
+
+
+
+
+
+ value: "{date}"
+
+ setDate('1900-01-01')}
+ />
+ setDate(generateRandomDate())}
+ />
+ setDate('2200-01-01')}
+ />
+
+
+
+
+
+ );
+};
+
+export default SimpleDatePickerScreen;
diff --git a/example/src/snack/screens/simple-date-picker/index.ts b/example/src/snack/screens/simple-date-picker/index.ts
new file mode 100644
index 0000000..f55da6f
--- /dev/null
+++ b/example/src/snack/screens/simple-date-picker/index.ts
@@ -0,0 +1 @@
+export {default as SimpleDatePickerScreen} from './SimpleDatePickerScreen';
diff --git a/example/src/components/example-blocks/CompareWithNativeIOSBlockExample.tsx b/example/src/snack/screens/simple-picker-and-ios-picker/SimplePickerAndIOSPickerScreen.tsx
similarity index 82%
rename from example/src/components/example-blocks/CompareWithNativeIOSBlockExample.tsx
rename to example/src/snack/screens/simple-picker-and-ios-picker/SimplePickerAndIOSPickerScreen.tsx
index 756e5e0..678839b 100644
--- a/example/src/components/example-blocks/CompareWithNativeIOSBlockExample.tsx
+++ b/example/src/snack/screens/simple-picker-and-ios-picker/SimplePickerAndIOSPickerScreen.tsx
@@ -1,18 +1,22 @@
import React, {FC, memo, useCallback, useState} from 'react';
import {Platform, StyleSheet, Text, View} from 'react-native';
+import {useInit} from '@rozhkov/react-useful-hooks';
+import {Picker as IOSPicker} from '@react-native-picker/picker';
import Picker, {
OnValueChanged,
PickerItem,
} from '@quidone/react-native-wheel-picker';
-import {Picker as IOSPicker} from '@react-native-picker/picker';
-import {useInit} from '@rozhkov/react-useful-hooks';
-import {withExamplePickerConfig} from '../../picker-config';
-import {Header} from '../base';
+import {Divider, PickerScreenViewContainer} from '../../ui-base';
+import {
+ PickerPropsChangerPanel,
+ PickerPropsChangerProvider,
+ withPropsChanger,
+} from '../../props-changer';
let WheelPickers: FC;
if (Platform.OS === 'ios') {
- const ExamplePicker = withExamplePickerConfig(Picker);
+ const ExamplePicker = withPropsChanger(Picker);
const createPickerItem = (index: number): PickerItem => ({
value: index,
label: index.toString(),
@@ -96,13 +100,18 @@ if (Platform.OS === 'ios') {
const styles = StyleSheet.create({root: {textAlign: 'center', padding: 20}});
}
-const CompareWithNativeIOSBlockExample = () => {
+const SimplePickerAndIOSPickerScreen = () => {
return (
- <>
-
-
- >
+
+
+
+
+
+
+
+
+
);
};
-export default CompareWithNativeIOSBlockExample;
+export default SimplePickerAndIOSPickerScreen;
diff --git a/example/src/snack/screens/simple-picker-and-ios-picker/index.ts b/example/src/snack/screens/simple-picker-and-ios-picker/index.ts
new file mode 100644
index 0000000..5fef349
--- /dev/null
+++ b/example/src/snack/screens/simple-picker-and-ios-picker/index.ts
@@ -0,0 +1 @@
+export {default as SimplePickerAndIOSPickerScreen} from './SimplePickerAndIOSPickerScreen';
diff --git a/example/src/snack/screens/simple-picker/SimplePickerScreen.tsx b/example/src/snack/screens/simple-picker/SimplePickerScreen.tsx
new file mode 100644
index 0000000..ad38e15
--- /dev/null
+++ b/example/src/snack/screens/simple-picker/SimplePickerScreen.tsx
@@ -0,0 +1,63 @@
+import React, {memo, useCallback, useState} from 'react';
+import {Text, View} from 'react-native';
+import {Button} from 'react-native-elements';
+import {useInit} from '@rozhkov/react-useful-hooks';
+import WheelPicker, {
+ PickerItem,
+ type ValueChangedEvent,
+} from '@quidone/react-native-wheel-picker';
+
+import {Divider, PickerScreenViewContainer} from '../../ui-base';
+import PickerPropsChangerProvider from '../../props-changer/PickerPropsChangerProvider';
+import PickerPropsChangerPanel from '../../props-changer/PickerPropsChangerPanel';
+import withPropsChanger from '../../props-changer/withPropsChanger';
+import {getRandomNumber} from '../../get-random-number';
+
+const ExampleWheelPicker = withPropsChanger(WheelPicker);
+const createPickerItem = (index: number): PickerItem => ({
+ value: index,
+ label: index.toString(),
+});
+
+const SimplePickerScreen = () => {
+ const data = useInit(() => [...Array(100).keys()].map(createPickerItem));
+ const [value, setValue] = useState(0);
+
+ const onValueChanged = useCallback(
+ ({item: {value: val}}: ValueChangedEvent>) => {
+ setValue(val);
+ },
+ [],
+ );
+
+ return (
+
+
+
+
+ {
+ console.log('_onScrollStart');
+ }}
+ _onScrollEnd={() => {
+ console.log('_onScrollEnd');
+ }}
+ />
+ value: "{value}"
+ setValue(getRandomNumber(value, data.length))}
+ />
+
+
+
+
+ );
+};
+
+export default memo(SimplePickerScreen);
diff --git a/example/src/snack/screens/simple-picker/index.ts b/example/src/snack/screens/simple-picker/index.ts
new file mode 100644
index 0000000..692a338
--- /dev/null
+++ b/example/src/snack/screens/simple-picker/index.ts
@@ -0,0 +1 @@
+export {default as SimplePickerScreen} from './SimplePickerScreen';
diff --git a/example/src/components/base/Box.tsx b/example/src/snack/ui-base/Box.tsx
similarity index 100%
rename from example/src/components/base/Box.tsx
rename to example/src/snack/ui-base/Box.tsx
diff --git a/example/src/components/base/Divider.tsx b/example/src/snack/ui-base/Divider.tsx
similarity index 100%
rename from example/src/components/base/Divider.tsx
rename to example/src/snack/ui-base/Divider.tsx
diff --git a/example/src/components/base/Header.tsx b/example/src/snack/ui-base/Header.tsx
similarity index 100%
rename from example/src/components/base/Header.tsx
rename to example/src/snack/ui-base/Header.tsx
diff --git a/example/src/components/base/ListItemCheckBox.tsx b/example/src/snack/ui-base/ListItemCheckBox.tsx
similarity index 98%
rename from example/src/components/base/ListItemCheckBox.tsx
rename to example/src/snack/ui-base/ListItemCheckBox.tsx
index 0dafa59..f2b8e7b 100644
--- a/example/src/components/base/ListItemCheckBox.tsx
+++ b/example/src/snack/ui-base/ListItemCheckBox.tsx
@@ -17,7 +17,7 @@ const ListItemCheckBox = ({title, value, onToggle}: ListItemCheckBoxProps) => {
};
const styles = StyleSheet.create({
- root: {flexDirection: 'row', alignItems: 'center', paddingVertical: 8},
+ root: {flexDirection: 'row', alignItems: 'center', paddingVertical: 4},
title: {flex: 1, fontSize: 16},
});
diff --git a/example/src/snack/ui-base/PickerScreenViewContainer.tsx b/example/src/snack/ui-base/PickerScreenViewContainer.tsx
new file mode 100644
index 0000000..f2ccbc8
--- /dev/null
+++ b/example/src/snack/ui-base/PickerScreenViewContainer.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import {View, type ViewProps} from 'react-native';
+
+const PickerScreenViewContainer = ({style, ...restProps}: ViewProps) => {
+ return (
+
+ );
+};
+
+export default PickerScreenViewContainer;
diff --git a/example/src/components/base/index.ts b/example/src/snack/ui-base/index.ts
similarity index 70%
rename from example/src/components/base/index.ts
rename to example/src/snack/ui-base/index.ts
index 364c26e..4653619 100644
--- a/example/src/components/base/index.ts
+++ b/example/src/snack/ui-base/index.ts
@@ -2,3 +2,4 @@ export {default as Box} from './Box';
export {default as Divider} from './Divider';
export {default as Header} from './Header';
export {default as ListItemCheckBox} from './ListItemCheckBox';
+export {default as PickerScreenViewContainer} from './PickerScreenViewContainer';
diff --git a/example/yarn.lock b/example/yarn.lock
index f3395cb..5a5c86e 100644
--- a/example/yarn.lock
+++ b/example/yarn.lock
@@ -47,6 +47,17 @@ __metadata:
languageName: node
linkType: hard
+"@babel/code-frame@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/code-frame@npm:7.27.1"
+ dependencies:
+ "@babel/helper-validator-identifier": "npm:^7.27.1"
+ js-tokens: "npm:^4.0.0"
+ picocolors: "npm:^1.1.1"
+ checksum: 10/721b8a6e360a1fa0f1c9fe7351ae6c874828e119183688b533c477aa378f1010f37cc9afbfc4722c686d1f5cdd00da02eab4ba7278a0c504fa0d7a321dcd4fdf
+ languageName: node
+ linkType: hard
+
"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.26.8":
version: 7.26.8
resolution: "@babel/compat-data@npm:7.26.8"
@@ -54,6 +65,13 @@ __metadata:
languageName: node
linkType: hard
+"@babel/compat-data@npm:^7.27.2":
+ version: 7.28.0
+ resolution: "@babel/compat-data@npm:7.28.0"
+ checksum: 10/1a56a5e48c7259f72cc4329adeca38e72fd650ea09de267ea4aa070e3da91e5c265313b6656823fff77d64a8bab9554f276c66dade9355fdc0d8604deea015aa
+ languageName: node
+ linkType: hard
+
"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.20.0, @babel/core@npm:^7.20.2, @babel/core@npm:^7.24.0, @babel/core@npm:^7.25.2":
version: 7.26.10
resolution: "@babel/core@npm:7.26.10"
@@ -90,6 +108,19 @@ __metadata:
languageName: node
linkType: hard
+"@babel/generator@npm:^7.28.0":
+ version: 7.28.0
+ resolution: "@babel/generator@npm:7.28.0"
+ dependencies:
+ "@babel/parser": "npm:^7.28.0"
+ "@babel/types": "npm:^7.28.0"
+ "@jridgewell/gen-mapping": "npm:^0.3.12"
+ "@jridgewell/trace-mapping": "npm:^0.3.28"
+ jsesc: "npm:^3.0.2"
+ checksum: 10/064c5ba4c07ecd7600377bd0022d5f6bdb3b35e9ff78d9378f6bd1e656467ca902c091647222ab2f0d2967f6d6c0ca33157d37dd9b1c51926c9b0e1527ab9b92
+ languageName: node
+ linkType: hard
+
"@babel/helper-annotate-as-pure@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-annotate-as-pure@npm:7.25.9"
@@ -99,6 +130,15 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-annotate-as-pure@npm:^7.27.1, @babel/helper-annotate-as-pure@npm:^7.27.3":
+ version: 7.27.3
+ resolution: "@babel/helper-annotate-as-pure@npm:7.27.3"
+ dependencies:
+ "@babel/types": "npm:^7.27.3"
+ checksum: 10/63863a5c936ef82b546ca289c9d1b18fabfc24da5c4ee382830b124e2e79b68d626207febc8d4bffc720f50b2ee65691d7d12cc0308679dee2cd6bdc926b7190
+ languageName: node
+ linkType: hard
+
"@babel/helper-compilation-targets@npm:^7.22.6, @babel/helper-compilation-targets@npm:^7.25.9, @babel/helper-compilation-targets@npm:^7.26.5":
version: 7.27.0
resolution: "@babel/helper-compilation-targets@npm:7.27.0"
@@ -112,6 +152,19 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-compilation-targets@npm:^7.27.2":
+ version: 7.27.2
+ resolution: "@babel/helper-compilation-targets@npm:7.27.2"
+ dependencies:
+ "@babel/compat-data": "npm:^7.27.2"
+ "@babel/helper-validator-option": "npm:^7.27.1"
+ browserslist: "npm:^4.24.0"
+ lru-cache: "npm:^5.1.1"
+ semver: "npm:^6.3.1"
+ checksum: 10/bd53c30a7477049db04b655d11f4c3500aea3bcbc2497cf02161de2ecf994fec7c098aabbcebe210ffabc2ecbdb1e3ffad23fb4d3f18723b814f423ea1749fe8
+ languageName: node
+ linkType: hard
+
"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.25.9, @babel/helper-create-class-features-plugin@npm:^7.27.0":
version: 7.27.0
resolution: "@babel/helper-create-class-features-plugin@npm:7.27.0"
@@ -129,6 +182,23 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-create-class-features-plugin@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/helper-create-class-features-plugin@npm:7.27.1"
+ dependencies:
+ "@babel/helper-annotate-as-pure": "npm:^7.27.1"
+ "@babel/helper-member-expression-to-functions": "npm:^7.27.1"
+ "@babel/helper-optimise-call-expression": "npm:^7.27.1"
+ "@babel/helper-replace-supers": "npm:^7.27.1"
+ "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1"
+ "@babel/traverse": "npm:^7.27.1"
+ semver: "npm:^6.3.1"
+ peerDependencies:
+ "@babel/core": ^7.0.0
+ checksum: 10/701579b49046cd42f6a6b1e693e6827df8623185adf0911c4d68a219a082d8fd4501672880d92b6b96263d1c92a3beb891b3464a662a55e69e7539d8db9277da
+ languageName: node
+ linkType: hard
+
"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.25.9":
version: 7.27.0
resolution: "@babel/helper-create-regexp-features-plugin@npm:7.27.0"
@@ -142,6 +212,19 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-create-regexp-features-plugin@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/helper-create-regexp-features-plugin@npm:7.27.1"
+ dependencies:
+ "@babel/helper-annotate-as-pure": "npm:^7.27.1"
+ regexpu-core: "npm:^6.2.0"
+ semver: "npm:^6.3.1"
+ peerDependencies:
+ "@babel/core": ^7.0.0
+ checksum: 10/dea272628cd8874f127ab7b2ee468620aabc1383d38bb40c49a9c7667db2258cdfe6620a1d1412f5f0706583f6301b4b7ad3d5932f24df7fe72e66bf9bc0be45
+ languageName: node
+ linkType: hard
+
"@babel/helper-define-polyfill-provider@npm:^0.6.3, @babel/helper-define-polyfill-provider@npm:^0.6.4":
version: 0.6.4
resolution: "@babel/helper-define-polyfill-provider@npm:0.6.4"
@@ -157,6 +240,13 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-globals@npm:^7.28.0":
+ version: 7.28.0
+ resolution: "@babel/helper-globals@npm:7.28.0"
+ checksum: 10/91445f7edfde9b65dcac47f4f858f68dc1661bf73332060ab67ad7cc7b313421099a2bfc4bda30c3db3842cfa1e86fffbb0d7b2c5205a177d91b22c8d7d9cb47
+ languageName: node
+ linkType: hard
+
"@babel/helper-member-expression-to-functions@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-member-expression-to-functions@npm:7.25.9"
@@ -167,6 +257,16 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-member-expression-to-functions@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/helper-member-expression-to-functions@npm:7.27.1"
+ dependencies:
+ "@babel/traverse": "npm:^7.27.1"
+ "@babel/types": "npm:^7.27.1"
+ checksum: 10/533a5a2cf1c9a8770d241b86d5f124c88e953c831a359faf1ac7ba1e632749c1748281b83295d227fe6035b202d81f3d3a1ea13891f150c6538e040668d6126a
+ languageName: node
+ linkType: hard
+
"@babel/helper-module-imports@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-module-imports@npm:7.25.9"
@@ -177,6 +277,16 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-module-imports@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/helper-module-imports@npm:7.27.1"
+ dependencies:
+ "@babel/traverse": "npm:^7.27.1"
+ "@babel/types": "npm:^7.27.1"
+ checksum: 10/58e792ea5d4ae71676e0d03d9fef33e886a09602addc3bd01388a98d87df9fcfd192968feb40ac4aedb7e287ec3d0c17b33e3ecefe002592041a91d8a1998a8d
+ languageName: node
+ linkType: hard
+
"@babel/helper-module-transforms@npm:^7.25.9, @babel/helper-module-transforms@npm:^7.26.0":
version: 7.26.0
resolution: "@babel/helper-module-transforms@npm:7.26.0"
@@ -190,6 +300,19 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-module-transforms@npm:^7.27.1":
+ version: 7.27.3
+ resolution: "@babel/helper-module-transforms@npm:7.27.3"
+ dependencies:
+ "@babel/helper-module-imports": "npm:^7.27.1"
+ "@babel/helper-validator-identifier": "npm:^7.27.1"
+ "@babel/traverse": "npm:^7.27.3"
+ peerDependencies:
+ "@babel/core": ^7.0.0
+ checksum: 10/47abc90ceb181b4bdea9bf1717adf536d1b5e5acb6f6d8a7a4524080318b5ca8a99e6d58677268c596bad71077d1d98834d2c3815f2443e6d3f287962300f15d
+ languageName: node
+ linkType: hard
+
"@babel/helper-optimise-call-expression@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-optimise-call-expression@npm:7.25.9"
@@ -199,6 +322,15 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-optimise-call-expression@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/helper-optimise-call-expression@npm:7.27.1"
+ dependencies:
+ "@babel/types": "npm:^7.27.1"
+ checksum: 10/0fb7ee824a384529d6b74f8a58279f9b56bfe3cce332168067dddeab2552d8eeb56dc8eaf86c04a3a09166a316cb92dfc79c4c623cd034ad4c563952c98b464f
+ languageName: node
+ linkType: hard
+
"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.25.9, @babel/helper-plugin-utils@npm:^7.26.5, @babel/helper-plugin-utils@npm:^7.8.0":
version: 7.26.5
resolution: "@babel/helper-plugin-utils@npm:7.26.5"
@@ -206,6 +338,13 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-plugin-utils@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/helper-plugin-utils@npm:7.27.1"
+ checksum: 10/96136c2428888e620e2ec493c25888f9ceb4a21099dcf3dd4508ea64b58cdedbd5a9fb6c7b352546de84d6c24edafe482318646932a22c449ebd16d16c22d864
+ languageName: node
+ linkType: hard
+
"@babel/helper-remap-async-to-generator@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-remap-async-to-generator@npm:7.25.9"
@@ -232,6 +371,19 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-replace-supers@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/helper-replace-supers@npm:7.27.1"
+ dependencies:
+ "@babel/helper-member-expression-to-functions": "npm:^7.27.1"
+ "@babel/helper-optimise-call-expression": "npm:^7.27.1"
+ "@babel/traverse": "npm:^7.27.1"
+ peerDependencies:
+ "@babel/core": ^7.0.0
+ checksum: 10/72e3f8bef744c06874206bf0d80a0abbedbda269586966511c2491df4f6bf6d47a94700810c7a6737345a545dfb8295222e1e72f506bcd0b40edb3f594f739ea
+ languageName: node
+ linkType: hard
+
"@babel/helper-skip-transparent-expression-wrappers@npm:^7.20.0, @babel/helper-skip-transparent-expression-wrappers@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.25.9"
@@ -242,6 +394,16 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-skip-transparent-expression-wrappers@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.27.1"
+ dependencies:
+ "@babel/traverse": "npm:^7.27.1"
+ "@babel/types": "npm:^7.27.1"
+ checksum: 10/4f380c5d0e0769fa6942a468b0c2d7c8f0c438f941aaa88f785f8752c103631d0904c7b4e76207a3b0e6588b2dec376595370d92ca8f8f1b422c14a69aa146d4
+ languageName: node
+ linkType: hard
+
"@babel/helper-string-parser@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-string-parser@npm:7.25.9"
@@ -249,6 +411,13 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-string-parser@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/helper-string-parser@npm:7.27.1"
+ checksum: 10/0ae29cc2005084abdae2966afdb86ed14d41c9c37db02c3693d5022fba9f5d59b011d039380b8e537c34daf117c549f52b452398f576e908fb9db3c7abbb3a00
+ languageName: node
+ linkType: hard
+
"@babel/helper-validator-identifier@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-validator-identifier@npm:7.25.9"
@@ -256,6 +425,13 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-validator-identifier@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/helper-validator-identifier@npm:7.27.1"
+ checksum: 10/75041904d21bdc0cd3b07a8ac90b11d64cd3c881e89cb936fa80edd734bf23c35e6bd1312611e8574c4eab1f3af0f63e8a5894f4699e9cfdf70c06fcf4252320
+ languageName: node
+ linkType: hard
+
"@babel/helper-validator-option@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-validator-option@npm:7.25.9"
@@ -263,6 +439,13 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-validator-option@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/helper-validator-option@npm:7.27.1"
+ checksum: 10/db73e6a308092531c629ee5de7f0d04390835b21a263be2644276cb27da2384b64676cab9f22cd8d8dbd854c92b1d7d56fc8517cf0070c35d1c14a8c828b0903
+ languageName: node
+ linkType: hard
+
"@babel/helper-wrap-function@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-wrap-function@npm:7.25.9"
@@ -307,6 +490,17 @@ __metadata:
languageName: node
linkType: hard
+"@babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.0":
+ version: 7.28.0
+ resolution: "@babel/parser@npm:7.28.0"
+ dependencies:
+ "@babel/types": "npm:^7.28.0"
+ bin:
+ parser: ./bin/babel-parser.js
+ checksum: 10/2c14a0d2600bae9ab81924df0a85bbd34e427caa099c260743f7c6c12b2042e743e776043a0d1a2573229ae648f7e66a80cfb26fc27e2a9eb59b55932d44c817
+ languageName: node
+ linkType: hard
+
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.25.9"
@@ -579,6 +773,17 @@ __metadata:
languageName: node
linkType: hard
+"@babel/plugin-syntax-jsx@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/plugin-syntax-jsx@npm:7.27.1"
+ dependencies:
+ "@babel/helper-plugin-utils": "npm:^7.27.1"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 10/c6d1324cff286a369aa95d99b8abd21dd07821b5d3affd5fe7d6058c84cff9190743287826463ee57a7beecd10fa1e4bc99061df532ee14e188c1c8937b13e3a
+ languageName: node
+ linkType: hard
+
"@babel/plugin-syntax-logical-assignment-operators@npm:^7.10.4":
version: 7.10.4
resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4"
@@ -678,6 +883,17 @@ __metadata:
languageName: node
linkType: hard
+"@babel/plugin-syntax-typescript@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/plugin-syntax-typescript@npm:7.27.1"
+ dependencies:
+ "@babel/helper-plugin-utils": "npm:^7.27.1"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 10/87836f7e32af624c2914c73cd6b9803cf324e07d43f61dbb973c6a86f75df725e12540d91fac7141c14b697aa9268fd064220998daced156e96ac3062d7afb41
+ languageName: node
+ linkType: hard
+
"@babel/plugin-syntax-unicode-sets-regex@npm:^7.18.6":
version: 7.18.6
resolution: "@babel/plugin-syntax-unicode-sets-regex@npm:7.18.6"
@@ -690,6 +906,17 @@ __metadata:
languageName: node
linkType: hard
+"@babel/plugin-transform-arrow-functions@npm:^7.0.0-0":
+ version: 7.27.1
+ resolution: "@babel/plugin-transform-arrow-functions@npm:7.27.1"
+ dependencies:
+ "@babel/helper-plugin-utils": "npm:^7.27.1"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 10/62c2cc0ae2093336b1aa1376741c5ed245c0987d9e4b4c5313da4a38155509a7098b5acce582b6781cc0699381420010da2e3086353344abe0a6a0ec38961eb7
+ languageName: node
+ linkType: hard
+
"@babel/plugin-transform-arrow-functions@npm:^7.24.7, @babel/plugin-transform-arrow-functions@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/plugin-transform-arrow-functions@npm:7.25.9"
@@ -749,6 +976,18 @@ __metadata:
languageName: node
linkType: hard
+"@babel/plugin-transform-class-properties@npm:^7.0.0-0":
+ version: 7.27.1
+ resolution: "@babel/plugin-transform-class-properties@npm:7.27.1"
+ dependencies:
+ "@babel/helper-create-class-features-plugin": "npm:^7.27.1"
+ "@babel/helper-plugin-utils": "npm:^7.27.1"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 10/475a6e5a9454912fe1bdc171941976ca10ea4e707675d671cdb5ce6b6761d84d1791ac61b6bca81a2e5f6430cb7b9d8e4b2392404110e69c28207a754e196294
+ languageName: node
+ linkType: hard
+
"@babel/plugin-transform-class-properties@npm:^7.25.4, @babel/plugin-transform-class-properties@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/plugin-transform-class-properties@npm:7.25.9"
@@ -773,6 +1012,22 @@ __metadata:
languageName: node
linkType: hard
+"@babel/plugin-transform-classes@npm:^7.0.0-0":
+ version: 7.28.0
+ resolution: "@babel/plugin-transform-classes@npm:7.28.0"
+ dependencies:
+ "@babel/helper-annotate-as-pure": "npm:^7.27.3"
+ "@babel/helper-compilation-targets": "npm:^7.27.2"
+ "@babel/helper-globals": "npm:^7.28.0"
+ "@babel/helper-plugin-utils": "npm:^7.27.1"
+ "@babel/helper-replace-supers": "npm:^7.27.1"
+ "@babel/traverse": "npm:^7.28.0"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 10/1a812a02f641ffc80b139b3c690ceba52476576f9df1a62dbdde9c412e88ca143b7b872da71665838c34276c4ed92f6547199044a424222b84f9a8ee7c85798f
+ languageName: node
+ linkType: hard
+
"@babel/plugin-transform-classes@npm:^7.25.4, @babel/plugin-transform-classes@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/plugin-transform-classes@npm:7.25.9"
@@ -985,6 +1240,18 @@ __metadata:
languageName: node
linkType: hard
+"@babel/plugin-transform-modules-commonjs@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/plugin-transform-modules-commonjs@npm:7.27.1"
+ dependencies:
+ "@babel/helper-module-transforms": "npm:^7.27.1"
+ "@babel/helper-plugin-utils": "npm:^7.27.1"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 10/9059243a977bc1f13e3dccfc6feb6508890e7c7bb191f7eb56626b20672b4b12338051ca835ab55426875a473181502c8f35b4df58ba251bef63b25866d995fe
+ languageName: node
+ linkType: hard
+
"@babel/plugin-transform-modules-systemjs@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/plugin-transform-modules-systemjs@npm:7.25.9"
@@ -1034,6 +1301,17 @@ __metadata:
languageName: node
linkType: hard
+"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.0.0-0":
+ version: 7.27.1
+ resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.27.1"
+ dependencies:
+ "@babel/helper-plugin-utils": "npm:^7.27.1"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 10/15333f4888ffedc449a2a21a0b1ca7983e089f43faa00cfb71d2466e20221a5fd979cdb1a3f57bc20fc62c67bd3ff3dde054133fb6324a58be8f64d20aefacd2
+ languageName: node
+ linkType: hard
+
"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.7, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.26.6":
version: 7.26.6
resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.26.6"
@@ -1092,6 +1370,18 @@ __metadata:
languageName: node
linkType: hard
+"@babel/plugin-transform-optional-chaining@npm:^7.0.0-0":
+ version: 7.27.1
+ resolution: "@babel/plugin-transform-optional-chaining@npm:7.27.1"
+ dependencies:
+ "@babel/helper-plugin-utils": "npm:^7.27.1"
+ "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 10/34b0f96400c259a2722740d17a001fe45f78d8ff052c40e29db2e79173be72c1cfe8d9681067e3f5da3989e4a557402df5c982c024c18257587a41e022f95640
+ languageName: node
+ linkType: hard
+
"@babel/plugin-transform-optional-chaining@npm:^7.24.8, @babel/plugin-transform-optional-chaining@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/plugin-transform-optional-chaining@npm:7.25.9"
@@ -1273,6 +1563,17 @@ __metadata:
languageName: node
linkType: hard
+"@babel/plugin-transform-shorthand-properties@npm:^7.0.0-0":
+ version: 7.27.1
+ resolution: "@babel/plugin-transform-shorthand-properties@npm:7.27.1"
+ dependencies:
+ "@babel/helper-plugin-utils": "npm:^7.27.1"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 10/fbba6e2aef0b69681acb68202aa249c0598e470cc0853d7ff5bd0171fd6a7ec31d77cfabcce9df6360fc8349eded7e4a65218c32551bd3fc0caaa1ac899ac6d4
+ languageName: node
+ linkType: hard
+
"@babel/plugin-transform-shorthand-properties@npm:^7.24.7, @babel/plugin-transform-shorthand-properties@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/plugin-transform-shorthand-properties@npm:7.25.9"
@@ -1318,6 +1619,17 @@ __metadata:
languageName: node
linkType: hard
+"@babel/plugin-transform-template-literals@npm:^7.0.0-0":
+ version: 7.27.1
+ resolution: "@babel/plugin-transform-template-literals@npm:7.27.1"
+ dependencies:
+ "@babel/helper-plugin-utils": "npm:^7.27.1"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 10/93aad782503b691faef7c0893372d5243df3219b07f1f22cfc32c104af6a2e7acd6102c128439eab15336d048f1b214ca134b87b0630d8cd568bf447f78b25ce
+ languageName: node
+ linkType: hard
+
"@babel/plugin-transform-template-literals@npm:^7.26.8":
version: 7.26.8
resolution: "@babel/plugin-transform-template-literals@npm:7.26.8"
@@ -1355,6 +1667,21 @@ __metadata:
languageName: node
linkType: hard
+"@babel/plugin-transform-typescript@npm:^7.27.1":
+ version: 7.28.0
+ resolution: "@babel/plugin-transform-typescript@npm:7.28.0"
+ dependencies:
+ "@babel/helper-annotate-as-pure": "npm:^7.27.3"
+ "@babel/helper-create-class-features-plugin": "npm:^7.27.1"
+ "@babel/helper-plugin-utils": "npm:^7.27.1"
+ "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1"
+ "@babel/plugin-syntax-typescript": "npm:^7.27.1"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 10/5ad7aae0e900974585c7e0d0ec08bde8cd70a31a9e79f5c9ddadb4f8f6207cb86a5882181c2b262b0fe27558e9f9743306259911bc1445635ec58dd96613cef4
+ languageName: node
+ linkType: hard
+
"@babel/plugin-transform-unicode-escapes@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/plugin-transform-unicode-escapes@npm:7.25.9"
@@ -1378,6 +1705,18 @@ __metadata:
languageName: node
linkType: hard
+"@babel/plugin-transform-unicode-regex@npm:^7.0.0-0":
+ version: 7.27.1
+ resolution: "@babel/plugin-transform-unicode-regex@npm:7.27.1"
+ dependencies:
+ "@babel/helper-create-regexp-features-plugin": "npm:^7.27.1"
+ "@babel/helper-plugin-utils": "npm:^7.27.1"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 10/a34d89a2b75fb78e66d97c3dc90d4877f7e31f43316b52176f95a5dee20e9bb56ecf158eafc42a001676ddf7b393d9e67650bad6b32f5405780f25fb83cd68e3
+ languageName: node
+ linkType: hard
+
"@babel/plugin-transform-unicode-regex@npm:^7.24.7, @babel/plugin-transform-unicode-regex@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/plugin-transform-unicode-regex@npm:7.25.9"
@@ -1538,6 +1877,21 @@ __metadata:
languageName: node
linkType: hard
+"@babel/preset-typescript@npm:^7.16.7":
+ version: 7.27.1
+ resolution: "@babel/preset-typescript@npm:7.27.1"
+ dependencies:
+ "@babel/helper-plugin-utils": "npm:^7.27.1"
+ "@babel/helper-validator-option": "npm:^7.27.1"
+ "@babel/plugin-syntax-jsx": "npm:^7.27.1"
+ "@babel/plugin-transform-modules-commonjs": "npm:^7.27.1"
+ "@babel/plugin-transform-typescript": "npm:^7.27.1"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 10/9d8e75326b3c93fa016ba7aada652800fc77bc05fcc181888700a049935e8cf1284b549de18a5d62ef3591d02f097ea6de1111f7d71a991aaf36ba74657bd145
+ languageName: node
+ linkType: hard
+
"@babel/register@npm:^7.13.16":
version: 7.25.9
resolution: "@babel/register@npm:7.25.9"
@@ -1573,6 +1927,17 @@ __metadata:
languageName: node
linkType: hard
+"@babel/template@npm:^7.27.2":
+ version: 7.27.2
+ resolution: "@babel/template@npm:7.27.2"
+ dependencies:
+ "@babel/code-frame": "npm:^7.27.1"
+ "@babel/parser": "npm:^7.27.2"
+ "@babel/types": "npm:^7.27.1"
+ checksum: 10/fed15a84beb0b9340e5f81566600dbee5eccd92e4b9cc42a944359b1aa1082373391d9d5fc3656981dff27233ec935d0bc96453cf507f60a4b079463999244d8
+ languageName: node
+ linkType: hard
+
"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3, @babel/traverse@npm:^7.20.0, @babel/traverse@npm:^7.25.3, @babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.26.10, @babel/traverse@npm:^7.26.5, @babel/traverse@npm:^7.26.8, @babel/traverse@npm:^7.27.0":
version: 7.27.0
resolution: "@babel/traverse@npm:7.27.0"
@@ -1588,6 +1953,21 @@ __metadata:
languageName: node
linkType: hard
+"@babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.27.3, @babel/traverse@npm:^7.28.0":
+ version: 7.28.0
+ resolution: "@babel/traverse@npm:7.28.0"
+ dependencies:
+ "@babel/code-frame": "npm:^7.27.1"
+ "@babel/generator": "npm:^7.28.0"
+ "@babel/helper-globals": "npm:^7.28.0"
+ "@babel/parser": "npm:^7.28.0"
+ "@babel/template": "npm:^7.27.2"
+ "@babel/types": "npm:^7.28.0"
+ debug: "npm:^4.3.1"
+ checksum: 10/c1c24b12b6cb46241ec5d11ddbd2989d6955c282715cbd8ee91a09fe156b3bdb0b88353ac33329c2992113e3dfb5198f616c834f8805bb3fa85da1f864bec5f3
+ languageName: node
+ linkType: hard
+
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.2, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4":
version: 7.27.0
resolution: "@babel/types@npm:7.27.0"
@@ -1598,6 +1978,25 @@ __metadata:
languageName: node
linkType: hard
+"@babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.0":
+ version: 7.28.1
+ resolution: "@babel/types@npm:7.28.1"
+ dependencies:
+ "@babel/helper-string-parser": "npm:^7.27.1"
+ "@babel/helper-validator-identifier": "npm:^7.27.1"
+ checksum: 10/b35b0c030326e45efd4ebd87f30a7e5463f0c78617661ff12e8deb3fe983c53c48696374434ffd3664681cbc5b1495ebc69043753b232193e8dc02d1ae7d0ff5
+ languageName: node
+ linkType: hard
+
+"@egjs/hammerjs@npm:^2.0.17":
+ version: 2.0.17
+ resolution: "@egjs/hammerjs@npm:2.0.17"
+ dependencies:
+ "@types/hammerjs": "npm:^2.0.36"
+ checksum: 10/f695129d45edfcfd6c5f2d1d36186da36ffade013991972ce23721a6b7ad7f214ce282abc4023e3f6b63062620852a63e897b523f247804afc7acd188fee9d9d
+ languageName: node
+ linkType: hard
+
"@expo/bunyan@npm:^4.0.0":
version: 4.0.1
resolution: "@expo/bunyan@npm:4.0.1"
@@ -2310,6 +2709,16 @@ __metadata:
languageName: node
linkType: hard
+"@jridgewell/gen-mapping@npm:^0.3.12":
+ version: 0.3.12
+ resolution: "@jridgewell/gen-mapping@npm:0.3.12"
+ dependencies:
+ "@jridgewell/sourcemap-codec": "npm:^1.5.0"
+ "@jridgewell/trace-mapping": "npm:^0.3.24"
+ checksum: 10/151667531566417a940d4dd0a319724979f7a90b9deb9f1617344e1183887d78c835bc1a9209c1ee10fc8a669cdd7ac8120a43a2b6bc8d0d5dd18a173059ff4b
+ languageName: node
+ linkType: hard
+
"@jridgewell/gen-mapping@npm:^0.3.2, @jridgewell/gen-mapping@npm:^0.3.5":
version: 0.3.8
resolution: "@jridgewell/gen-mapping@npm:0.3.8"
@@ -2352,6 +2761,13 @@ __metadata:
languageName: node
linkType: hard
+"@jridgewell/sourcemap-codec@npm:^1.5.0":
+ version: 1.5.4
+ resolution: "@jridgewell/sourcemap-codec@npm:1.5.4"
+ checksum: 10/f677787f52224c6c971a7a41b7a074243240a6917fa75eceb9f7a442866f374fb0522b505e0496ee10a650c5936727e76d11bf36a6d0ae9e6c3b726c9e284cc7
+ languageName: node
+ linkType: hard
+
"@jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25":
version: 0.3.25
resolution: "@jridgewell/trace-mapping@npm:0.3.25"
@@ -2362,6 +2778,16 @@ __metadata:
languageName: node
linkType: hard
+"@jridgewell/trace-mapping@npm:^0.3.28":
+ version: 0.3.29
+ resolution: "@jridgewell/trace-mapping@npm:0.3.29"
+ dependencies:
+ "@jridgewell/resolve-uri": "npm:^3.1.0"
+ "@jridgewell/sourcemap-codec": "npm:^1.4.14"
+ checksum: 10/64e1ce0dc3a9e56b0118eaf1b2f50746fd59a36de37516cc6855b5370d5f367aa8229e1237536d738262e252c70ee229619cb04e3f3b822146ee3eb1b7ab297f
+ languageName: node
+ linkType: hard
+
"@leichtgewicht/ip-codec@npm:^2.0.1":
version: 2.0.5
resolution: "@leichtgewicht/ip-codec@npm:2.0.5"
@@ -2653,6 +3079,84 @@ __metadata:
languageName: node
linkType: hard
+"@react-navigation/core@npm:^7.12.1":
+ version: 7.12.1
+ resolution: "@react-navigation/core@npm:7.12.1"
+ dependencies:
+ "@react-navigation/routers": "npm:^7.4.1"
+ escape-string-regexp: "npm:^4.0.0"
+ nanoid: "npm:^3.3.11"
+ query-string: "npm:^7.1.3"
+ react-is: "npm:^19.1.0"
+ use-latest-callback: "npm:^0.2.4"
+ use-sync-external-store: "npm:^1.5.0"
+ peerDependencies:
+ react: ">= 18.2.0"
+ checksum: 10/efc8d06cde19f64bf9e25240612086e0c8feb4b7fc7bdd5a7bae1d743a711abd1c83596597bf044a79b7830fc32b65a125313700235e62b99772ca3213e2ca17
+ languageName: node
+ linkType: hard
+
+"@react-navigation/elements@npm:^2.5.2":
+ version: 2.5.2
+ resolution: "@react-navigation/elements@npm:2.5.2"
+ dependencies:
+ color: "npm:^4.2.3"
+ use-latest-callback: "npm:^0.2.4"
+ use-sync-external-store: "npm:^1.5.0"
+ peerDependencies:
+ "@react-native-masked-view/masked-view": ">= 0.2.0"
+ "@react-navigation/native": ^7.1.14
+ react: ">= 18.2.0"
+ react-native: "*"
+ react-native-safe-area-context: ">= 4.0.0"
+ peerDependenciesMeta:
+ "@react-native-masked-view/masked-view":
+ optional: true
+ checksum: 10/f6c472f82487661aee6f41e82e453cb80fea85e3956d94e58aa45b7c3f5ae04e4862df400439d0a58e6c54f8e091d7e08c547a153165d881fa8c74fd3b63d6a3
+ languageName: node
+ linkType: hard
+
+"@react-navigation/native-stack@npm:^7.3.21":
+ version: 7.3.21
+ resolution: "@react-navigation/native-stack@npm:7.3.21"
+ dependencies:
+ "@react-navigation/elements": "npm:^2.5.2"
+ warn-once: "npm:^0.1.1"
+ peerDependencies:
+ "@react-navigation/native": ^7.1.14
+ react: ">= 18.2.0"
+ react-native: "*"
+ react-native-safe-area-context: ">= 4.0.0"
+ react-native-screens: ">= 4.0.0"
+ checksum: 10/c79c6fea6fc786c7c2f46f23b9a1d7bd8d1a8bf2bc5022a7c97fc3fabbc12bd2370f57982cac56097feb3d845b3591cd1d39fb12e4e6eab71eb1be11b2f833d8
+ languageName: node
+ linkType: hard
+
+"@react-navigation/native@npm:^7.1.14":
+ version: 7.1.14
+ resolution: "@react-navigation/native@npm:7.1.14"
+ dependencies:
+ "@react-navigation/core": "npm:^7.12.1"
+ escape-string-regexp: "npm:^4.0.0"
+ fast-deep-equal: "npm:^3.1.3"
+ nanoid: "npm:^3.3.11"
+ use-latest-callback: "npm:^0.2.4"
+ peerDependencies:
+ react: ">= 18.2.0"
+ react-native: "*"
+ checksum: 10/167ab132a2d06a41f969d62b47bd78ca961406af68f57cd954e17815c1ea1ba01eb9e0470cbaa6fb1fd11d0515f608b0771fb97165792e2d75c6194d6ff4996e
+ languageName: node
+ linkType: hard
+
+"@react-navigation/routers@npm:^7.4.1":
+ version: 7.4.1
+ resolution: "@react-navigation/routers@npm:7.4.1"
+ dependencies:
+ nanoid: "npm:^3.3.11"
+ checksum: 10/67fca3e78d83336747de98a0b6797cba394e73a717fd0fad6add5434c7938327501ed71962bd8916c94141c552013f081f45de81960dd4c9c0d918174b732c49
+ languageName: node
+ linkType: hard
+
"@rneui/base@npm:4.0.0-rc.2":
version: 4.0.0-rc.2
resolution: "@rneui/base@npm:4.0.0-rc.2"
@@ -2932,6 +3436,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/hammerjs@npm:^2.0.36":
+ version: 2.0.46
+ resolution: "@types/hammerjs@npm:2.0.46"
+ checksum: 10/1b6502d668f45ca49fb488c01f7938d3aa75e989d70c64801c8feded7d659ca1a118f745c1b604d220efe344c93231767d5cc68c05e00e069c14539b6143cfd9
+ languageName: node
+ linkType: hard
+
"@types/html-minifier-terser@npm:^6.0.0":
version: 6.1.0
resolution: "@types/html-minifier-terser@npm:6.1.0"
@@ -4507,7 +5018,7 @@ __metadata:
languageName: node
linkType: hard
-"color-string@npm:^1.6.0":
+"color-string@npm:^1.6.0, color-string@npm:^1.9.0":
version: 1.9.1
resolution: "color-string@npm:1.9.1"
dependencies:
@@ -4527,6 +5038,16 @@ __metadata:
languageName: node
linkType: hard
+"color@npm:^4.2.3":
+ version: 4.2.3
+ resolution: "color@npm:4.2.3"
+ dependencies:
+ color-convert: "npm:^2.0.1"
+ color-string: "npm:^1.9.0"
+ checksum: 10/b23f5e500a79ea22428db43d1a70642d983405c0dd1f95ef59dbdb9ba66afbb4773b334fa0b75bb10b0552fd7534c6b28d4db0a8b528f91975976e70973c0152
+ languageName: node
+ linkType: hard
+
"colord@npm:^2.9.1":
version: 2.9.3
resolution: "colord@npm:2.9.3"
@@ -5028,6 +5549,13 @@ __metadata:
languageName: node
linkType: hard
+"decode-uri-component@npm:^0.2.2":
+ version: 0.2.2
+ resolution: "decode-uri-component@npm:0.2.2"
+ checksum: 10/17a0e5fa400bf9ea84432226e252aa7b5e72793e16bf80b907c99b46a799aeacc139ec20ea57121e50c7bd875a1a4365928f884e92abf02e21a5a13790a0f33e
+ languageName: node
+ linkType: hard
+
"decompress-response@npm:^6.0.0":
version: 6.0.0
resolution: "decompress-response@npm:6.0.0"
@@ -5593,6 +6121,9 @@ __metadata:
"@faker-js/faker": "npm:^8.0.2"
"@quidone/react-native-wheel-picker-feedback": "npm:^2.0.0"
"@react-native-picker/picker": "npm:2.9.0"
+ "@react-navigation/elements": "npm:^2.5.2"
+ "@react-navigation/native": "npm:^7.1.14"
+ "@react-navigation/native-stack": "npm:^7.3.21"
"@rozhkov/react-useful-hooks": "npm:^1.0.9"
babel-loader: "npm:^8.1.0"
babel-plugin-module-resolver: "npm:^4.1.0"
@@ -5604,7 +6135,10 @@ __metadata:
react-native: "npm:0.76.7"
react-native-builder-bob: "npm:^0.38.4"
react-native-elements: "npm:^4.0.0-rc.2"
+ react-native-gesture-handler: "npm:~2.20.2"
+ react-native-reanimated: "npm:~3.16.1"
react-native-safe-area-context: "npm:4.12.0"
+ react-native-screens: "npm:~4.4.0"
react-native-web: "npm:~0.19.10"
languageName: unknown
linkType: soft
@@ -5996,6 +6530,13 @@ __metadata:
languageName: node
linkType: hard
+"filter-obj@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "filter-obj@npm:1.1.0"
+ checksum: 10/9d681939eec2b4b129cb4f307b7e93d954a0657421d4e5357d86093b26d3f4f570909ed43717dcfd62428b3cf8cddd9841b35f9d40d12ac62cfabaa677942593
+ languageName: node
+ linkType: hard
+
"finalhandler@npm:1.1.2":
version: 1.1.2
resolution: "finalhandler@npm:1.1.2"
@@ -6630,7 +7171,7 @@ __metadata:
languageName: node
linkType: hard
-"hoist-non-react-statics@npm:^3.3.2":
+"hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2":
version: 3.3.2
resolution: "hoist-non-react-statics@npm:3.3.2"
dependencies:
@@ -8851,7 +9392,7 @@ __metadata:
languageName: node
linkType: hard
-"nanoid@npm:^3.3.7, nanoid@npm:^3.3.8":
+"nanoid@npm:^3.3.11, nanoid@npm:^3.3.7, nanoid@npm:^3.3.8":
version: 3.3.11
resolution: "nanoid@npm:3.3.11"
bin:
@@ -10048,7 +10589,7 @@ __metadata:
languageName: node
linkType: hard
-"prop-types@npm:^15.8.1":
+"prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
version: 15.8.1
resolution: "prop-types@npm:15.8.1"
dependencies:
@@ -10104,6 +10645,18 @@ __metadata:
languageName: node
linkType: hard
+"query-string@npm:^7.1.3":
+ version: 7.1.3
+ resolution: "query-string@npm:7.1.3"
+ dependencies:
+ decode-uri-component: "npm:^0.2.2"
+ filter-obj: "npm:^1.1.0"
+ split-on-first: "npm:^1.0.0"
+ strict-uri-encode: "npm:^2.0.0"
+ checksum: 10/3b6f2c167e76ca4094c5f1a9eb276efcbb9ebfd8b1a28c413f3c4e4e7d6428c8187bf46c8cbc9f92a229369dd0015de10a7fd712c8cee98d5d84c2ac6140357e
+ languageName: node
+ linkType: hard
+
"queue-microtask@npm:^1.2.2":
version: 1.2.3
resolution: "queue-microtask@npm:1.2.3"
@@ -10191,6 +10744,15 @@ __metadata:
languageName: node
linkType: hard
+"react-freeze@npm:^1.0.0":
+ version: 1.0.4
+ resolution: "react-freeze@npm:1.0.4"
+ peerDependencies:
+ react: ">=17.0.0"
+ checksum: 10/1dc433319341ec3dca84513c4197ef4f4c8232604d35f83546a8abfb41d9591f934b66aaaa4dc3dc8b1b65f488705a2a48ae6c1d9792660119a9cdedeab4ca8f
+ languageName: node
+ linkType: hard
+
"react-is@npm:^16.13.1, react-is@npm:^16.7.0":
version: 16.13.1
resolution: "react-is@npm:16.13.1"
@@ -10205,6 +10767,13 @@ __metadata:
languageName: node
linkType: hard
+"react-is@npm:^19.1.0":
+ version: 19.1.0
+ resolution: "react-is@npm:19.1.0"
+ checksum: 10/4aceca94492032a19411138bf68c6ca7dce4abee7ae6c7f23db9291d2dff14d6a643717b176ef9a151e8cdf6a2b71fdbc59fd998328e086a6f752512304a252b
+ languageName: node
+ linkType: hard
+
"react-native-builder-bob@npm:^0.38.4":
version: 0.38.4
resolution: "react-native-builder-bob@npm:0.38.4"
@@ -10247,6 +10816,21 @@ __metadata:
languageName: node
linkType: hard
+"react-native-gesture-handler@npm:~2.20.2":
+ version: 2.20.2
+ resolution: "react-native-gesture-handler@npm:2.20.2"
+ dependencies:
+ "@egjs/hammerjs": "npm:^2.0.17"
+ hoist-non-react-statics: "npm:^3.3.0"
+ invariant: "npm:^2.2.4"
+ prop-types: "npm:^15.7.2"
+ peerDependencies:
+ react: "*"
+ react-native: "*"
+ checksum: 10/64ab125c539ca8c275f5d305f5e11d366e6098d9e24e3cab25cbfd46a8d618fc3925ea86219972ccc63364e578384bb0120a72562312e596894a04ee0518a363
+ languageName: node
+ linkType: hard
+
"react-native-ratings@npm:^8.1.0":
version: 8.1.0
resolution: "react-native-ratings@npm:8.1.0"
@@ -10259,6 +10843,29 @@ __metadata:
languageName: node
linkType: hard
+"react-native-reanimated@npm:~3.16.1":
+ version: 3.16.7
+ resolution: "react-native-reanimated@npm:3.16.7"
+ dependencies:
+ "@babel/plugin-transform-arrow-functions": "npm:^7.0.0-0"
+ "@babel/plugin-transform-class-properties": "npm:^7.0.0-0"
+ "@babel/plugin-transform-classes": "npm:^7.0.0-0"
+ "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.0.0-0"
+ "@babel/plugin-transform-optional-chaining": "npm:^7.0.0-0"
+ "@babel/plugin-transform-shorthand-properties": "npm:^7.0.0-0"
+ "@babel/plugin-transform-template-literals": "npm:^7.0.0-0"
+ "@babel/plugin-transform-unicode-regex": "npm:^7.0.0-0"
+ "@babel/preset-typescript": "npm:^7.16.7"
+ convert-source-map: "npm:^2.0.0"
+ invariant: "npm:^2.2.4"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ react: "*"
+ react-native: "*"
+ checksum: 10/37099178efca335ea74bb0fc1b50ad898c2b24b1ed99e67bfb90e8898e1fadace2d2765a275d7d23b9c141d19fc673bb4b78dcb9f47d08921dd911c76b5040aa
+ languageName: node
+ linkType: hard
+
"react-native-safe-area-context@npm:4.12.0":
version: 4.12.0
resolution: "react-native-safe-area-context@npm:4.12.0"
@@ -10269,6 +10876,19 @@ __metadata:
languageName: node
linkType: hard
+"react-native-screens@npm:~4.4.0":
+ version: 4.4.0
+ resolution: "react-native-screens@npm:4.4.0"
+ dependencies:
+ react-freeze: "npm:^1.0.0"
+ warn-once: "npm:^0.1.0"
+ peerDependencies:
+ react: "*"
+ react-native: "*"
+ checksum: 10/a70d036674611b327c01e6c3a147b9d22b59d7e58cfd82a9df5a070fac26af17b65bcd1ec911ec3516c6d9e2782a48577b5ac2f576a196ad5fff1fddcf17bf38
+ languageName: node
+ linkType: hard
+
"react-native-size-matters@npm:^0.4.0":
version: 0.4.2
resolution: "react-native-size-matters@npm:0.4.2"
@@ -11270,6 +11890,13 @@ __metadata:
languageName: node
linkType: hard
+"split-on-first@npm:^1.0.0":
+ version: 1.1.0
+ resolution: "split-on-first@npm:1.1.0"
+ checksum: 10/16ff85b54ddcf17f9147210a4022529b343edbcbea4ce977c8f30e38408b8d6e0f25f92cd35b86a524d4797f455e29ab89eb8db787f3c10708e0b47ebf528d30
+ languageName: node
+ linkType: hard
+
"split@npm:^1.0.1":
version: 1.0.1
resolution: "split@npm:1.0.1"
@@ -11364,6 +11991,13 @@ __metadata:
languageName: node
linkType: hard
+"strict-uri-encode@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "strict-uri-encode@npm:2.0.0"
+ checksum: 10/eaac4cf978b6fbd480f1092cab8b233c9b949bcabfc9b598dd79a758f7243c28765ef7639c876fa72940dac687181b35486ea01ff7df3e65ce3848c64822c581
+ languageName: node
+ linkType: hard
+
"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3":
version: 4.2.3
resolution: "string-width@npm:4.2.3"
@@ -12042,6 +12676,24 @@ __metadata:
languageName: node
linkType: hard
+"use-latest-callback@npm:^0.2.4":
+ version: 0.2.4
+ resolution: "use-latest-callback@npm:0.2.4"
+ peerDependencies:
+ react: ">=16.8"
+ checksum: 10/60c3a6b1b6567e1794f9e48cd86b8cde8a149485cc2fed60570f69ec3b157f6812e0ff0a877f0b971592fb9254b1363cc21c120fd1fc993b1dad1406c69211df
+ languageName: node
+ linkType: hard
+
+"use-sync-external-store@npm:^1.5.0":
+ version: 1.5.0
+ resolution: "use-sync-external-store@npm:1.5.0"
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ checksum: 10/ddae7c4572511f7f641d6977bd0725340aa7dbeda8250418b54c1a57ec285083d96cf50d1a1acbd6cf729f7a87071b2302c6fbd29310432bf1b21a961a313279
+ languageName: node
+ linkType: hard
+
"util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1":
version: 1.0.2
resolution: "util-deprecate@npm:1.0.2"
@@ -12120,6 +12772,13 @@ __metadata:
languageName: node
linkType: hard
+"warn-once@npm:^0.1.0, warn-once@npm:^0.1.1":
+ version: 0.1.1
+ resolution: "warn-once@npm:0.1.1"
+ checksum: 10/e6a5a1f5a8dba7744399743d3cfb571db4c3947897875d4962a7c5b1bf2195ab4518c838cb4cea652e71729f21bba2e98dc75686f5fccde0fabbd894e2ed0c0d
+ languageName: node
+ linkType: hard
+
"watchpack@npm:^2.4.1":
version: 2.4.2
resolution: "watchpack@npm:2.4.2"
diff --git a/local-namespace-config.js b/local-namespace-config.js
index 7f993af..2ccf2c3 100644
--- a/local-namespace-config.js
+++ b/local-namespace-config.js
@@ -1,6 +1,7 @@
module.exports = {
'@implementation/virtualized': './src/hoc/virtualized/index',
'@implementation/base': './src/base/index',
+ '@implementation/picker-control': './src/picker-control/index',
'@utils/react': './src/utils/react/index',
'@utils/math': './src/utils/math/index',
'@utils/debounce': './src/utils/debounce/index',
diff --git a/package.json b/package.json
index 58c2700..b0984fc 100644
--- a/package.json
+++ b/package.json
@@ -65,7 +65,9 @@
"react-native": ">=0.71.6"
},
"dependencies": {
- "@rozhkov/react-useful-hooks": "^1.0.10"
+ "@rozhkov/react-useful-hooks": "^1.0.10",
+ "date-fns": "^4.1.0",
+ "nanoevents": "^9.1.0"
},
"devDependencies": {
"@babel/cli": "^7.21.5",
diff --git a/src/base/index.ts b/src/base/index.ts
index e9e05ee..9cc53bc 100644
--- a/src/base/index.ts
+++ b/src/base/index.ts
@@ -17,6 +17,6 @@ export type {
export {useScrollContentOffset} from './contexts/ScrollContentOffsetContext';
export {usePickerItemHeight} from './contexts/PickerItemHeightContext';
-export {PickerProps} from './picker/Picker';
+export {type PickerProps, useValueIndex} from './picker/Picker';
import WheelPicker from './picker/Picker';
export default WheelPicker;
diff --git a/src/base/list/List.tsx b/src/base/list/List.tsx
index b302cf2..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
@@ -112,6 +114,7 @@ const List = >(
ref={listRef}
contentOffset={initialOffset}
onScroll={onScroll}
+ scrollOffset={scrollOffset}
snapToOffsets={snapToOffsets}
style={styles.list}
contentContainerStyle={contentContainerStyle}
@@ -120,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 8e77706..6737528 100644
--- a/src/base/picker/Picker.tsx
+++ b/src/base/picker/Picker.tsx
@@ -22,12 +22,13 @@ 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> = {
data: ReadonlyArray;
value?: ItemT['value'];
+ extraValues?: unknown[];
itemHeight?: number;
visibleItemCount?: number;
width?: number | 'auto' | `${number}%`;
@@ -50,6 +51,10 @@ export type PickerProps> = {
contentContainerStyle?: StyleProp;
scrollEventThrottle?: number;
+
+ _enableSyncScrollAfterScrollEnd?: boolean;
+ _onScrollStart?: () => void;
+ _onScrollEnd?: () => void;
};
const defaultKeyExtractor: KeyExtractor = (_, index) => index.toString();
@@ -72,7 +77,10 @@ const defaultRenderList: RenderList = (props) => {
return
;
};
-const useValueIndex = (data: ReadonlyArray>, value: any) => {
+export const useValueIndex = (
+ data: ReadonlyArray>,
+ value: any,
+) => {
return useMemo(() => {
const index = data.findIndex((x) => x.value === value);
return index >= 0 ? index : 0;
@@ -82,6 +90,7 @@ const useValueIndex = (data: ReadonlyArray>, value: any) => {
const Picker = >({
data,
value,
+ extraValues = [],
width = 'auto',
itemHeight = 48,
visibleItemCount = 5,
@@ -102,11 +111,22 @@ const Picker = >({
itemTextStyle,
overlayItemStyle,
contentContainerStyle,
+
+ _enableSyncScrollAfterScrollEnd = true,
+ _onScrollStart,
+ _onScrollEnd,
...restProps
}: PickerProps) => {
const valueIndex = useValueIndex(data, value);
+ console.log('[Picker] [value]', value);
+ console.log('[Picker] [valueIndex]', valueIndex);
+
const initialIndex = useInit(() => valueIndex);
- const offsetY = useRef(new Animated.Value(valueIndex * itemHeight)).current;
+ const offsetY = useMemo(
+ () => new Animated.Value(valueIndex * itemHeight),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [readOnly], // when scrollEnabled changes, the events stop coming. Re-creating
+ );
const listRef = useRef(null);
const touching = useBoolean(false);
@@ -138,20 +158,31 @@ 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,
+ extraValues,
activeIndexRef,
touching: touching.value,
+ enableSyncScrollAfterScrollEnd: _enableSyncScrollAfterScrollEnd,
+ });
+
+ const onScrollEnd = useStableCallback(() => {
+ // consistency matters
+ _onScrollEnd?.();
+ onScrollEndForValueEvents();
+ onScrollEndForSyncScroll();
});
return (
@@ -176,6 +207,7 @@ const Picker = >({
onTouchStart: touching.setTrue,
onTouchEnd: touching.setFalse,
onTouchCancel: touching.setFalse,
+ onScrollStart: _onScrollStart,
onScrollEnd,
contentContainerStyle,
})}
diff --git a/src/base/picker/hooks/useSyncScrollEffect.ts b/src/base/picker/hooks/useSyncScrollEffect.ts
index 6983fee..0b79aec 100644
--- a/src/base/picker/hooks/useSyncScrollEffect.ts
+++ b/src/base/picker/hooks/useSyncScrollEffect.ts
@@ -1,18 +1,26 @@
-import {type RefObject, useEffect} from 'react';
+import {type RefObject, 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,
+ enableSyncScrollAfterScrollEnd,
}: {
listRef: RefObject;
+ value: unknown;
valueIndex: number;
+ extraValues: unknown[] | undefined;
activeIndexRef: RefObject;
touching: boolean;
+ enableSyncScrollAfterScrollEnd: boolean;
}) => {
- useEffect(() => {
+ const syncScroll = useStableCallback(() => {
if (
listRef.current == null ||
touching ||
@@ -22,7 +30,23 @@ const useSyncScrollEffect = ({
}
listRef.current.scrollToIndex({index: valueIndex, animated: true});
- }, [valueIndex]); // eslint-disable-line react-hooks/exhaustive-deps
+ });
+
+ useEffectWithDynamicDepsLength(() => {
+ syncScroll();
+ }, [valueIndex, enableSyncScrollAfterScrollEnd, ...extraValues]);
+
+ const timeoutId = useRef(undefined);
+ const onScrollEnd = useStableCallback(() => {
+ if (enableSyncScrollAfterScrollEnd && value !== undefined) {
+ clearTimeout(timeoutId.current);
+ timeoutId.current = setTimeout(syncScroll, 0);
+ }
+ });
+
+ return {
+ onScrollEnd,
+ };
};
export default useSyncScrollEffect;
diff --git a/src/base/types.ts b/src/base/types.ts
index 29463eb..155dbb5 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;
@@ -76,7 +77,7 @@ export type RenderOverlay = (
) => React.ReactElement | null;
// events
-export type ValueChangingEvent = {item: ItemT; index: number};
-export type ValueChangedEvent = {item: ItemT; index: number};
-export type OnValueChanging = (event: ValueChangingEvent) => void;
-export type OnValueChanged = (event: ValueChangedEvent) => void;
+export type ValueChangingEvent> = {item: ItemT; index: number};
+export type ValueChangedEvent> = {item: ItemT; index: number};
+export type OnValueChanging> = (event: ValueChangingEvent) => void;
+export type OnValueChanged> = (event: ValueChangedEvent) => void;
diff --git a/src/date/DatePicker.tsx b/src/date/DatePicker.tsx
new file mode 100644
index 0000000..362bbf9
--- /dev/null
+++ b/src/date/DatePicker.tsx
@@ -0,0 +1,100 @@
+import React, {ReactNode} from 'react';
+import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
+import DatePickerValueProvider from './DatePickerValueProvider';
+import DatePickerDates from './DatePickerDate';
+import DatePickerMonth from './DatePickerMonth';
+import DatePickerYear from './DatePickerYear';
+import DatePickerContainer, {type DateNodeType} from './DatePickerContainer';
+import type {OnlyDateFormat} from './date';
+import DatePickerLocaleProvider from './DatePickerLocaleProvider';
+import DatePickerCommonPropsProvider from './DatePickerCommonPropsProvider';
+
+type DatePickerProps = {
+ date: OnlyDateFormat;
+ onDateChanged: (event: {date: OnlyDateFormat}) => void;
+ minDate?: OnlyDateFormat;
+ maxDate?: OnlyDateFormat;
+ locale?: string;
+
+ renderDate?: () => ReactNode;
+ renderMonth?: () => ReactNode;
+ renderYear?: () => ReactNode;
+ children?: (props: {
+ dateNodes: {node: ReactNode; type: DateNodeType}[];
+ }) => ReactNode;
+
+ // region common props for all child wheel pickers
+ itemHeight?: number;
+ visibleItemCount?: number;
+ readOnly?: boolean;
+ enableScrollByTapOnItem?: boolean;
+ scrollEventThrottle?: number;
+ pickerStyle?: StyleProp;
+ itemTextStyle?: StyleProp;
+ overlayItemStyle?: StyleProp;
+ contentContainerStyle?: StyleProp;
+ // endregion
+};
+
+const DatePickerComponent = ({
+ date,
+ onDateChanged,
+ minDate,
+ maxDate,
+ locale,
+ renderDate,
+ renderMonth,
+ renderYear,
+ children = ({dateNodes}) => <>{dateNodes.map((dateNode) => dateNode.node)}>,
+
+ // region common props for all child wheel pickers
+ itemHeight,
+ visibleItemCount,
+ readOnly,
+ enableScrollByTapOnItem,
+ scrollEventThrottle,
+ pickerStyle,
+ itemTextStyle,
+ overlayItemStyle,
+ contentContainerStyle,
+}: // endregion
+DatePickerProps) => {
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
+};
+
+DatePickerComponent.displayName = 'DatePicker';
+
+export const DatePicker = Object.assign(DatePickerComponent, {
+ Date: DatePickerDates,
+ Month: DatePickerMonth,
+ Year: DatePickerYear,
+});
diff --git a/src/date/DatePickerCommonPropsProvider.tsx b/src/date/DatePickerCommonPropsProvider.tsx
new file mode 100644
index 0000000..5fb08ed
--- /dev/null
+++ b/src/date/DatePickerCommonPropsProvider.tsx
@@ -0,0 +1,113 @@
+import React, {
+ type ComponentType,
+ createContext,
+ type ForwardedRef,
+ forwardRef,
+ memo,
+ type PropsWithChildren,
+ useContext,
+} from 'react';
+import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
+import {useMemoObject} from '@rozhkov/react-useful-hooks';
+import type {PickerProps} from '@implementation/base';
+
+type ContextValue = {
+ itemHeight: number | undefined;
+ visibleItemCount: number | undefined;
+ readOnly: boolean | undefined;
+ enableScrollByTapOnItem: boolean | undefined;
+ scrollEventThrottle: number | undefined;
+ pickerStyle: StyleProp | undefined;
+ itemTextStyle: StyleProp | undefined;
+ overlayItemStyle: StyleProp | undefined;
+ contentContainerStyle: StyleProp | undefined;
+};
+
+const DatePickerCommonPropsContext = createContext(
+ undefined,
+);
+
+type DatePickerCommonPropsProviderProps = PropsWithChildren;
+
+const DatePickerCommonPropsProvider = ({
+ children,
+ ...restProps
+}: DatePickerCommonPropsProviderProps) => {
+ const memoizedValue = useMemoObject(restProps);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default DatePickerCommonPropsProvider;
+
+const useDatePickerCommonProps = () => {
+ const value = useContext(DatePickerCommonPropsContext);
+ if (value === undefined) {
+ throw new Error(
+ 'useDatePickerCommonProps must be called from within DatePickerCommonPropsContext.Provider!',
+ );
+ }
+ return useContext(DatePickerCommonPropsContext)!;
+};
+
+type PickedWheelPickerProps = Pick<
+ PickerProps,
+ Exclude | 'style'
+>;
+
+export const withCommonProps = (
+ WheelPickerComponent: ComponentType,
+) => {
+ const WrappedWheelPicker = (
+ {
+ style: pickerStyleProp,
+ contentContainerStyle: contentContainerStyleProp,
+ itemTextStyle: itemTextStyleProp,
+ overlayItemStyle: overlayItemStyleProp,
+ ...restProps
+ }: PickedWheelPickerProps,
+ forwardedRef: ForwardedRef,
+ ) => {
+ const {
+ pickerStyle: pickerStyleCommon,
+ contentContainerStyle: contentContainerStyleCommon,
+ itemTextStyle: itemTextStyleCommon,
+ overlayItemStyle: overlayItemStyleCommon,
+ ...restCommonProps
+ } = useDatePickerCommonProps();
+
+ const style = useMemoObject([pickerStyleCommon, pickerStyleProp]);
+ const contentContainerStyle = useMemoObject([
+ contentContainerStyleCommon,
+ contentContainerStyleProp,
+ ]);
+ const itemTextStyle = useMemoObject([
+ itemTextStyleCommon,
+ itemTextStyleProp,
+ ]);
+ const overlayItemStyle = useMemoObject([
+ overlayItemStyleCommon,
+ overlayItemStyleProp,
+ ]);
+
+ return (
+
+ );
+ };
+
+ WrappedWheelPicker.displayName = `withDateCommonProps(${WheelPickerComponent.displayName})`;
+
+ return memo(forwardRef(WrappedWheelPicker)) as typeof WheelPickerComponent;
+};
diff --git a/src/date/DatePickerContainer.tsx b/src/date/DatePickerContainer.tsx
new file mode 100644
index 0000000..2f8c2b4
--- /dev/null
+++ b/src/date/DatePickerContainer.tsx
@@ -0,0 +1,45 @@
+import React, {Fragment, ReactNode} from 'react';
+import {View} from 'react-native';
+import DatePickerDate from './DatePickerDate';
+import DatePickerMonth from './DatePickerMonth';
+import DatePickerYear from './DatePickerYear';
+import {useDatePickerLocale} from './DatePickerLocaleProvider';
+
+export type DateNodeType = 'date' | 'month' | 'year';
+
+type DatePickerContainerProps = {
+ renderDate?: () => ReactNode;
+ renderMonth?: () => ReactNode;
+ renderYear?: () => ReactNode;
+
+ children: (props: {
+ dateNodes: {node: ReactNode; type: DateNodeType}[];
+ }) => ReactNode;
+};
+
+const DatePickerContainer = ({
+ renderDate = () => ,
+ renderMonth = () => ,
+ renderYear = () => ,
+ children,
+}: DatePickerContainerProps) => {
+ const localeData = useDatePickerLocale();
+ const typeToRenderMap: Record ReactNode> = {
+ date: renderDate,
+ month: renderMonth,
+ year: renderYear,
+ };
+
+ return (
+
+ {children({
+ dateNodes: localeData.sortedDateUnitTypes.map((type) => ({
+ type,
+ node: {typeToRenderMap[type]()},
+ })),
+ })}
+
+ );
+};
+
+export default DatePickerContainer;
diff --git a/src/date/DatePickerDate.tsx b/src/date/DatePickerDate.tsx
new file mode 100644
index 0000000..c3e3d2f
--- /dev/null
+++ b/src/date/DatePickerDate.tsx
@@ -0,0 +1,51 @@
+import React, {memo, useMemo} from 'react';
+import Picker, {type PickerProps} from '@implementation/base';
+import {withPickerControl} from '@implementation/picker-control';
+import {useDateContext} from './DatePickerValueProvider';
+import {useOverlayItemStyle} from './useOverlayItemStyle';
+import {DateUtils} from './date';
+import {useDatePickerLocale} from './DatePickerLocaleProvider';
+import {withCommonProps} from './DatePickerCommonPropsProvider';
+
+const HocPicker = withCommonProps(withPickerControl(Picker));
+
+export type DatePickerDateProps = Omit<
+ PickerProps<{value: number}>,
+ 'value' | 'data'
+>;
+
+const DatePickerDate = ({
+ width = 60,
+ overlayItemStyle: overlayItemStyleProp,
+ ...restProps
+}: DatePickerDateProps) => {
+ const localeData = useDatePickerLocale();
+ const dateContext = useDateContext();
+ const value = dateContext.value;
+ const daysInMount = DateUtils.getDaysInMonth(value.year, value.month);
+ const data = useMemo(() => {
+ return [...Array(daysInMount).keys()].map((index) => ({
+ value: index + 1,
+ }));
+ }, [daysInMount]);
+
+ const overlayItemStyle = useOverlayItemStyle({
+ curUnit: 'date',
+ unitPositions: localeData.sortedDateUnitTypes,
+ propStyle: overlayItemStyleProp,
+ });
+
+ return (
+
+ );
+};
+
+export default memo(DatePickerDate);
diff --git a/src/date/DatePickerLocaleProvider.tsx b/src/date/DatePickerLocaleProvider.tsx
new file mode 100644
index 0000000..614bc5a
--- /dev/null
+++ b/src/date/DatePickerLocaleProvider.tsx
@@ -0,0 +1,51 @@
+import React, {
+ createContext,
+ type PropsWithChildren,
+ useContext,
+ useMemo,
+} from 'react';
+import {type DateUnitType, DateUtils} from './date';
+
+type ContextValue = {
+ locale: string;
+ sortedDateUnitTypes: DateUnitType[];
+ monthLongNames: string[];
+};
+
+const DatePickerLocaleContext = createContext(
+ undefined,
+);
+
+type DatePickerLocaleProviderProps = PropsWithChildren<{locale?: string}>;
+
+const DatePickerLocaleProvider = ({
+ locale = 'en',
+ children,
+}: DatePickerLocaleProviderProps) => {
+ const value = useMemo(
+ () => ({
+ locale,
+ monthLongNames: DateUtils.getLocalizedMonthNames(locale),
+ sortedDateUnitTypes: DateUtils.getSortedDateUnitPositions(locale),
+ }),
+ [locale],
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default DatePickerLocaleProvider;
+
+export const useDatePickerLocale = () => {
+ const value = useContext(DatePickerLocaleContext);
+ if (value === undefined) {
+ throw new Error(
+ 'useDatePickerLocale must be called from within DatePickerLocaleContext.Provider!',
+ );
+ }
+ return useContext(DatePickerLocaleContext)!;
+};
diff --git a/src/date/DatePickerMonth.tsx b/src/date/DatePickerMonth.tsx
new file mode 100644
index 0000000..ed1d6a5
--- /dev/null
+++ b/src/date/DatePickerMonth.tsx
@@ -0,0 +1,51 @@
+import React, {memo, useMemo} from 'react';
+import Picker, {type PickerProps} from '@implementation/base';
+import {useDateContext} from './DatePickerValueProvider';
+import {useOverlayItemStyle} from './useOverlayItemStyle';
+import {useDatePickerLocale} from './DatePickerLocaleProvider';
+import {withCommonProps} from './DatePickerCommonPropsProvider';
+import {DateUtils} from './date';
+import {withPickerControl} from '@implementation/picker-control';
+
+const HocPicker = withCommonProps(withPickerControl(Picker));
+
+export type DatePickerMonthProps = Omit<
+ PickerProps<{value: number}>,
+ 'value' | 'data'
+>;
+
+const DatePickerMonth = ({
+ width = 120,
+ overlayItemStyle: overlayItemStyleProp,
+ ...restProps
+}: DatePickerMonthProps) => {
+ const localeData = useDatePickerLocale();
+ const dateContext = useDateContext();
+ const value = dateContext.value;
+ const data = useMemo(() => {
+ return [...Array(DateUtils.MONTH_COUNT).keys()].map((index) => ({
+ value: index,
+ label: localeData.monthLongNames[index],
+ }));
+ }, [localeData.monthLongNames]);
+
+ const overlayItemStyle = useOverlayItemStyle({
+ curUnit: 'month',
+ unitPositions: localeData.sortedDateUnitTypes,
+ propStyle: overlayItemStyleProp,
+ });
+
+ return (
+
+ );
+};
+
+export default memo(DatePickerMonth);
diff --git a/src/date/DatePickerValueProvider.tsx b/src/date/DatePickerValueProvider.tsx
new file mode 100644
index 0000000..cb7c769
--- /dev/null
+++ b/src/date/DatePickerValueProvider.tsx
@@ -0,0 +1,121 @@
+import React, {
+ createContext,
+ type PropsWithChildren,
+ useContext,
+ useMemo,
+} from 'react';
+import {isSameDay} from 'date-fns';
+import type {PickerItem} from '@implementation/base';
+import {
+ PickerControl,
+ useOnPickerValueChangedEffect,
+ usePickerControl,
+} from '@implementation/picker-control';
+import {
+ DateLocale,
+ DateUtils,
+ type OnlyDateFormat,
+ type OnlyDateUnits,
+} from './date';
+
+type ContextValue = {
+ pickerControl: PickerControl;
+ value: OnlyDateUnits;
+ max: Date;
+ min: Date;
+};
+
+type ControlPickersMap = {
+ year: {item: PickerItem};
+ month: {item: PickerItem};
+ date: {item: PickerItem};
+};
+
+const DatePickerContext = createContext(undefined);
+
+type DatePickerValueProviderProps = PropsWithChildren<{
+ date: OnlyDateFormat;
+ locale?: DateLocale;
+ minDate?: OnlyDateFormat;
+ maxDate?: OnlyDateFormat;
+ onDateChanged: (event: {date: OnlyDateFormat}) => void;
+}>;
+
+const DatePickerValueProvider = ({
+ date,
+ maxDate,
+ minDate,
+ onDateChanged,
+ children,
+}: DatePickerValueProviderProps) => {
+ const {min, max} = useMemo(() => {
+ const now = new Date();
+
+ const getMaxDefault = () => {
+ const year = now.getFullYear() + 100;
+ const month = 11;
+ return new Date(year, month, DateUtils.getDaysInMonth(year, month));
+ };
+ const getMinDefault = () => new Date(now.getFullYear() - 100, 0, 1);
+
+ return {
+ max: maxDate ? new Date(maxDate) : getMaxDefault(),
+ min: minDate ? new Date(minDate) : getMinDefault(),
+ };
+ }, [maxDate, minDate]);
+
+ const pickerControl = usePickerControl();
+
+ useOnPickerValueChangedEffect(pickerControl, (event) => {
+ const nextUnits = event.pickers.reduce((r, n) => {
+ r[n.name as keyof OnlyDateUnits] = n.item.value as number;
+ return r;
+ }, {} as OnlyDateUnits);
+
+ const curDateObj = new Date(date);
+ const dateObj = new Date(nextUnits.year, nextUnits.month, nextUnits.date);
+ const normalizedDateObj = DateUtils.withBoundaries(dateObj, min, max);
+
+ if (isSameDay(curDateObj, normalizedDateObj)) {
+ return;
+ }
+
+ onDateChanged?.({
+ date: DateUtils.toOnlyDateFormat({
+ year: normalizedDateObj.getFullYear(),
+ month: normalizedDateObj.getMonth(),
+ date: normalizedDateObj.getDate(),
+ }),
+ });
+ });
+
+ const value = useMemo(
+ () => ({
+ pickerControl,
+ value: DateUtils.toUnits(
+ DateUtils.withBoundaries(new Date(date), min, max),
+ ),
+ max,
+ min,
+ }),
+ [pickerControl, date, max, min],
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default DatePickerValueProvider;
+
+export const useDateContext = () => {
+ const value = useContext(DatePickerContext);
+ if (value === undefined) {
+ throw new Error(
+ 'useDateContext must be called from within DatePicker.Provider!',
+ );
+ }
+ return useContext(DatePickerContext)!;
+};
diff --git a/src/date/DatePickerYear.tsx b/src/date/DatePickerYear.tsx
new file mode 100644
index 0000000..b35afea
--- /dev/null
+++ b/src/date/DatePickerYear.tsx
@@ -0,0 +1,51 @@
+import React, {memo, useMemo} from 'react';
+import Picker, {type PickerProps} from '@implementation/base';
+import {useDateContext} from './DatePickerValueProvider';
+import {useOverlayItemStyle} from './useOverlayItemStyle';
+import {useDatePickerLocale} from './DatePickerLocaleProvider';
+import {withCommonProps} from './DatePickerCommonPropsProvider';
+import {withPickerControl} from '@implementation/picker-control';
+
+const HocPicker = withCommonProps(withPickerControl(Picker));
+
+export type DatePickerYearProps = Omit<
+ PickerProps<{value: number}>,
+ 'value' | 'data'
+>;
+
+const DatePickerYear = ({
+ width = 100,
+ overlayItemStyle: overlayItemStyleProp,
+ ...restProps
+}: DatePickerYearProps) => {
+ const localeData = useDatePickerLocale();
+ const dateContext = useDateContext();
+ const value = dateContext.value;
+ const data = useMemo(() => {
+ const startYear = dateContext.min.getFullYear();
+ const endYear = dateContext.max.getFullYear();
+ return Array.from({length: endYear - startYear + 1}, (_, index) => ({
+ value: startYear + index,
+ }));
+ }, [dateContext.max, dateContext.min]);
+
+ const overlayItemStyle = useOverlayItemStyle({
+ curUnit: 'year',
+ unitPositions: localeData.sortedDateUnitTypes,
+ propStyle: overlayItemStyleProp,
+ });
+
+ return (
+
+ );
+};
+
+export default memo(DatePickerYear);
diff --git a/src/date/date.ts b/src/date/date.ts
new file mode 100644
index 0000000..246f678
--- /dev/null
+++ b/src/date/date.ts
@@ -0,0 +1,115 @@
+import {isAfter, isBefore, isWithinInterval, startOfDay} from 'date-fns';
+
+export type OnlyDateFormat = string; // YYYY-MM-DD
+export type OnlyDateUnits = {date: number; month: number; year: number};
+
+const withBoundaries = (date: Date, min: Date, max: Date) => {
+ if (isBefore(date, min)) {
+ date = min;
+ } else if (isAfter(date, max)) {
+ date = max;
+ }
+ return date;
+};
+
+const toUnits = (date: OnlyDateFormat | Date): OnlyDateUnits => {
+ let dateObj = new Date(date);
+
+ return {
+ year: dateObj.getFullYear(),
+ month: dateObj.getMonth(),
+ date: dateObj.getDate(),
+ };
+};
+
+const inRange = (units: OnlyDateUnits, start: Date, end: Date) => {
+ return isWithinInterval(toDate(units), {
+ start: startOfDay(start),
+ end: startOfDay(end),
+ });
+};
+
+const toDate = (units: OnlyDateUnits) => {
+ return new Date(units.year, units.month, units.date);
+};
+
+const toOnlyDateFormat = (units: OnlyDateUnits): OnlyDateFormat => {
+ const date = new Date(units.year, units.month, units.date);
+ const year = date.getFullYear();
+ const month = date.getMonth() + 1; // getMonth() returns 0-11
+ const day = date.getDate();
+
+ // Pad single digit month and day with leading zeros
+ const monthFormatted = month < 10 ? `0${month}` : month;
+ const dayFormatted = day < 10 ? `0${day}` : day;
+
+ return `${year}-${monthFormatted}-${dayFormatted}`;
+};
+
+const getDaysInMonth = (year: number, month: number): number => {
+ return new Date(year, month + 1, 0).getDate();
+};
+
+export type DateUnitType = 'date' | 'month' | 'year';
+
+export type DateLocale = {
+ locale: string;
+};
+
+const getSortedDateUnitPositions = (locale: string) => {
+ const formatter = new Intl.DateTimeFormat(locale, {
+ year: 'numeric',
+ month: 'numeric',
+ day: 'numeric',
+ });
+ const parts = formatter.formatToParts(new Date());
+ const orderMap: Record = {
+ day: 'date',
+ month: 'month',
+ year: 'year',
+ };
+
+ // Создаем массив с правильным порядком элементов
+ const order: DateUnitType[] = [];
+ parts.forEach((part) => {
+ // part - это объект {type: string, value: string}
+ if (part.type in orderMap) {
+ order.push(orderMap[part.type as keyof typeof orderMap]!);
+ }
+ });
+
+ return order;
+};
+
+const getLocalizedMonthNames = (locale: string): string[] => {
+ const monthNames: string[] = [];
+ for (let monthIndex = 0; monthIndex < DateUtils.MONTH_COUNT; monthIndex++) {
+ const date = new Date(2025, monthIndex, 1); // Создаем дату для каждого месяца
+ const formatter = new Intl.DateTimeFormat(locale, {month: 'long'});
+ const monthName = formatter.format(date);
+ monthNames.push(monthName);
+ }
+ return monthNames;
+};
+
+const isFirstUnitPosition = (list: DateUnitType[], search: DateUnitType) => {
+ return list[0] === search;
+};
+const isLastUnitPosition = (list: DateUnitType[], search: DateUnitType) => {
+ return list.at(-1) === search;
+};
+
+export const DateUtils = {
+ MONTH_COUNT: 12,
+
+ toUnits,
+ toDate,
+ toOnlyDateFormat,
+ inRange,
+ withBoundaries,
+ getDaysInMonth,
+ getSortedDateUnitPositions,
+ getLocalizedMonthNames,
+ isFirstUnitPosition,
+ isLastUnitPosition,
+};
diff --git a/src/date/index.ts b/src/date/index.ts
new file mode 100644
index 0000000..312f946
--- /dev/null
+++ b/src/date/index.ts
@@ -0,0 +1,5 @@
+export {DatePicker} from './DatePicker';
+export {DatePickerDateProps} from './DatePickerDate';
+export {DatePickerMonthProps} from './DatePickerMonth';
+export {DatePickerYearProps} from './DatePickerYear';
+export {useDatePickerLocale} from './DatePickerLocaleProvider';
diff --git a/src/date/useOverlayItemStyle.ts b/src/date/useOverlayItemStyle.ts
new file mode 100644
index 0000000..e5803fa
--- /dev/null
+++ b/src/date/useOverlayItemStyle.ts
@@ -0,0 +1,37 @@
+import {useMemo} from 'react';
+import {type StyleProp, StyleSheet, type ViewStyle} from 'react-native';
+import {type DateUnitType, DateUtils} from './date';
+
+export const useOverlayItemStyle = ({
+ curUnit,
+ unitPositions,
+ propStyle,
+}: {
+ unitPositions: DateUnitType[];
+ curUnit: DateUnitType;
+ propStyle: StyleProp;
+}) => {
+ return useMemo(() => {
+ if (DateUtils.isFirstUnitPosition(unitPositions, curUnit)) {
+ return [dateStyles.leftItemOverlay, propStyle];
+ } else if (DateUtils.isLastUnitPosition(unitPositions, curUnit)) {
+ return [dateStyles.rightItemOverlay, propStyle];
+ } else {
+ return [dateStyles.zeroBorderRadius, propStyle];
+ }
+ }, [curUnit, propStyle, unitPositions]);
+};
+
+export const dateStyles = StyleSheet.create({
+ leftItemOverlay: {
+ borderRadius: 0,
+ borderTopLeftRadius: 8,
+ borderBottomLeftRadius: 8,
+ },
+ rightItemOverlay: {
+ borderRadius: 0,
+ borderTopRightRadius: 8,
+ borderBottomRightRadius: 8,
+ },
+ zeroBorderRadius: {borderRadius: 0},
+});
diff --git a/src/hoc/virtualized/VirtualizedList.tsx b/src/hoc/virtualized/VirtualizedList.tsx
index b367794..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,
@@ -119,12 +123,14 @@ const VirtualizedList = >(
getItemLayout={getItemLayout}
initialScrollIndex={initialIndex}
onScroll={onScroll}
+ scrollOffset={scrollOffset}
snapToOffsets={snapToOffsets}
style={styles.list}
contentContainerStyle={contentContainerStyle}
onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd}
onTouchCancel={onTouchCancel}
+ onScrollStart={onScrollStart}
onScrollEnd={onScrollEnd}
initialNumToRender={initialNumToRender ?? Math.ceil(visibleItemCount / 2)}
maxToRenderPerBatch={
diff --git a/src/index.tsx b/src/index.tsx
index 31559c1..f561910 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -27,3 +27,17 @@ export {
withVirtualized,
WithVirtualizedProps,
} from '@implementation/virtualized';
+
+export {
+ usePickerControl,
+ withPickerControl,
+ useOnPickerValueChangedEffect,
+ useOnPickerValueChangingEffect,
+} from '@implementation/picker-control';
+
+export {
+ DatePicker,
+ DatePickerYearProps,
+ DatePickerMonthProps,
+ DatePickerDateProps,
+} from './date';
diff --git a/src/picker-control/create-control.ts b/src/picker-control/create-control.ts
new file mode 100644
index 0000000..1b0276d
--- /dev/null
+++ b/src/picker-control/create-control.ts
@@ -0,0 +1,262 @@
+import type {
+ PickerItem,
+ ValueChangedEvent,
+ ValueChangingEvent,
+} from '@implementation/base';
+import {createNanoEvents, type Unsubscribe} from 'nanoevents';
+
+type PickerName = string;
+export type BaseControlConfig = Record}>;
+
+type MapToPickersFromConfig = {
+ [K in keyof T]: {name: K} & T[K];
+}[keyof T][];
+
+export type ControlEvents = {
+ onValueChanged: (event: {
+ pickers: MapToPickersFromConfig<
+ NonNullable
+ >;
+ }) => void;
+ onValueChanging: (event: {
+ pickers: MapToPickersFromConfig<
+ NonNullable
+ >;
+ }) => void;
+};
+
+type SubscriberEvents = {
+ // emit from subscriber
+ onValueChanged: (event: ValueChangedEvent>) => void;
+ onValueChanging: (event: ValueChangingEvent>) => void;
+ onNewPropValue: (event: {item: PickerItem}) => void;
+ onScrollStart: () => void;
+ onScrollEnd: () => void;
+
+ // emit to subscriber
+ onNewExtraValues: () => void;
+ onAllScrollEnd: () => void;
+};
+
+type SubId = string;
+let nextSubId = 1;
+const getNewSubId = (): SubId => `${++nextSubId}`;
+
+export type ControlSubscriber = {
+ getExtraValues: () => unknown[];
+ getEveryIsStopped: () => boolean;
+ omitOnValueChanged: (event: ValueChangedEvent>) => void;
+ omitOnValueChanging: (event: ValueChangingEvent>) => void;
+ omitOnNewPropValue: (event: {item: PickerItem}) => void;
+ omitOnScrollStart: () => void;
+ omitOnScrollEnd: () => void;
+ onNewExtraValues: (cb: () => void) => Unsubscribe;
+ onAllScrollEnd: (cb: () => void) => Unsubscribe;
+
+ disconnect: () => void;
+};
+
+export type Control = {
+ _connect: (info: {
+ pickerName: PickerName;
+ item: PickerItem;
+ }) => ControlSubscriber;
+
+ _on: (
+ event: NameT,
+ callback: ControlEvents[NameT],
+ ) => Unsubscribe;
+
+ // It's used to simplify typing from outside, in the future it makes sense to make better typing inside
+ __SAVED_TYPE_CONFIG__?: ConfigT;
+};
+
+export const createControl = <
+ ConfigT extends BaseControlConfig,
+>(): Control => {
+ const controlEmitter = createNanoEvents();
+
+ const subscribers: Record<
+ SubId,
+ {
+ pickerName: string;
+ item: PickerItem;
+ isStopped: boolean;
+ emitter: ReturnType>;
+ }
+ > = {};
+
+ const getEveryIsStopped = () => {
+ return Object.values(subscribers).every((s) => s.isStopped);
+ };
+
+ const notifyChangedExtraValues = (notifierPickerId: string) => {
+ Object.keys(subscribers).forEach((pickerId) => {
+ if (pickerId === notifierPickerId) {
+ return;
+ }
+ const sub = subscribers[pickerId]!;
+ sub.emitter.emit('onNewExtraValues');
+ });
+ };
+
+ return {
+ _on: (event, callback) => {
+ return controlEmitter.on(event, callback);
+ },
+ _connect: ({pickerName, item}) => {
+ if (__DEV__) {
+ const isExisted = Object.values(subscribers).some(
+ (s) => s.pickerName === pickerName,
+ );
+ if (isExisted) {
+ throw new Error(
+ `It is not possible to register 2 pickers with the same name "${pickerName}"`,
+ );
+ }
+ }
+
+ const subEmitter = createNanoEvents();
+ const subId = getNewSubId();
+ subscribers[subId] = {
+ pickerName,
+ item,
+ isStopped: true,
+ emitter: subEmitter,
+ };
+ const disconnect = () => {
+ delete subscribers[subId];
+ notifyChangedExtraValues(subId);
+ };
+
+ subEmitter.on('onNewPropValue', (event) => {
+ if (!subscribers[subId]) {
+ return;
+ }
+
+ subscribers[subId]!.item = event.item;
+ console.log(
+ '[create-control] subscribers',
+ Object.entries(subscribers).map(([_, data]) => [
+ data.pickerName,
+ data.item.value,
+ ]),
+ );
+ notifyChangedExtraValues(subId);
+ });
+ subEmitter.on('onValueChanged', (event) => {
+ if (!subscribers[subId]) {
+ return;
+ }
+
+ subscribers[subId]!.item = event.item;
+ console.log(
+ '[create-control] subscribers',
+ Object.entries(subscribers).map(([_, data]) => [
+ data.pickerName,
+ data.item.value,
+ ]),
+ );
+
+ const isAllStopped = getEveryIsStopped();
+ console.log('isAllStopped', isAllStopped);
+ if (isAllStopped) {
+ Object.keys(subscribers).forEach((pickerId) => {
+ const sub = subscribers[pickerId]!;
+ sub.emitter.emit('onAllScrollEnd');
+ });
+
+ controlEmitter.emit('onValueChanged', {
+ pickers: Object.values(subscribers).map((s) => ({
+ name: s.pickerName,
+ item: s.item,
+ })),
+ });
+ }
+ });
+ subEmitter.on('onValueChanging', (event) => {
+ if (!subscribers[subId]) {
+ return;
+ }
+
+ subscribers[subId]!.item = event.item;
+ console.log(
+ '[create-control] subscribers',
+ Object.entries(subscribers).map(([_, data]) => [
+ data.pickerName,
+ data.item.value,
+ ]),
+ );
+
+ controlEmitter.emit('onValueChanging', {
+ pickers: Object.values(subscribers).map((s) => ({
+ name: s.pickerName,
+ item: s.item,
+ })),
+ });
+ });
+ subEmitter.on('onScrollStart', () => {
+ if (!subscribers[subId]) {
+ return;
+ }
+
+ subscribers[subId]!.isStopped = false;
+ });
+ subEmitter.on('onScrollEnd', () => {
+ if (!subscribers[subId]) {
+ return;
+ }
+
+ subscribers[subId]!.isStopped = true;
+ });
+
+ // TODO change omit -> emit
+ return {
+ getExtraValues: () => {
+ return Object.keys(subscribers)
+ .filter((id) => id !== subId)
+ .map((id) => subscribers[id]!.item.value);
+ },
+ getEveryIsStopped,
+
+ omitOnNewPropValue: (...args) => {
+ console.log('[create-control] omitOnNewPropValue');
+ subEmitter.emit('onNewPropValue', ...args);
+ },
+ omitOnValueChanged: (...args) => {
+ console.log('[create-control] omitOnValueChanged');
+
+ subEmitter.emit('onValueChanged', ...args);
+ },
+ omitOnValueChanging: (...args) => {
+ // console.log('[create-control] omitOnValueChanging');
+
+ subEmitter.emit('onValueChanging', ...args);
+ },
+ omitOnScrollStart: () => {
+ console.log('[create-control] omitOnScrollStart');
+
+ subEmitter.emit('onScrollStart');
+ },
+ omitOnScrollEnd: () => {
+ console.log('[create-control] omitOnScrollEnd');
+
+ subEmitter.emit('onScrollEnd');
+ },
+
+ onNewExtraValues: (callback) => {
+ console.log('[create-control] onNewExtraValues');
+
+ return subEmitter.on('onNewExtraValues', callback);
+ },
+ onAllScrollEnd: (callback) => {
+ console.log('[create-control] onAllScrollEnd');
+
+ return subEmitter.on('onAllScrollEnd', callback);
+ },
+
+ disconnect,
+ } satisfies ControlSubscriber;
+ },
+ } satisfies Control;
+};
diff --git a/src/picker-control/index.ts b/src/picker-control/index.ts
new file mode 100644
index 0000000..e1c7396
--- /dev/null
+++ b/src/picker-control/index.ts
@@ -0,0 +1,9 @@
+export {Control as PickerControl} from './create-control';
+
+export {
+ usePickerControl,
+ useOnPickerValueChangingEffect,
+ useOnPickerValueChangedEffect,
+} from './usePickerControl';
+
+export {withPickerControl, WithPickerControlProps} from './withPickerControl';
diff --git a/src/picker-control/usePickerControl.ts b/src/picker-control/usePickerControl.ts
new file mode 100644
index 0000000..369d25c
--- /dev/null
+++ b/src/picker-control/usePickerControl.ts
@@ -0,0 +1,48 @@
+import {useEffect} from 'react';
+import {useInit, useStableCallback} from '@rozhkov/react-useful-hooks';
+import {
+ type BaseControlConfig,
+ type Control,
+ type ControlEvents,
+ createControl,
+} from './create-control';
+
+export const usePickerControl = <
+ Config extends BaseControlConfig = BaseControlConfig,
+>() => {
+ return useInit(() => createControl());
+};
+
+export const useOnPickerValueChangedEffect = (
+ control: ControlT,
+ effect: ControlEvents['onValueChanged'],
+) => {
+ const effectStable = useStableCallback(effect);
+
+ useEffect(() => {
+ const unsubscribe = control._on(
+ 'onValueChanged',
+ effectStable as ControlEvents['onValueChanged'],
+ );
+ return () => {
+ unsubscribe();
+ };
+ }, [control]); // eslint-disable-line react-hooks/exhaustive-deps
+};
+
+export const useOnPickerValueChangingEffect = (
+ control: ControlT,
+ effect: ControlEvents['onValueChanging'],
+) => {
+ const effectStable = useStableCallback(effect);
+
+ useEffect(() => {
+ const unsubscribe = control._on(
+ 'onValueChanging',
+ effectStable as ControlEvents['onValueChanged'],
+ );
+ return () => {
+ unsubscribe();
+ };
+ }, [control]); // eslint-disable-line react-hooks/exhaustive-deps
+};
diff --git a/src/picker-control/usePickerControlSubscriber.ts b/src/picker-control/usePickerControlSubscriber.ts
new file mode 100644
index 0000000..e4ecdaf
--- /dev/null
+++ b/src/picker-control/usePickerControlSubscriber.ts
@@ -0,0 +1,92 @@
+import {useEffect, useState} from 'react';
+import {useStableCallback} from '@rozhkov/react-useful-hooks';
+import type {PickerItem} from '@implementation/base';
+import type {Control, ControlSubscriber} from './create-control';
+
+const useConnectSub = ({
+ control,
+ pickerName,
+ currentItem,
+}: {
+ control: Control;
+ pickerName: string;
+ currentItem: PickerItem;
+}) => {
+ const [subscriber, setSubscriber] = useState(null);
+
+ useEffect(() => {
+ const sub = control._connect({pickerName, item: currentItem});
+ setSubscriber(sub);
+
+ return () => {
+ sub.disconnect();
+ };
+ }, [control, pickerName]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ return subscriber;
+};
+
+export const usePickerControlSubscriber = ({
+ control,
+ pickerName,
+ currentItem,
+}: {
+ control: Control;
+ pickerName: string;
+ currentItem: PickerItem;
+}) => {
+ const subscriber = useConnectSub({
+ control,
+ pickerName,
+ currentItem,
+ });
+
+ const [extraValues, setExtraValues] = useState([]);
+ const [enableSyncScrollAfterScrollEnd, setEnableSyncScrollAfterScrollEnd] =
+ useState(true);
+
+ const onScrollStart = useStableCallback(() => {
+ setEnableSyncScrollAfterScrollEnd(false);
+ subscriber?.omitOnScrollStart();
+ });
+ const onScrollEnd = useStableCallback(subscriber?.omitOnScrollEnd);
+ const omitOnValueChanged = useStableCallback(subscriber?.omitOnValueChanged);
+ const omitOnValueChanging = useStableCallback(
+ subscriber?.omitOnValueChanging,
+ );
+
+ useEffect(() => {
+ if (!subscriber) {
+ return;
+ }
+
+ setExtraValues(subscriber.getExtraValues());
+ const unsubscribeNewExtraValues = subscriber.onNewExtraValues(() => {
+ console.log('onNewExtraValues subscriber.getExtraValues()', subscriber.getExtraValues(),); // eslint-disable-line prettier/prettier
+ setExtraValues(subscriber.getExtraValues());
+ });
+
+ setEnableSyncScrollAfterScrollEnd(subscriber.getEveryIsStopped());
+ const unsubscribeAllScrollEnd = subscriber.onAllScrollEnd(() => {
+ setEnableSyncScrollAfterScrollEnd(subscriber.getEveryIsStopped());
+ });
+
+ return () => {
+ unsubscribeNewExtraValues();
+ unsubscribeAllScrollEnd();
+ };
+ }, [subscriber]);
+
+ useEffect(() => {
+ subscriber?.omitOnNewPropValue({item: currentItem});
+ }, [currentItem, subscriber]);
+
+ return {
+ extraValues,
+ enableSyncScrollAfterScrollEnd,
+ onScrollStart,
+ onScrollEnd,
+ omitOnValueChanged,
+ omitOnValueChanging,
+ };
+};
diff --git a/src/picker-control/withPickerControl.tsx b/src/picker-control/withPickerControl.tsx
new file mode 100644
index 0000000..dbbf170
--- /dev/null
+++ b/src/picker-control/withPickerControl.tsx
@@ -0,0 +1,121 @@
+import type {Control} from './create-control';
+import React, {
+ type ComponentRef,
+ type ComponentType,
+ forwardRef,
+ memo,
+} from 'react';
+import {useStableCallback} from '@rozhkov/react-useful-hooks';
+import {usePickerControlSubscriber} from './usePickerControlSubscriber';
+import {
+ type OnValueChanged,
+ type OnValueChanging,
+ type PickerItem,
+ useValueIndex,
+} from '@implementation/base';
+
+type RequiredPickerProps = {
+ data: ReadonlyArray>;
+ value?: unknown;
+ extraValues?: unknown[];
+ onValueChanging?: OnValueChanging>;
+ onValueChanged?: OnValueChanged>;
+ _enableSyncScrollAfterScrollEnd?: boolean;
+ _onScrollStart?: () => void;
+ _onScrollEnd?: () => void;
+};
+
+export type WithPickerControlProps =
+ PickerPropsT & {
+ pickerName: string;
+ control: Control;
+ };
+
+export const withPickerControl = (
+ PickerComponent: ComponentType,
+) => {
+ const WrappedPicker = (
+ {
+ pickerName,
+ control,
+ data,
+ value,
+ // extraValues TODO pass to props
+ onValueChanging: onValueChangingProp,
+ onValueChanged: onValueChangedProp,
+ _onScrollStart: onScrollStartProp,
+ _onScrollEnd: onScrollEndProp,
+ ...restProps
+ }: WithPickerControlProps,
+ forwardedRef: any,
+ ) => {
+ const valueIndex = useValueIndex(data, value);
+ const currentItem = data[valueIndex]!;
+
+ const subscriber = usePickerControlSubscriber({
+ control,
+ pickerName,
+ currentItem,
+ });
+
+ const onValueChangingStable = useStableCallback<
+ OnValueChanging>
+ >((event) => {
+ // console.log('onValueChangingStable');
+ subscriber.omitOnValueChanging(event);
+ onValueChangingProp?.(event);
+ });
+ const onValueChangedStable = useStableCallback<
+ OnValueChanged>
+ >((event) => {
+ console.log('onValueChangedStable');
+ subscriber.omitOnValueChanged(event);
+ onValueChangedProp?.(event);
+ });
+ const onScrollStartStable = useStableCallback(() => {
+ console.log('onScrollStartStable');
+ subscriber.onScrollStart();
+ onScrollStartProp?.();
+ });
+ const onScrollEndStable = useStableCallback(() => {
+ console.log('onScrollEndStable');
+ subscriber.onScrollEnd();
+ onScrollEndProp?.();
+ });
+
+ console.log(
+ `[withPickerControl] [PickerName ${pickerName}] subscriber.extraValues`,
+ subscriber?.extraValues,
+ );
+ console.log(
+ `[withPickerControl] [PickerName ${pickerName}] subscriber.enableSyncScrollAfterScrollEnd`,
+ subscriber?.enableSyncScrollAfterScrollEnd,
+ );
+
+ return (
+
+ );
+ };
+
+ WrappedPicker.displayName = `withPickerControl(${PickerComponent.displayName})`;
+
+ return memo(
+ forwardRef<
+ ComponentRef>,
+ WithPickerControlProps
+ >(WrappedPicker as any),
+ );
+};
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;
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/withScrollEndEvent.tsx
deleted file mode 100644
index f3b0b1f..0000000
--- a/src/utils/scrolling/withScrollEndEvent.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import React, {
- type ComponentRef,
- type ComponentType,
- type ForwardedRef,
- forwardRef,
- memo,
- useCallback,
- useMemo,
-} from 'react';
-import type {
- ScrollViewProps,
- NativeSyntheticEvent,
- NativeScrollEvent,
-} from 'react-native';
-import debounce from '@utils/debounce';
-
-type ComponentProps = Pick<
- ScrollViewProps,
- | 'onScrollBeginDrag'
- | 'onScrollEndDrag'
- | 'onMomentumScrollBegin'
- | 'onMomentumScrollEnd'
->;
-
-type ExtendProps = PropsT & {
- onScrollEnd?: () => void;
-};
-
-const withScrollEndEvent = (
- Component: ComponentType,
-) => {
- const Wrapper = (
- {
- onScrollEnd: onScrollEndProp = () => {},
- onScrollEndDrag: onScrollEndDragProp,
- onMomentumScrollBegin: onMomentumScrollBeginProp,
- onMomentumScrollEnd: onMomentumScrollEndProp,
- ...rest
- }: ExtendProps,
- forwardedRef: ForwardedRef>>,
- ) => {
- const onScrollEnd = useMemo(
- () => debounce(onScrollEndProp, 0), // This works well with onScrollEndDrag -> onMomentumScrollBegin transitions
- [onScrollEndProp],
- );
-
- const onScrollEndDrag = useCallback(
- (args: NativeSyntheticEvent) => {
- onScrollEndDragProp?.(args);
- onScrollEnd();
- },
- [onScrollEnd, onScrollEndDragProp],
- );
-
- const onMomentumScrollBegin = useCallback(
- (args: NativeSyntheticEvent) => {
- onScrollEnd.clear();
- onMomentumScrollBeginProp?.(args);
- },
- [onScrollEnd, onMomentumScrollBeginProp],
- );
-
- const onMomentumScrollEnd = useCallback(
- (args: NativeSyntheticEvent) => {
- onMomentumScrollEndProp?.(args);
- onScrollEnd();
- },
- [onScrollEnd, onMomentumScrollEndProp],
- );
-
- return (
-
- );
- };
-
- Wrapper.displayName = `withScrollEndEvent(${
- Component.displayName || 'Component'
- })`;
-
- return memo(
- forwardRef>, ExtendProps>(
- Wrapper as any,
- ),
- );
-};
-
-export default withScrollEndEvent;
diff --git a/src/utils/scrolling/withScrollStartEndEvent.tsx b/src/utils/scrolling/withScrollStartEndEvent.tsx
new file mode 100644
index 0000000..af1a520
--- /dev/null
+++ b/src/utils/scrolling/withScrollStartEndEvent.tsx
@@ -0,0 +1,141 @@
+import React, {
+ type ComponentRef,
+ type ComponentType,
+ type ForwardedRef,
+ forwardRef,
+ memo,
+ useEffect,
+ useMemo,
+ useRef,
+} from 'react';
+import type {
+ Animated,
+ NativeScrollEvent,
+ NativeSyntheticEvent,
+ ScrollViewProps,
+} from 'react-native';
+import {useStableCallback} from '@rozhkov/react-useful-hooks';
+import debounce from '@utils/debounce';
+
+type ComponentProps = Pick<
+ ScrollViewProps,
+ | 'onScrollBeginDrag'
+ | 'onScrollEndDrag'
+ | 'onMomentumScrollBegin'
+ | 'onMomentumScrollEnd'
+>;
+
+type ExtendProps = PropsT & {
+ scrollOffset: Animated.Value;
+ onScrollStart?: () => void;
+ onScrollEnd?: () => void;
+};
+
+const withScrollStartEndEvent = (
+ Component: ComponentType,
+) => {
+ const Wrapper = (
+ {
+ onScrollStart: onScrollStartProp,
+ onScrollEnd: onScrollEndProp,
+ onScrollBeginDrag: onScrollBeginDragProp,
+ onScrollEndDrag: onScrollEndDragProp,
+ onMomentumScrollBegin: onMomentumScrollBeginProp,
+ onMomentumScrollEnd: onMomentumScrollEndProp,
+ scrollOffset,
+ ...rest
+ }: 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(() => {
+ console.log('[withScrollStartEndEvent] onScrollEndStable');
+ maybeCallOnScrollStart();
+ 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: NativeSyntheticEvent) => {
+ console.log('[withScrollStartEndEvent] onScrollBeginDrag');
+ maybeCallOnScrollStart();
+ onScrollBeginDragProp?.(args);
+ },
+ );
+
+ const onScrollEndDrag = useStableCallback(
+ (args: NativeSyntheticEvent) => {
+ console.log('[withScrollStartEndEvent] onScrollEndDrag');
+ onScrollEndDragProp?.(args);
+ onScrollEnd();
+ },
+ );
+
+ const onMomentumScrollBegin = useStableCallback(
+ (args: NativeSyntheticEvent) => {
+ console.log('[withScrollStartEndEvent] onMomentumScrollBegin');
+ maybeCallOnScrollStart();
+ onScrollEnd.clear();
+ onMomentumScrollBeginProp?.(args);
+ },
+ );
+
+ const onMomentumScrollEnd = useStableCallback(
+ (args: NativeSyntheticEvent) => {
+ console.log('[withScrollStartEndEvent] onMomentumScrollEnd');
+ onMomentumScrollEndProp?.(args);
+ onScrollEnd();
+ },
+ );
+
+ useEffect(() => {
+ const sub = scrollOffset.addListener(() => {
+ maybeCallOnScrollStart();
+ onScrollEnd.clear();
+ });
+ return () => {
+ scrollOffset.removeListener(sub);
+ };
+ }, [maybeCallOnScrollStart, onScrollEnd, scrollOffset]);
+
+ return (
+
+ );
+ };
+
+ Wrapper.displayName = `withScrollStartEndEvent(${
+ Component.displayName || 'Component'
+ })`;
+
+ return memo(
+ forwardRef>, ExtendProps>(
+ Wrapper as any,
+ ),
+ );
+};
+
+export default withScrollStartEndEvent;
diff --git a/tsconfig.json b/tsconfig.json
index dfdbf38..23e886e 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -4,6 +4,7 @@
"paths": {
"@quidone/react-native-wheel-picker": ["./src/index"],
"@implementation/base": ["./src/base/index"],
+ "@implementation/picker-control": ["./src/picker-control/index"],
"@implementation/virtualized": ["./src/hoc/virtualized/index"],
"@utils/react": ["./src/utils/react/index"],
"@utils/math": ["./src/utils/math/index"],
diff --git a/yarn.lock b/yarn.lock
index b67cbb1..73b305b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2622,6 +2622,7 @@ __metadata:
"@types/react-test-renderer": "npm:^18"
babel-plugin-module-resolver: "npm:^4.1.0"
commitlint: "npm:^17.0.2"
+ date-fns: "npm:^4.1.0"
del-cli: "npm:^5.0.0"
eslint: "npm:^8.4.1"
eslint-config-prettier: "npm:^8.5.0"
@@ -2629,6 +2630,7 @@ __metadata:
eslint-plugin-prettier: "npm:^4.0.0"
jest: "npm:^28.1.1"
metro-react-native-babel-preset: "npm:^0.77.0"
+ nanoevents: "npm:^9.1.0"
pod-install: "npm:^0.1.0"
prettier: "npm:^2.0.5"
react: "npm:18.3.1"
@@ -5026,6 +5028,13 @@ __metadata:
languageName: node
linkType: hard
+"date-fns@npm:^4.1.0":
+ version: 4.1.0
+ resolution: "date-fns@npm:4.1.0"
+ checksum: 10/d5f6e9de5bbc52310f786099e18609289ed5e30af60a71e0646784c8185ddd1d0eebcf7c96b7faaaefc4a8366f3a3a4244d099b6d0866ee2bec80d1361e64342
+ languageName: node
+ linkType: hard
+
"dateformat@npm:^3.0.0":
version: 3.0.3
resolution: "dateformat@npm:3.0.3"
@@ -9635,6 +9644,13 @@ __metadata:
languageName: node
linkType: hard
+"nanoevents@npm:^9.1.0":
+ version: 9.1.0
+ resolution: "nanoevents@npm:9.1.0"
+ checksum: 10/77abbc15c4efc15518a0075b604e5db045e5d6184f4d08892c771928a84f53b1dd46138902213f66e8a9989e6c930168ca6441b80ff9098aec83c01a782bd42b
+ languageName: node
+ linkType: hard
+
"natural-compare-lite@npm:^1.4.0":
version: 1.4.0
resolution: "natural-compare-lite@npm:1.4.0"