Skip to content

Picker - migrate prop deprecation end #3737

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/getting-started/v8.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,22 @@ Removed
Use the `backgroundColor` prop instead of `contentContainerStyle={{backgroundColor}}`
Fix card being transparent on Android
`onCollapseChanged` will now be called after the animation has ended (as was intended)

### Picker
The component was refactored to simplify its API and improve type safety.

#### Migration Steps

**Picker:**

- `value` - The picker now only supports primitive values (string | number) instead of object-based values
- `migrate` - Removed

**Picker.Item:**

- Items structure remains the same: `{label: string, value: primitive}` format is unchanged
- All items now use the `label` prop directly - no custom label transformation needed
- `getItemLabel` - Removed (use `item.label` to get label)
- `getItemValue` - Removed (use `item.value` to get value)

Check out the full API: https://wix.github.io/react-native-ui-lib/docs/components/form/Picker
26 changes: 9 additions & 17 deletions src/components/picker/PickerItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import View from '../view';
import TouchableOpacity from '../touchableOpacity';
import Image from '../image';
import Text from '../text';
import {getItemLabel, isItemSelected} from './PickerPresenter';
import {isItemSelected} from './PickerPresenter';
import PickerContext from './PickerContext';
import {PickerItemProps, PickerSingleValue} from './types';
import {PickerItemProps} from './types';

/**
* @description: Picker.Item, for configuring the Picker's selectable options
Expand All @@ -29,12 +29,8 @@ const PickerItem = (props: PickerItemProps) => {
testID
} = props;
const context = useContext(PickerContext);
const {migrate} = context;
const customRenderItem = props.renderItem || context.renderItem;
// @ts-expect-error TODO: fix after removing migrate prop completely
const itemValue = !migrate && typeof value === 'object' ? value?.value : value;
const isSelected = isItemSelected(itemValue, context.value);
const itemLabel = getItemLabel(label, value, props.getItemLabel || context.getItemLabel);
const isSelected = isItemSelected(value, context.value);
const selectedCounter = context.selectionLimit && _.isArray(context.value) && context.value?.length;
const accessibilityProps = {
accessibilityState: isSelected ? {selected: true} : undefined,
Expand Down Expand Up @@ -65,16 +61,12 @@ const PickerItem = (props: PickerItemProps) => {
const _onPress = useCallback(async (props: any) => {
// Using !(await onPress?.(item)) does not work properly when onPress is not sent
// We have to explicitly state `false` so a synchronous void (undefined) will still work as expected
if (onPress && await onPress(context.isMultiMode ? !isSelected : undefined, props) === false) {
if (onPress && (await onPress(context.isMultiMode ? !isSelected : undefined, props)) === false) {
return;
}
if (migrate) {
context.onPress(value);
} else {
// @ts-expect-error TODO: fix after removing migrate prop completely
context.onPress(typeof value === 'object' || context.isMultiMode ? value : ({value, label: itemLabel}) as PickerSingleValue);
}
}, [migrate, value, context.onPress, onPress]);
context.onPress(value);
},
[value, context.onPress, onPress]);

const onSelectedLayout = useCallback((...args: any[]) => {
_.invoke(context, 'onSelectedLayout', ...args);
Expand All @@ -84,7 +76,7 @@ const PickerItem = (props: PickerItemProps) => {
return (
<View style={styles.container} flex row spread centerV>
<Text numberOfLines={1} style={itemLabelStyle}>
{itemLabel}
{label}
</Text>
{selectedIndicator}
</View>
Expand All @@ -102,7 +94,7 @@ const PickerItem = (props: PickerItemProps) => {
customValue={props.customValue}
{...accessibilityProps}
>
{customRenderItem ? customRenderItem(value, {...props, isSelected, isItemDisabled}, itemLabel) : _renderItem()}
{customRenderItem ? customRenderItem(value, {...props, isSelected, isItemDisabled}, label) : _renderItem()}
</TouchableOpacity>
);
};
Expand Down
22 changes: 1 addition & 21 deletions src/components/picker/PickerPresenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,34 +19,14 @@ export function isItemSelected(childValue: PickerSingleValue, selectedValue?: Pi
if (Array.isArray(selectedValue)) {
isSelected =
_.find(selectedValue, v => {
// @ts-expect-error TODO: fix after removing migrate prop completely
return v === childValue || (typeof v === 'object' && v?.value === childValue);
return v === childValue;
}) !== undefined;
} else {
isSelected = childValue === selectedValue;
}
return isSelected;
}

// export function getItemValue(props) {
// if (_.isArray(props.value)) {
// return props.getItemValue ? _.map(props.value, item => props.getItemValue(item)) : _.map(props.value, 'value');
// } else if (!_.isObject(props.value)) {
// return props.value;
// }
// return _.invoke(props, 'getItemValue', props.value) || _.get(props.value, 'value');
// }

export function getItemLabel(label: string, value: PickerValue, getItemLabel?: PickerProps['getItemLabel']) {
if (_.isObject(value)) {
if (getItemLabel) {
return getItemLabel(value);
}
return _.get(value, 'label');
}
return label;
}

export function shouldFilterOut(searchValue: string, itemLabel?: string) {
return !_.includes(_.lowerCase(itemLabel), _.lowerCase(searchValue));
}
43 changes: 0 additions & 43 deletions src/components/picker/__tests__/PickerPresenter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,47 +12,4 @@ describe('components/PickerPresenter', () => {
expect(uut.isItemSelected('value', [])).toBe(false);
expect(uut.isItemSelected('value', undefined)).toBe(false);
});

// describe('getItemValue', () => {
// it('should return item value when item has value prop', () => {
// expect(uut.getItemValue({value: {value: 'item value'}})).toBe('item value');
// });

// it('should return item value for multiple values', () => {
// const itemProps = {value: [{value: '1'}, {value: '2'}, {value: '3'}]};
// expect(uut.getItemValue(itemProps)).toEqual(['1', '2', '3']);
// });

// it('should return item value when item has getItemValue prop', () => {
// const itemProps = {value: {name: 'value', age: 12}, getItemValue: item => item.name};
// expect(uut.getItemValue(itemProps)).toBe('value');
// });

// it('should return item value for multiple values when item has getItemValue prop', () => {
// const itemProps = {value: [{name: 'david'}, {name: 'sarah'}, {name: 'jack'}], getItemValue: item => item.name};
// expect(uut.getItemValue(itemProps)).toEqual(['david', 'sarah', 'jack']);
// });

// it('should support backward compatibility for when child item value was not an object', () => {
// const itemProps = {value: 'item-value'};
// expect(uut.getItemValue(itemProps)).toEqual('item-value');
// });
// });

describe('getItemLabel', () => {
it('should return item label when value is not an object', () => {
expect(uut.getItemLabel('label', 'value', undefined)).toEqual('label');
});

it('should return item label when value is an object', () => {
const itemProps = {value: {value: 'value', label: 'label'}};
expect(uut.getItemLabel(undefined, itemProps.value, undefined)).toEqual('label');
});

it('should return item label according to getLabel function ', () => {
const getLabel = itemValue => `${itemValue.value} - ${itemValue.label}`;
const itemProps = {value: {value: 'value', label: 'label'}, getLabel};
expect(uut.getItemLabel(undefined, itemProps.value, getLabel)).toEqual('value - label');
});
});
});
1 change: 0 additions & 1 deletion src/components/picker/api/picker.api.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Picker/CustomPicker.gif?raw=true"
],
"props": [
{"name": "migrate", "type": "boolean", "description": "Temporary prop required for migration to Picker's new API"},
{"name": "value", "type": "string | number", "description": "Picker current value"},
{
"name": "onChange",
Expand Down
5 changes: 0 additions & 5 deletions src/components/picker/api/pickerItem.api.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@
{"name": "value", "type": "string | number", "description": "Item's value"},
{"name": "label", "type": "string", "description": "Item's label"},
{"name": "labelStyle", "type": "ViewStyle", "description": "Item's label style"},
{
"name": "getItemLabel",
"type": "(value: string | number) => string",
"description": "Custom function for the item label"
},
{"name": "isSelected", "type": "boolean", "description": "Is the item selected"},
{"name": "selectedIcon", "type": "string", "description": "Pass to change the selected icon"},
{"name": "selectedIconColor", "type": "ImageSource", "description": "Pass to change the selected icon color"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ describe('usePickerLabel hook tests', () => {
value,
items,
getLabel
// getItemLabel,
// accessibilityLabel,
// accessibilityHint,
// placeholder
Expand All @@ -36,7 +35,6 @@ describe('usePickerLabel hook tests', () => {
value,
items,
getLabel
// getItemLabel,
// accessibilityLabel,
// accessibilityHint,
// placeholder
Expand Down
8 changes: 4 additions & 4 deletions src/components/picker/helpers/usePickerLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@ import {PickerProps, PickerValue} from '../types';
interface UsePickerLabelProps
extends Pick<
PickerProps,
'value' | 'getLabel' | 'getItemLabel' | 'placeholder' | 'accessibilityLabel' | 'accessibilityHint'
'value' | 'getLabel' | 'placeholder' | 'accessibilityLabel' | 'accessibilityHint'
> {
items: {value: string | number; label: string}[] | null | undefined;
}

const usePickerLabel = (props: UsePickerLabelProps) => {
const {value, items, getLabel, getItemLabel, placeholder, accessibilityLabel, accessibilityHint} = props;
const {value, items, getLabel, placeholder, accessibilityLabel, accessibilityHint} = props;

const getLabelsFromArray = useCallback((value: PickerValue) => {
const itemsByValue = _.keyBy(items, 'value');
return _.flow(arr =>
_.map(arr, item => (_.isPlainObject(item) ? getItemLabel?.(item) || item?.label : itemsByValue[item]?.label)),
_.map(arr, item => (_.isPlainObject(item) ? item?.label : itemsByValue[item]?.label)),
arr => _.join(arr, ', '))(value);
}, [getItemLabel, items]);
}, [items]);

const _getLabel = useCallback((value: PickerValue) => {
if (_.isFunction(getLabel) && !_.isUndefined(getLabel(value))) {
Expand Down
19 changes: 2 additions & 17 deletions src/components/picker/helpers/usePickerMigrationWarnings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,15 @@ import {LogService} from '../../../services';
import {PickerProps} from '../types';

// TODO: Remove this whole file when migration is completed
type UsePickerMigrationWarnings = Pick<
PickerProps,
'children' | 'migrate' | 'getItemLabel' | 'getItemValue' | 'onShow'
>;
type UsePickerMigrationWarnings = Pick<PickerProps, 'children' | 'onShow'>;

const usePickerMigrationWarnings = (props: UsePickerMigrationWarnings) => {
const {children, migrate, getItemLabel, getItemValue, onShow} = props;
const {children, onShow} = props;
useEffect(() => {
if (children) {
LogService.warn(`UILib Picker will stop supporting the 'children' prop in the next major version, please pass 'items' prop instead`);
}

if (migrate) {
LogService.warn(`UILib Picker will stop supporting the 'migrate' prop in the next major version, please stop using it. The picker uses the new implementation by default.`);
}

if (getItemLabel) {
LogService.warn(`UILib Picker will stop supporting the 'getItemLabel' prop in the next major version, please pass the 'getItemLabel' prop to the specific item instead`);
}

if (getItemValue) {
LogService.warn(`UILib Picker will stop supporting the 'getItemValue' prop in the next major version, please stop using it. The value will be extract from 'items' prop instead`);
}

if (onShow) {
LogService.warn(`UILib Picker will stop supporting the 'onShow' prop in the next major version, please pass the 'onShow' prop from the 'pickerModalProps' instead`);
}
Expand Down
13 changes: 6 additions & 7 deletions src/components/picker/helpers/usePickerSearch.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
import {useCallback, useState, useMemo} from 'react';
import _ from 'lodash';
import {PickerProps} from '../types';
import {getItemLabel as getItemLabelPresenter, shouldFilterOut} from '../PickerPresenter';
import {shouldFilterOut} from '../PickerPresenter';

type UsePickerSearchProps = Pick<PickerProps, 'showSearch' | 'onSearchChange' | 'children' | 'getItemLabel' | 'items'>;
type UsePickerSearchProps = Pick<PickerProps, 'showSearch' | 'onSearchChange' | 'children' | 'items'>;

const usePickerSearch = (props: UsePickerSearchProps) => {
const {showSearch, onSearchChange, children, getItemLabel, items} = props;
const {showSearch, onSearchChange, children, items} = props;
const [searchValue, setSearchValue] = useState('');

const filterItems = useCallback((items: any) => {
if (showSearch && !_.isEmpty(searchValue)) {
return _.filter(items, item => {
const {label, value, getItemLabel: childGetItemLabel} = item.props || item;
const itemLabel = getItemLabelPresenter(label, value, childGetItemLabel || getItemLabel);
return !shouldFilterOut(searchValue, itemLabel);
const {label} = item.props || item;
return !shouldFilterOut(searchValue, label);
});
}
return items;
},
[showSearch, searchValue, getItemLabel]);
[showSearch, searchValue]);

const filteredItems = useMemo(() => {
return filterItems(children || items);
Expand Down
14 changes: 4 additions & 10 deletions src/components/picker/helpers/usePickerSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import _ from 'lodash';
import {PickerProps, PickerValue, PickerSingleValue, PickerMultiValue, PickerModes} from '../types';

interface UsePickerSelectionProps
extends Pick<PickerProps, 'migrate' | 'value' | 'onChange' | 'getItemValue' | 'topBarProps' | 'mode' | 'items'> {
extends Pick<PickerProps, 'value' | 'onChange' | 'topBarProps' | 'mode' | 'items'> {
pickerExpandableRef: RefObject<any>;
setSearchValue: (searchValue: string) => void;
}

const usePickerSelection = (props: UsePickerSelectionProps) => {
const {migrate, value, onChange, topBarProps, pickerExpandableRef, getItemValue, setSearchValue, mode, items} = props;
const {value, onChange, topBarProps, pickerExpandableRef, setSearchValue, mode, items} = props;
const [multiDraftValue, setMultiDraftValue] = useState(value as PickerMultiValue);
const [multiFinalValue, setMultiFinalValue] = useState(value as PickerMultiValue);

Expand All @@ -29,17 +29,11 @@ const usePickerSelection = (props: UsePickerSelectionProps) => {
[onChange]);

const toggleItemSelection = useCallback((item: PickerSingleValue) => {
let newValue;
const itemAsArray = [item];
if (!migrate) {
newValue = _.xorBy(multiDraftValue, itemAsArray, getItemValue || 'value');
} else {
newValue = _.xor(multiDraftValue, itemAsArray);
}

const newValue = _.xor(multiDraftValue, itemAsArray);
setMultiDraftValue(newValue);
},
[multiDraftValue, getItemValue]);
[multiDraftValue]);

const cancelSelect = useCallback(() => {
setSearchValue('');
Expand Down
Loading