Skip to content

Picker selection control bar and toggle all items functionality #3557

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 34 commits into from
Mar 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3fe8e8d
Picker selection control bar and toggle all items functionality
adids1221 Mar 6, 2025
c0c0abd
Merge branch 'master' into feat/Picker_selection_control_bar
M-i-k-e-l Mar 11, 2025
81ee794
Refactor PickerScreen layout
adids1221 Mar 17, 2025
fb6f149
Rename PickerSelectionControlBar to PickerSelectionStatusToolbar and …
adids1221 Mar 17, 2025
557cc54
Enhance selection status label functionality in PickerSelectionStatus…
adids1221 Mar 17, 2025
fa26403
Refactor PickerSelectionStatusToolbar and types for improved selectio…
adids1221 Mar 17, 2025
ed00693
Remove 'none' option from select all types in Picker and update relat…
adids1221 Mar 17, 2025
f589ba1
Fix types issue in PickerItemsList
adids1221 Mar 17, 2025
c612817
Refactor toggleAllItemsSelection and availableItems usage
adids1221 Mar 17, 2025
24e1569
Remove customLabel prop from PickerSelectionStatusProps
adids1221 Mar 17, 2025
a07a980
Add renderTopCustomElement and renderBottomCustomElement props to Pic…
adids1221 Mar 17, 2025
2b71bbb
Rename getLabel prop to getSelectionStatusLabe and update related usages
adids1221 Mar 18, 2025
a87e350
Refactor selectionStatus handling in PickerItemsList
adids1221 Mar 18, 2025
3c4dc79
Remove unnecessary null return in renderLabel
adids1221 Mar 18, 2025
23f0fec
Remove renderTopCustomElement prop
adids1221 Mar 18, 2025
59af447
Rename _onSegmentChange to onSegmentChange
adids1221 Mar 18, 2025
171e20b
Fix TypeScript ignore directive for selectAllType in Picker
adids1221 Mar 18, 2025
2d4d7d4
Update buttonProps onPress parameter to use selectionValue and set de…
adids1221 Mar 18, 2025
cdb80a2
Refactor Picker selection logic to use areAllItemsSelected and simpli…
adids1221 Mar 18, 2025
91aa052
Rename getSelectionStatusLabel to getLabel and update type definition…
adids1221 Mar 20, 2025
f76d7f8
Remove renderBottomCustomElement prop and add divider in PickerSelect…
adids1221 Mar 20, 2025
a93f066
Refactor Picker component to remove PickerSelectAllType enum
adids1221 Mar 20, 2025
9d75b2b
Refactor PickerSelectionStatusToolbar to simplify props and selection…
adids1221 Mar 20, 2025
f0ef350
Rename PickerSelectionStatusToolbar to PickerSelectionStatusBar and u…
adids1221 Mar 20, 2025
7d2086d
Update buttonProps onPress to use customValue instead of selectionVal…
adids1221 Mar 20, 2025
de9f42f
Add selectAllType prop to PickerSelectionStatusBar for flexible selec…
adids1221 Mar 20, 2025
45bca54
Refactor PickerSelectionStatusBar to remove selectAllType prop and si…
adids1221 Mar 20, 2025
0b584ff
Fix type assertion for value in PickerSelectionStatusBar to ensure co…
adids1221 Mar 20, 2025
dafe4f0
Refactor renderSelectionStatus, fix when showLabel is false
adids1221 Mar 20, 2025
34d7710
Add PickerSelectionStatusProps import to PickerScreen and index files
adids1221 Mar 23, 2025
5735e78
Remove unused setMultiFinalValue from usePickerSelection hook
adids1221 Mar 23, 2025
4abcfd7
Remove value from PickerSelectionStatusLabelData
adids1221 Mar 23, 2025
373e9d4
selectedCount moved to usePickerSelection
adids1221 Mar 24, 2025
40790cd
PickerSelectionStatusBar simplify selectedCount
adids1221 Mar 24, 2025
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
51 changes: 32 additions & 19 deletions demo/src/screens/componentScreens/PickerScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import {
PanningProvider,
PickerProps,
RenderCustomModalProps,
PickerMethods
PickerMethods,
SegmentedControl,
PickerSelectionStatusProps
} from 'react-native-ui-lib'; //eslint-disable-line
import contactsData from '../../data/conversations';
import {longOptions} from './PickerScreenLongOptions';
Expand All @@ -24,6 +26,16 @@ const tagIcon = require('../../assets/icons/tags.png');
const dropdown = require('../../assets/icons/chevronDown.png');
const dropdownIcon = <Icon source={dropdown} tintColor={Colors.$iconDefault}/>;

const selectAllSegment = [{label: 'button'}, {label: 'checkbox'}];

const buttonProps = {
onPress: (selectionValue: any) => console.log('onPress', selectionValue)
};

const checkboxProps = {
onValueChange: (value: boolean) => console.log('onValueChange', value)
};

const renderContact = (contactValue: any, props: any) => {
const contact = contacts[contactValue as number];
return (
Expand Down Expand Up @@ -107,7 +119,12 @@ export default class PickerScreen extends Component {
filter: undefined,
statOption: [],
scheme: undefined,
contact: 0
contact: 0,
selectAllType: 'button' as PickerSelectionStatusProps['selectAllType']
};

onSegmentChange = (index: number) => {
this.setState({selectAllType: selectAllSegment[index].label});
};

renderDialog: PickerProps['renderOverlay'] = (modalProps: RenderCustomModalProps) => {
Expand Down Expand Up @@ -148,7 +165,7 @@ export default class PickerScreen extends Component {
<Text text40 $textDefault>
Picker
</Text>

<Picker
placeholder="Favorite Language"
floatingPlaceholder
Expand Down Expand Up @@ -215,9 +232,12 @@ export default class PickerScreen extends Component {
items={dialogOptions}
/>

<Text text70 $textDefault>
Custom Top Element:
</Text>
<View row spread centerV>
<Text text70 $textDefault>
Selection Status:
</Text>
<SegmentedControl segments={selectAllSegment} onChangeIndex={this.onSegmentChange}/>
</View>
<Picker
placeholder="Status"
floatingPlaceholder
Expand All @@ -226,21 +246,14 @@ export default class PickerScreen extends Component {
topBarProps={{title: 'Status'}}
mode={Picker.modes.MULTI}
items={statusOptions}
renderCustomTopElement={value => {
const allOptionsSelected = Array.isArray(value) && value.length === statusOptions.length;
return (
<View margin-s3>
<Button
label={allOptionsSelected ? 'Unselect All' : 'Select All'}
onPress={() => this.onTopElementPress(allOptionsSelected)}
size="small"
/>
</View>
);
selectionStatus={{
selectAllType: this.state.selectAllType,
buttonProps,
checkboxProps
}}
/>

<Text marginB-10 text70 $textDefault>
<Text marginV-10 text70 $textDefault>
Custom Picker:
</Text>
<Picker
Expand Down Expand Up @@ -299,7 +312,7 @@ export default class PickerScreen extends Component {
style={{alignSelf: 'flex-start'}}
onPress={() => this.picker.current?.openExpandable?.()}
/>

<Text text60 marginT-s5>
Different Field Types
</Text>
Expand Down
8 changes: 7 additions & 1 deletion src/components/picker/PickerItemsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {PickerItemProps, PickerItemsListProps, PickerSingleValue, PickerModes} f
import PickerContext from './PickerContext';
import PickerItem from './PickerItem';
import {Constants} from '../../commons/new';
import PickerSelectionStatusBar from './PickerSelectionStatusBar';

const keyExtractor = (_item: string, index: number) => index.toString();

Expand All @@ -35,7 +36,8 @@ const PickerItemsList = (props: PickerItemsListProps) => {
testID,
showLoader,
customLoaderElement,
renderCustomTopElement
renderCustomTopElement,
selectionStatus: selectionStatusProps
} = props;
const context = useContext(PickerContext);

Expand Down Expand Up @@ -169,13 +171,17 @@ const PickerItemsList = (props: PickerItemsListProps) => {
);
};

const selectionStatus = useMemo(() => mode === PickerModes.MULTI && selectionStatusProps && <PickerSelectionStatusBar {...selectionStatusProps}/>,
[selectionStatusProps, mode]);

const renderContent = () => {
return useWheelPicker ? (
renderWheel()
) : (
<>
{renderSearchInput()}
{renderCustomTopElement?.(context.value)}
{selectionStatus}
{renderList()}
</>
);
Expand Down
77 changes: 77 additions & 0 deletions src/components/picker/PickerSelectionStatusBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React, {useCallback, useContext} from 'react';
import {StyleSheet} from 'react-native';
import Button from '../button';
import Checkbox from '../checkbox';
import View from '../view';
import Text from '../text';
import Dividers from '../../style/dividers';
import PickerContext from './PickerContext';
import {PickerSelectionStatusProps} from './types';

export default function PickerSelectionStatusBar(props: PickerSelectionStatusProps) {
const {containerStyle, getLabel, showLabel = true} = props;
const context = useContext(PickerContext);
const {toggleAllItemsSelection, selectedCount, areAllItemsSelected} = context;

const checkboxIndeterminate = selectedCount > 0 && !areAllItemsSelected;
const label =
getLabel?.({selectedCount, areAllItemsSelected}) ??
`${selectedCount} Selected ${areAllItemsSelected ? '(All)' : ''}`;

const handlePress = useCallback(() => {
const newSelectionState = !areAllItemsSelected;
toggleAllItemsSelection?.(newSelectionState);
if (props.selectAllType === 'button') {
props?.buttonProps?.onPress?.({customValue: newSelectionState});
} else if (props.selectAllType === 'checkbox') {
props?.checkboxProps?.onValueChange?.(newSelectionState);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [areAllItemsSelected, toggleAllItemsSelection]);

const renderSelectionStatus = () => {
return (
<View flexG={!showLabel} style={!showLabel && styles.componentContainer}>
{props.selectAllType === 'button' ? (
<Button
label={areAllItemsSelected ? 'Deselect All' : 'Select All'}
link
{...props?.buttonProps}
onPress={handlePress}
/>
) : (
props.selectAllType === 'checkbox' && (
<Checkbox
{...props.checkboxProps}
value={selectedCount > 0}
indeterminate={checkboxIndeterminate}
onValueChange={handlePress}
/>
)
)}
</View>
);
};

const renderLabel = () => {
if (showLabel) {
return <Text>{label}</Text>;
}
};

return (
<View>
<View row spread paddingH-page marginV-s4 style={containerStyle}>
{renderLabel()}
{renderSelectionStatus()}
</View>
<View style={Dividers.d20}/>
</View>
);
}

const styles = StyleSheet.create({
componentContainer: {
alignItems: 'flex-end'
}
});
28 changes: 24 additions & 4 deletions src/components/picker/helpers/usePickerSelection.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import {RefObject, useCallback, useState, useEffect} from 'react';
import {RefObject, useCallback, useState, useEffect, useMemo} from 'react';
import _ from 'lodash';
import {PickerProps, PickerValue, PickerSingleValue, PickerMultiValue, PickerModes} from '../types';

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

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

Expand Down Expand Up @@ -48,11 +48,31 @@ const usePickerSelection = (props: UsePickerSelectionProps) => {
topBarProps?.onCancel?.();
}, [multiFinalValue, topBarProps]);

const availableItems: PickerMultiValue = useMemo(() => {
return items?.filter(item => !item.disabled).map(item => item.value) || [];
}, [items]);

const areAllItemsSelected = useMemo(() => {
return multiDraftValue?.length === availableItems.length;
}, [multiDraftValue, availableItems]);

const selectedCount = useMemo(() => {
return multiDraftValue?.length;
}, [multiDraftValue]);

const toggleAllItemsSelection = useCallback((selectAll: boolean) => {
setMultiDraftValue(selectAll ? availableItems : []);
},
[availableItems]);

return {
multiDraftValue,
onDoneSelecting,
toggleItemSelection,
cancelSelect
cancelSelect,
areAllItemsSelected,
selectedCount,
toggleAllItemsSelection
};
};

Expand Down
31 changes: 25 additions & 6 deletions src/components/picker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ import {
PickerSearchStyle,
RenderCustomModalProps,
PickerItemsListProps,
PickerMethods
PickerMethods,
PickerSelectionStatusProps
} from './types';
import {DialogProps} from '../../incubator/dialog';

Expand Down Expand Up @@ -79,6 +80,7 @@ const Picker = React.forwardRef((props: PickerProps, ref) => {
showLoader,
customLoaderElement,
renderCustomTopElement,
selectionStatus,
...others
} = themeProps;
const {preset, placeholder, style, trailingAccessory, label: propsLabel} = others;
Expand All @@ -102,15 +104,24 @@ const Picker = React.forwardRef((props: PickerProps, ref) => {
setSearchValue,
onSearchChange: _onSearchChange
} = usePickerSearch({showSearch, onSearchChange, getItemLabel, children, items});
const {multiDraftValue, onDoneSelecting, toggleItemSelection, cancelSelect} = usePickerSelection({
const {
multiDraftValue,
onDoneSelecting,
toggleItemSelection,
cancelSelect,
areAllItemsSelected,
selectedCount,
toggleAllItemsSelection
} = usePickerSelection({
migrate,
value,
onChange,
pickerExpandableRef: pickerExpandable,
getItemValue,
topBarProps,
setSearchValue,
mode
mode,
items
});

const {label, accessibilityInfo} = usePickerLabel({
Expand Down Expand Up @@ -152,7 +163,10 @@ const Picker = React.forwardRef((props: PickerProps, ref) => {
getItemLabel,
onSelectedLayout: onSelectedItemLayout,
renderItem,
selectionLimit
selectionLimit,
areAllItemsSelected,
selectedCount,
toggleAllItemsSelection
};
}, [
migrate,
Expand All @@ -165,7 +179,10 @@ const Picker = React.forwardRef((props: PickerProps, ref) => {
selectionLimit,
onSelectedItemLayout,
toggleItemSelection,
onDoneSelecting
onDoneSelecting,
areAllItemsSelected,
selectedCount,
toggleAllItemsSelection
]);

const renderPickerItem = useCallback((item: PickerItemProps, index: number): React.ReactElement => {
Expand Down Expand Up @@ -247,6 +264,7 @@ const Picker = React.forwardRef((props: PickerProps, ref) => {
showLoader={showLoader}
customLoaderElement={customLoaderElement}
renderCustomTopElement={renderCustomTopElement}
selectionStatus={selectionStatus}
>
{filteredItems}
</PickerItemsList>
Expand Down Expand Up @@ -316,7 +334,8 @@ export {
PickerSearchStyle,
RenderCustomModalProps,
PickerItemsListProps,
PickerMethods
PickerMethods,
PickerSelectionStatusProps
};
export {Picker}; // For tests
export default Picker as typeof Picker & PickerStatics;
Loading