import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {TextStyle} from 'react-native'; import { type StyleProp, type ViewStyle, Animated, Platform, StyleSheet, View, } from 'react-native'; import PickerItemComponent from '../item/PickerItem'; import {ScrollContentOffsetContext} from '../contexts/ScrollContentOffsetContext'; import {PickerItemHeightContext} from '../contexts/PickerItemHeightContext'; import useValueEventsEffect from './hooks/useValueEventsEffect'; import useSyncScrollEffect from './hooks/useSyncScrollEffect'; import type { KeyExtractor, ListMethods, OnValueChanged, OnValueChanging, PickerItem, RenderItem, RenderItemContainer, RenderList, RenderOverlay, RenderPickerItem, } from '../types'; import Overlay from '../overlay/Overlay'; import {calcPickerHeight, createFaces} from '../item/faces'; import PickerItemContainer from '../item/PickerItemContainer'; import {useBoolean} from '../../utils/react'; 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}%`; readOnly?: boolean; testID?: string; enableScrollByTapOnItem?: boolean; onValueChanging?: OnValueChanging; onValueChanged?: OnValueChanged; keyExtractor?: KeyExtractor; renderItem?: RenderItem; renderItemContainer?: RenderItemContainer; renderOverlay?: RenderOverlay | null; renderList?: RenderList; style?: StyleProp; itemTextStyle?: StyleProp; overlayItemStyle?: StyleProp; contentContainerStyle?: StyleProp; scrollEventThrottle?: number; _enableSyncScrollAfterScrollEnd?: boolean; _onScrollStart?: () => void; _onScrollEnd?: () => void; }; const defaultKeyExtractor: KeyExtractor = (_, index) => index.toString(); const defaultRenderItem: RenderItem> = ({ item: {value, label}, itemTextStyle, }) => ( ); const defaultRenderItemContainer: RenderItemContainer = ({ key, ...props }) => ; const defaultRenderOverlay: RenderOverlay = (props) => ; const defaultRenderList: RenderList = (props) => { return ; }; export const useValueIndex = ( data: ReadonlyArray>, value: any, ) => { return useMemo(() => { const index = data.findIndex((x) => x.value === value); return index >= 0 ? index : 0; }, [data, value]); }; const Picker = >({ data, value, extraValues = [], width = 'auto', itemHeight = 48, visibleItemCount = 5, readOnly = false, enableScrollByTapOnItem, testID, onValueChanged, onValueChanging, keyExtractor = defaultKeyExtractor, renderItem = defaultRenderItem, renderItemContainer = defaultRenderItemContainer, renderOverlay = defaultRenderOverlay, renderList = defaultRenderList, style, itemTextStyle, overlayItemStyle, contentContainerStyle, _enableSyncScrollAfterScrollEnd = true, _onScrollStart, _onScrollEnd, ...restProps }: PickerProps) => { const valueIndex = useValueIndex(data, value); const initialIndex = useInit(() => valueIndex); 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); const [faces, pickerHeight] = useMemo(() => { const items = createFaces(itemHeight, visibleItemCount); const height = calcPickerHeight(items, itemHeight); return [items, height]; }, [itemHeight, visibleItemCount]); const renderPickerItem = useCallback>( ({item, index, key}) => renderItemContainer({ listRef, key, item, index, faces, renderItem, itemTextStyle, enableScrollByTapOnItem, readOnly, }), [ enableScrollByTapOnItem, faces, itemTextStyle, readOnly, renderItem, renderItemContainer, ], ); const {activeIndexRef, onScrollEnd: onScrollEndForValueEvents} = useValueEventsEffect( { data, valueIndex, itemHeight, offsetYAv: offsetY, }, { onValueChanging, onValueChanged, }, ); const { onScrollStart: onScrollStartForSyncScroll, onScrollEnd: onScrollEndForSyncScroll, } = useSyncScrollEffect({ listRef, value, valueIndex, extraValues, activeIndexRef, touching: touching.value, enableSyncScrollAfterScrollEnd: _enableSyncScrollAfterScrollEnd, }); const onScrollStart = useStableCallback(() => { onScrollStartForSyncScroll(); _onScrollStart?.(); }); const onScrollEnd = useStableCallback(() => { // consistency matters _onScrollEnd?.(); onScrollEndForValueEvents(); onScrollEndForSyncScroll(); }); // iOS can mount the picker list with a wrong initial contentOffset, so re-apply it after mount. // See https://github.com/quidone/react-native-wheel-picker/issues/67. useEffect(() => { if (Platform.OS === 'ios') { listRef.current?.scrollToIndex({ index: initialIndex, animated: false, }); } }, []); // eslint-disable-line react-hooks/exhaustive-deps return ( {renderList({ ...restProps, ref: listRef, data, initialIndex, itemHeight, pickerHeight, visibleItemCount, readOnly, keyExtractor, renderItem: renderPickerItem, scrollOffset: offsetY, onTouchStart: touching.setTrue, onTouchEnd: touching.setFalse, onTouchCancel: touching.setFalse, onScrollStart, onScrollEnd, contentContainerStyle, })} {renderOverlay && renderOverlay({ itemHeight, pickerWidth: width, pickerHeight, overlayItemStyle, })} ); }; const styles = StyleSheet.create({ root: { justifyContent: 'center', alignItems: 'center', }, }); export default Picker;