diff --git a/docs/getting-started/v8.md b/docs/getting-started/v8.md index 2ffbc1fda0..586c2e3ad1 100644 --- a/docs/getting-started/v8.md +++ b/docs/getting-started/v8.md @@ -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 diff --git a/src/components/picker/PickerItem.tsx b/src/components/picker/PickerItem.tsx index 2b4405258d..7afef0b2ba 100644 --- a/src/components/picker/PickerItem.tsx +++ b/src/components/picker/PickerItem.tsx @@ -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 @@ -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, @@ -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); @@ -84,7 +76,7 @@ const PickerItem = (props: PickerItemProps) => { return ( - {itemLabel} + {label} {selectedIndicator} @@ -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()} ); }; diff --git a/src/components/picker/PickerPresenter.ts b/src/components/picker/PickerPresenter.ts index 482047b3c3..4d27a5fbb4 100644 --- a/src/components/picker/PickerPresenter.ts +++ b/src/components/picker/PickerPresenter.ts @@ -19,8 +19,7 @@ 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; @@ -28,25 +27,6 @@ export function isItemSelected(childValue: PickerSingleValue, selectedValue?: Pi 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)); } diff --git a/src/components/picker/__tests__/PickerPresenter.spec.js b/src/components/picker/__tests__/PickerPresenter.spec.js index c23883855e..19b90be019 100644 --- a/src/components/picker/__tests__/PickerPresenter.spec.js +++ b/src/components/picker/__tests__/PickerPresenter.spec.js @@ -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'); - }); - }); }); diff --git a/src/components/picker/api/picker.api.json b/src/components/picker/api/picker.api.json index ef9381df89..87d5e82dfe 100644 --- a/src/components/picker/api/picker.api.json +++ b/src/components/picker/api/picker.api.json @@ -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", diff --git a/src/components/picker/api/pickerItem.api.json b/src/components/picker/api/pickerItem.api.json index 865925c4b0..88d8156fce 100644 --- a/src/components/picker/api/pickerItem.api.json +++ b/src/components/picker/api/pickerItem.api.json @@ -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"}, diff --git a/src/components/picker/helpers/__tests__/usePickerLabel.spec.ts b/src/components/picker/helpers/__tests__/usePickerLabel.spec.ts index c2db686de5..e1667ce951 100644 --- a/src/components/picker/helpers/__tests__/usePickerLabel.spec.ts +++ b/src/components/picker/helpers/__tests__/usePickerLabel.spec.ts @@ -26,7 +26,6 @@ describe('usePickerLabel hook tests', () => { value, items, getLabel - // getItemLabel, // accessibilityLabel, // accessibilityHint, // placeholder @@ -36,7 +35,6 @@ describe('usePickerLabel hook tests', () => { value, items, getLabel - // getItemLabel, // accessibilityLabel, // accessibilityHint, // placeholder diff --git a/src/components/picker/helpers/usePickerLabel.tsx b/src/components/picker/helpers/usePickerLabel.tsx index 12224e1a08..0ab495aade 100644 --- a/src/components/picker/helpers/usePickerLabel.tsx +++ b/src/components/picker/helpers/usePickerLabel.tsx @@ -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))) { diff --git a/src/components/picker/helpers/usePickerMigrationWarnings.tsx b/src/components/picker/helpers/usePickerMigrationWarnings.tsx index 95f074deca..799955cd78 100644 --- a/src/components/picker/helpers/usePickerMigrationWarnings.tsx +++ b/src/components/picker/helpers/usePickerMigrationWarnings.tsx @@ -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; 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`); } diff --git a/src/components/picker/helpers/usePickerSearch.tsx b/src/components/picker/helpers/usePickerSearch.tsx index 8a52a85b8f..b390788ee4 100644 --- a/src/components/picker/helpers/usePickerSearch.tsx +++ b/src/components/picker/helpers/usePickerSearch.tsx @@ -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; +type UsePickerSearchProps = Pick; 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); diff --git a/src/components/picker/helpers/usePickerSelection.tsx b/src/components/picker/helpers/usePickerSelection.tsx index f8ae064a52..177048b107 100644 --- a/src/components/picker/helpers/usePickerSelection.tsx +++ b/src/components/picker/helpers/usePickerSelection.tsx @@ -3,13 +3,13 @@ import _ from 'lodash'; import {PickerProps, PickerValue, PickerSingleValue, PickerMultiValue, PickerModes} from '../types'; interface UsePickerSelectionProps - extends Pick { + extends Pick { pickerExpandableRef: RefObject; 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); @@ -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(''); diff --git a/src/components/picker/index.tsx b/src/components/picker/index.tsx index 3cf21536d8..30e75786b5 100644 --- a/src/components/picker/index.tsx +++ b/src/components/picker/index.tsx @@ -67,13 +67,9 @@ const Picker = React.forwardRef((props: PickerProps, ref) => { listProps, value, getLabel, - getItemLabel, - getItemValue, renderItem, children, useSafeArea, - // TODO: Remove migrate props and migrate code - migrate = true, accessibilityLabel, accessibilityHint, items: propItems, @@ -90,9 +86,6 @@ const Picker = React.forwardRef((props: PickerProps, ref) => { const pickerExpandable = useRef(null); const pickerRef = useImperativePickerHandle(ref, pickerExpandable); - // TODO: Remove this when migration is completed, starting of v8 - // usePickerMigrationWarnings({children, migrate, getItemLabel, getItemValue}); - useEffect(() => { if (propItems) { setItems(propItems); @@ -103,7 +96,7 @@ const Picker = React.forwardRef((props: PickerProps, ref) => { filteredItems, setSearchValue, onSearchChange: _onSearchChange - } = usePickerSearch({showSearch, onSearchChange, getItemLabel, children, items}); + } = usePickerSearch({showSearch, onSearchChange, children, items}); const { multiDraftValue, onDoneSelecting, @@ -113,11 +106,9 @@ const Picker = React.forwardRef((props: PickerProps, ref) => { selectedCount, toggleAllItemsSelection } = usePickerSelection({ - migrate, value, onChange, pickerExpandableRef: pickerExpandable, - getItemValue, topBarProps, setSearchValue, mode, @@ -128,8 +119,10 @@ const Picker = React.forwardRef((props: PickerProps, ref) => { if (propItems) { return filteredItems.map((item: PickerItemProps) => ({ ...item, - onPress: useWheelPicker && Constants.accessibility.isScreenReaderEnabled ? - () => onDoneSelecting(item.value) : undefined + onPress: + useWheelPicker && Constants.accessibility.isScreenReaderEnabled + ? () => onDoneSelecting(item.value) + : undefined })); } return filteredItems; @@ -138,7 +131,6 @@ const Picker = React.forwardRef((props: PickerProps, ref) => { const {label, accessibilityInfo} = usePickerLabel({ value, items, - getItemLabel, getLabel, accessibilityLabel, accessibilityHint, @@ -163,15 +155,10 @@ const Picker = React.forwardRef((props: PickerProps, ref) => { }, []); const contextValue = useMemo(() => { - // @ts-expect-error cleanup after removing migrate prop - const pickerValue = !migrate && typeof value === 'object' && !_.isArray(value) ? value?.value : value; return { - migrate, - value: mode === PickerModes.MULTI ? multiDraftValue : pickerValue, + value: mode === PickerModes.MULTI ? multiDraftValue : value, onPress: mode === PickerModes.MULTI ? toggleItemSelection : onDoneSelecting, isMultiMode: mode === PickerModes.MULTI, - getItemValue, - getItemLabel, onSelectedLayout: onSelectedItemLayout, renderItem, selectionLimit, @@ -180,13 +167,10 @@ const Picker = React.forwardRef((props: PickerProps, ref) => { toggleAllItemsSelection }; }, [ - migrate, mode, value, multiDraftValue, renderItem, - getItemValue, - getItemLabel, selectionLimit, onSelectedItemLayout, toggleItemSelection, diff --git a/src/components/picker/types.ts b/src/components/picker/types.ts index f92a75a4ed..f7cf075484 100644 --- a/src/components/picker/types.ts +++ b/src/components/picker/types.ts @@ -50,21 +50,6 @@ export interface PickerSearchStyle { } type PickerPropsDeprecation = { - /** - * @deprecated - * Temporary prop required for migration to Picker's new API - */ - migrate?: boolean; - /** - * @deprecated - * A function that extract the unique value out of the value prop in case value has a custom structure (e.g. {myValue, myLabel}) - */ - getItemValue?: (value: PickerValue) => any; - /** - * @deprecated - * A function that extract the label out of the value prop in case value has a custom structure (e.g. {myValue, myLabel}) - */ - getItemLabel?: (value: PickerValue) => string; /** * @deprecated * Callback for modal onShow event @@ -315,14 +300,6 @@ export interface PickerItemProps extends Pick; - /** - * Custom function for the item label (e.g (value) => customLabel) - */ - getItemLabel?: (value: PickerValue) => string; - /** - * @deprecated Function to return the value out of the item value prop when value is custom shaped. - */ - getItemValue?: PickerProps['getItemValue']; /** * Render custom item */ @@ -351,8 +328,7 @@ export interface PickerItemProps extends Pick { +export interface PickerContextProps extends Pick { onPress: (value: PickerSingleValue) => void; isMultiMode: boolean; onSelectedLayout: (event: any) => any;