Skip to content

Commit d6f49e0

Browse files
authored
WheelPicker - generic value (#2959)
* WheelPicker - item's value to generic type * WheelPickerItem - replace asBaseComponent with useThemeProps hook, move memo to the export * initialNumber type - remove ItemProps * fix usages * clean imports * removing default type (number) and renaming ItemProps `WheelPickerItemProps` * remove casting * add WheelPickerItemValue type * export as type * remove useComponentDriver generic types * removing generic from ComponentDriver
1 parent eb25f4d commit d6f49e0

File tree

10 files changed

+131
-108
lines changed

10 files changed

+131
-108
lines changed

demo/src/screens/componentScreens/SectionsWheelPickerScreen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ const SectionsWheelPickerScreen = () => {
7474
setSelectedMinutes(0);
7575
}, []);
7676

77-
const sections: WheelPickerProps[] = useMemo(() => {
77+
const sections: WheelPickerProps<string | number>[] = useMemo(() => {
7878
return [
7979
{
8080
items: getItems(DAYS),

src/components/WheelPicker/Item.tsx

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
import React, {useCallback, useMemo, memo, useRef} from 'react';
22
import {TextStyle, StyleSheet} from 'react-native';
33
import Animated, {interpolateColor, useAnimatedStyle} from 'react-native-reanimated';
4-
import Text, {TextProps} from '../../components/text';
5-
import TouchableOpacity from '../../components/touchableOpacity';
6-
import {Colors, Spacings} from '../../../src/style';
7-
import {asBaseComponent} from '../../commons/new';
8-
import {WheelPickerAlign} from './types';
4+
import {Colors, Spacings} from '../../style';
5+
import {useThemeProps} from '../../hooks';
6+
import Text, {TextProps} from '../text';
7+
import TouchableOpacity from '../touchableOpacity';
8+
import {WheelPickerAlign, WheelPickerItemValue} from './types';
99

1010
const AnimatedTouchableOpacity = Animated.createAnimatedComponent(TouchableOpacity);
1111
const AnimatedText = Animated.createAnimatedComponent(Text);
1212

13-
export interface ItemProps {
13+
export interface WheelPickerItemProps<T = WheelPickerItemValue> {
1414
label: string;
15-
value: string | number;
15+
value: T;
1616
align?: WheelPickerAlign;
1717
disableRTL?: boolean;
1818
}
1919

20-
interface InternalProps extends ItemProps {
20+
interface InternalProps<T> extends WheelPickerItemProps<T> {
2121
index: number;
2222
offset: Animated.SharedValue<number>;
2323
itemHeight: number;
@@ -32,23 +32,26 @@ interface InternalProps extends ItemProps {
3232
testID?: string;
3333
}
3434

35-
const WheelPickerItem = memo(({
36-
index,
37-
label,
38-
fakeLabel,
39-
fakeLabelStyle,
40-
fakeLabelProps,
41-
itemHeight,
42-
onSelect,
43-
offset,
44-
activeColor = Colors.$textPrimary,
45-
inactiveColor = Colors.$textNeutralHeavy,
46-
style,
47-
testID,
48-
centerH = true,
49-
align,
50-
disableRTL
51-
}: InternalProps) => {
35+
const WheelPickerItem = <T extends WheelPickerItemValue = number>(props: InternalProps<T>) => {
36+
const themeProps = useThemeProps(props, 'WheelPickerItem');
37+
const {
38+
index,
39+
label,
40+
fakeLabel,
41+
fakeLabelStyle,
42+
fakeLabelProps,
43+
itemHeight,
44+
onSelect,
45+
offset,
46+
activeColor = Colors.$textPrimary,
47+
inactiveColor = Colors.$textNeutralHeavy,
48+
style,
49+
testID,
50+
centerH = true,
51+
align,
52+
disableRTL
53+
} = themeProps;
54+
5255
const selectItem = useCallback(() => onSelect(index), [index]);
5356
const itemOffset = index * itemHeight;
5457
const _activeColor = useRef(activeColor.toString());
@@ -99,9 +102,9 @@ const WheelPickerItem = memo(({
99102
)}
100103
</AnimatedTouchableOpacity>
101104
);
102-
});
105+
};
103106

104-
export default asBaseComponent<InternalProps>(WheelPickerItem);
107+
export default memo(WheelPickerItem);
105108

106109
const styles = StyleSheet.create({
107110
container: {

src/components/WheelPicker/__tests__/usePresenter.spec.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import usePresenter from '../usePresenter';
22
import {renderHook} from '@testing-library/react-hooks';
33

44
describe('WheelPicker presenter tests', () => {
5+
jest.spyOn(global.console, 'warn');
6+
57
const makeSUT = ({items = makeItems(9), children, initialValue, itemHeight = 10, preferredNumVisibleRows = 20}) => {
68
return renderHook(() =>
79
usePresenter({
@@ -69,6 +71,7 @@ describe('WheelPicker presenter tests', () => {
6971
it('Expect to find items by their object of {value, label} types', () => {
7072
const {result} = makeSUT({items: makeItems(15, 'b'), initialValue: {value: 'b6', label: 'abc'}});
7173
expect(result.current.index).toEqual(6);
74+
expect(console.warn).toHaveBeenCalledWith('UILib WheelPicker will stop supporting initialValue prop type as an object (ItemProps). Please pass string or number only');
7275
});
7376

7477
it('Expect getRowItemAtOffset to return the right row for offset', () => {

src/components/WheelPicker/index.tsx

Lines changed: 48 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,28 @@ import {
1212
} from 'react-native';
1313
import Animated, {useSharedValue, useAnimatedScrollHandler} from 'react-native-reanimated';
1414
import {FlatList} from 'react-native-gesture-handler';
15-
import {Colors, Spacings} from 'style';
16-
import {Constants, asBaseComponent} from '../../commons/new';
17-
import View from '../../components/view';
18-
import Fader, {FaderPosition, FaderProps} from '../../components/fader';
19-
import Item, {ItemProps} from './Item';
20-
import Text, {TextProps} from '../../components/text';
15+
import {Colors, Spacings} from '../../style';
16+
import {Constants} from '../../commons/new';
17+
import {useThemeProps} from '../../hooks';
18+
import View from '../view';
19+
import Text, {TextProps} from '../text';
20+
import Fader, {FaderPosition, FaderProps} from '../fader';
21+
import Item, {WheelPickerItemProps} from './Item';
2122
import usePresenter from './usePresenter';
22-
import {WheelPickerAlign} from './types';
23-
export {WheelPickerAlign};
23+
import {WheelPickerAlign, WheelPickerItemValue} from './types';
24+
export type {WheelPickerAlign, WheelPickerItemValue};
2425

25-
const AnimatedFlatList = Animated.createAnimatedComponent<FlatListProps<ItemProps>>(FlatList);
2626
export const ITEM_HEIGHT = 44;
2727

28-
export interface WheelPickerProps {
28+
export type WheelPickerProps<T = WheelPickerItemValue> = {
2929
/**
3030
* Initial value
3131
*/
32-
initialValue?: ItemProps | number | string;
32+
initialValue?: T;
3333
/**
3434
* Data source for WheelPicker
3535
*/
36-
items?: ItemProps[];
36+
items?: WheelPickerItemProps<T>[];
3737
/**
3838
* Describe the height of each item in the WheelPicker
3939
* default value: 44
@@ -71,7 +71,7 @@ export interface WheelPickerProps {
7171
/**
7272
* Event, on active row change
7373
*/
74-
onChange?: (item: string | number, index: number) => void;
74+
onChange?: (item: T, index: number) => void;
7575
/**
7676
* Container's ViewStyle, height is computed according to itemHeight * numberOfVisibleRows
7777
*/
@@ -97,30 +97,35 @@ export interface WheelPickerProps {
9797
/**
9898
* Props to be sent to the FlatList
9999
*/
100-
flatListProps?: Partial<FlatListProps<ItemProps>>;
100+
flatListProps?: Partial<FlatListProps<WheelPickerItemProps<T>>>;
101101
}
102102

103-
const WheelPicker = ({
104-
items: propItems,
105-
itemHeight = ITEM_HEIGHT,
106-
numberOfVisibleRows = 5,
107-
activeTextColor = Colors.$textPrimary,
108-
inactiveTextColor,
109-
textStyle,
110-
label,
111-
labelStyle,
112-
labelProps,
113-
onChange,
114-
align = WheelPickerAlign.CENTER,
115-
disableRTL,
116-
style,
117-
children,
118-
initialValue = 0,
119-
separatorsStyle,
120-
testID,
121-
faderProps,
122-
flatListProps
123-
}: WheelPickerProps) => {
103+
const WheelPicker = <T extends WheelPickerItemValue>(props: WheelPickerProps<T>) => {
104+
const AnimatedFlatList =
105+
useMemo(() => Animated.createAnimatedComponent<FlatListProps<WheelPickerItemProps<T>>>(FlatList), []);
106+
const themeProps = useThemeProps(props, 'WheelPicker');
107+
108+
const {
109+
items: propItems,
110+
itemHeight = ITEM_HEIGHT,
111+
numberOfVisibleRows = 5,
112+
activeTextColor = Colors.$textPrimary,
113+
inactiveTextColor,
114+
textStyle,
115+
label,
116+
labelStyle,
117+
labelProps,
118+
onChange,
119+
align = WheelPickerAlign.CENTER,
120+
disableRTL,
121+
style,
122+
children,
123+
initialValue,
124+
separatorsStyle,
125+
testID,
126+
faderProps,
127+
flatListProps
128+
} = themeProps;
124129
const scrollView = useRef<Animated.ScrollView>();
125130
const offset = useSharedValue(0);
126131
const scrollHandler = useAnimatedScrollHandler(e => {
@@ -133,7 +138,7 @@ const WheelPicker = ({
133138
const {
134139
height,
135140
items,
136-
index: currentIndex,
141+
index: currentIndex = 0,
137142
getRowItemAtOffset
138143
} = usePresenter({
139144
initialValue,
@@ -146,7 +151,7 @@ const WheelPicker = ({
146151
const prevInitialValue = useRef(initialValue);
147152
const prevIndex = useRef(currentIndex);
148153
const [flatListWidth, setFlatListWidth] = useState(0);
149-
const keyExtractor = useCallback((item: ItemProps, index: number) => `${item}.${index}`, []);
154+
const keyExtractor = useCallback((item: WheelPickerItemProps<T>, index: number) => `${item}.${index}`, []);
150155
const androidFlatListProps = useMemo(() => {
151156
if (Constants.isAndroid) {
152157
return {
@@ -156,7 +161,7 @@ const WheelPicker = ({
156161
}, [items]);
157162

158163
useEffect(() => {
159-
//This effect should replace the onLyout function in the flatlist, should happen only once
164+
// This effect should replace the onLayout function in the FlatList, should happen only once
160165
scrollToIndex(currentIndex, true);
161166
}, []);
162167

@@ -165,7 +170,7 @@ const WheelPicker = ({
165170
!isUndefined(initialValue) && scrollToIndex(currentIndex, true);
166171
}, [currentIndex]);
167172

168-
const _onChange = useCallback((value: string | number, index: number) => {
173+
const _onChange = useCallback((value: T, index: number) => {
169174
if (prevInitialValue.current !== initialValue) {
170175
// don't invoke 'onChange' if 'initialValue' changed
171176
prevInitialValue.current = initialValue;
@@ -176,7 +181,7 @@ const WheelPicker = ({
176181
[initialValue, onChange]);
177182

178183
const onValueChange = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
179-
const {index, value} = getRowItemAtOffset(event.nativeEvent.contentOffset.y);
184+
const {value, index} = getRowItemAtOffset(event.nativeEvent.contentOffset.y);
180185
_onChange(value, index);
181186
},
182187
[_onChange, getRowItemAtOffset]);
@@ -224,7 +229,7 @@ const WheelPicker = ({
224229
return {...labelMargins, ...labelProps};
225230
}, [labelMargins, labelProps]);
226231

227-
const renderItem = useCallback(({item, index}: ListRenderItemInfo<ItemProps>) => {
232+
const renderItem = useCallback(({item, index}: ListRenderItemInfo<WheelPickerItemProps<T>>) => {
228233
return (
229234
<Item
230235
index={index}
@@ -378,8 +383,8 @@ const WheelPicker = ({
378383
};
379384

380385
WheelPicker.alignments = WheelPickerAlign;
381-
export default asBaseComponent<WheelPickerProps, typeof WheelPicker>(WheelPicker);
382-
export {ItemProps as WheelPickerItemProps};
386+
export default WheelPicker;
387+
export {WheelPickerItemProps};
383388

384389
const styles = StyleSheet.create({
385390
separators: {

src/components/WheelPicker/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ export enum WheelPickerAlign {
33
RIGHT = 'right',
44
LEFT = 'left'
55
}
6+
7+
export type WheelPickerItemValue = number | string;

src/components/WheelPicker/usePresenter.ts

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,57 @@
11
import _ from 'lodash';
22
import React from 'react';
3-
import {ItemProps} from './Item';
3+
import {LogService} from '../../services';
44
import useMiddleIndex from './helpers/useListMiddleIndex';
5+
import {WheelPickerItemValue} from './types';
6+
import {WheelPickerItemProps} from './Item';
57

6-
export type ItemValueTypes = ItemProps | number | string;
8+
//TODO: deprecate this type
79

8-
type PropTypes = {
9-
initialValue?: ItemValueTypes;
10+
type PropTypes<T> = {
11+
initialValue?: T;
1012
children?: JSX.Element | JSX.Element[];
11-
items?: ItemProps[];
13+
items?: WheelPickerItemProps<T>[];
1214
itemHeight: number;
1315
preferredNumVisibleRows: number;
1416
};
1517

16-
type RowItem = {
17-
value: string | number;
18+
type RowItem<T> = {
19+
value: T;
1820
index: number;
1921
};
2022

21-
interface Presenter {
22-
items: ItemProps[];
23-
index: number;
24-
height: number;
25-
getRowItemAtOffset: (offset: number) => RowItem;
26-
}
27-
28-
const usePresenter = ({
29-
initialValue = 0,
23+
const usePresenter = <T extends WheelPickerItemValue>({
24+
initialValue,
3025
children,
3126
items: propItems,
3227
itemHeight,
3328
preferredNumVisibleRows
34-
}: PropTypes): Presenter => {
35-
const extractItemsFromChildren = (): ItemProps[] => {
29+
}: PropTypes<T>) => {
30+
31+
const extractItemsFromChildren = (): WheelPickerItemProps<T>[] => {
3632
const items = React.Children.map(children, child => {
37-
const childAsType: ItemProps = {value: child?.props.value, label: child?.props.label};
33+
const childAsType: WheelPickerItemProps<T> = {value: child?.props.value, label: child?.props.label};
3834
return childAsType;
3935
});
4036
return items || [];
4137
};
42-
4338
const items = children ? extractItemsFromChildren() : propItems || [];
4439
const middleIndex = useMiddleIndex({itemHeight, listSize: items.length});
4540

4641
const getSelectedValueIndex = () => {
47-
if (_.isString(initialValue) || _.isNumber(initialValue)) {
48-
return _.findIndex(items, {value: initialValue});
42+
if (_.isObject(initialValue)) {
43+
LogService.warn('UILib WheelPicker will stop supporting initialValue prop type as an object (ItemProps). Please pass string or number only');
44+
//@ts-expect-error
45+
return _.findIndex(items, {value: initialValue?.value});
46+
} else {
47+
return initialValue && _.findIndex(items, (item) => item.value === initialValue);
4948
}
50-
return _.findIndex(items, {value: initialValue?.value});
5149
};
5250

53-
const getRowItemAtOffset = (offset: number): RowItem => {
51+
const getRowItemAtOffset = (offset: number): RowItem<T> => {
5452
const index = middleIndex(offset);
5553
const value = items[index].value;
56-
return {index, value};
54+
return {value, index};
5755
};
5856

5957
return {

src/components/sectionsWheelPicker/SectionsWheelPicker.driver.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import _ from 'lodash';
22
import {useComponentDriver, ComponentProps} from '../../testkit/new/Component.driver';
33
import {WheelPickerDriver} from '../WheelPicker/WheelPicker.driver';
4+
import {WheelPickerItemValue} from '../WheelPicker';
45
import {SectionsWheelPickerProps} from './index';
56

6-
export const SectionsWheelPickerDriver = (props: ComponentProps) => {
7+
export const SectionsWheelPickerDriver = <T extends WheelPickerItemValue>(props: ComponentProps) => {
78
const driver = useComponentDriver(props);
8-
const sections = driver.getElement().children as SectionsWheelPickerProps;
9+
const sections = driver.getElement().children as SectionsWheelPickerProps<T>;
910
const sectionsDrivers = _.map(sections, (_, index) => {
1011
const sectionTestID = `${props.testID}.${index}`;
1112
return WheelPickerDriver({

0 commit comments

Comments
 (0)