Skip to content
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

refactor: SelectionList multiple selection #22622

Merged
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
717e22b
chore: wip
thiagobrez Jun 22, 2023
ae15864
chore: fix conflicts
thiagobrez Jun 27, 2023
3cc3c93
chore: add avatar to checkbox item
thiagobrez Jun 27, 2023
647441f
chore: add errors to checkbox item
thiagobrez Jun 28, 2023
4cf5918
chore: wip
thiagobrez Jun 29, 2023
06b650c
chore: workspace invite page working
thiagobrez Jun 29, 2023
af2e1b9
chore: money request split page working
thiagobrez Jun 30, 2023
4a076cb
chore: new group page working
thiagobrez Jun 30, 2023
2059d9d
chore: fix conflicts
thiagobrez Jul 10, 2023
b2e57ad
chore: fix conflicts
thiagobrez Jul 10, 2023
128477f
chore: fix invite page
thiagobrez Jul 11, 2023
5404d2b
chore: cleanup
thiagobrez Jul 11, 2023
0ae6181
chore: fix conflicts
thiagobrez Jul 11, 2023
fba18bf
refactor: rename SelectionListRadio to SelectionList
thiagobrez Jul 11, 2023
246d6e0
chore: add stories for multiple selection
thiagobrez Jul 11, 2023
39c8c74
chore: add stories for multiple selection
thiagobrez Jul 11, 2023
4cdc5da
test: add more perf test cases
thiagobrez Jul 12, 2023
46f4328
chore: fix srolling issues
thiagobrez Jul 12, 2023
254d5d2
chore: address pr comments
thiagobrez Jul 13, 2023
b02b04a
chore: fix conflicts
thiagobrez Jul 13, 2023
2f8118a
chore: fix lint issues
thiagobrez Jul 13, 2023
cd96314
chore: make onSelectRow required
thiagobrez Jul 13, 2023
4d5c63d
Merge branch 'main' into refactor/selection_list/checkbox_list
thiagobrez Jul 14, 2023
87755e3
chore: fix conflicts
thiagobrez Jul 17, 2023
f21a96a
chore: fix YearPickerModal propTypes
thiagobrez Jul 24, 2023
990690c
chore: fix conflicts
thiagobrez Jul 24, 2023
35301b5
test: add test for formatMemberForList
thiagobrez Jul 25, 2023
137cff3
test: improve comment
thiagobrez Jul 25, 2023
3d12b6d
chore: fix conflicts
thiagobrez Jul 26, 2023
dc9b8a9
chore: fix lint issues
thiagobrez Jul 26, 2023
7a999c1
Merge branch 'main' into refactor/selection_list/checkbox_list
thiagobrez Jul 26, 2023
5f6921e
chore: fix lint issues
thiagobrez Jul 26, 2023
5199ca3
Merge branch 'main' into refactor/selection_list/checkbox_list
thiagobrez Jul 27, 2023
1acf551
feat(selection-list): focus next available index
thiagobrez Jul 28, 2023
6b34e19
fix(selection-list): highlight position on select
thiagobrez Aug 1, 2023
0321599
fix(selection-list): highlight position on select
thiagobrez Aug 2, 2023
3ac0892
chore: fix conflicts
thiagobrez Aug 2, 2023
fdc04d3
fix(selection-list): highlight for single section
thiagobrez Aug 2, 2023
be53f55
fix(selection-list): add fallback to avatar
thiagobrez Aug 7, 2023
6b0ebc4
chore: fix conflicts
thiagobrez Aug 7, 2023
da61781
feat(selection-list): add scroll indicator support
thiagobrez Aug 7, 2023
f856020
chore: fix conflicts
thiagobrez Aug 11, 2023
a683dac
chore: fix lint issues
thiagobrez Aug 11, 2023
194820e
chore: fix conflicts
thiagobrez Aug 14, 2023
9be8ad7
chore: fix conflicts
thiagobrez Aug 16, 2023
7197326
fix(selection-list): avatar generation
thiagobrez Aug 17, 2023
1b5a908
fix(selection-list): make rightElement generic
thiagobrez Aug 18, 2023
a97b078
chore: fix conflicts
thiagobrez Aug 18, 2023
64e4ba0
Merge branch 'main' into refactor/selection_list/checkbox_list
thiagobrez Aug 21, 2023
889e403
chore: fix conflicts
thiagobrez Aug 22, 2023
0b7be52
fix(selection-list): add loading placeholder
thiagobrez Aug 22, 2023
d9191ad
chore: fix tests
thiagobrez Aug 22, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import PropTypes from 'prop-types';
import _ from 'underscore';
import HeaderWithBackButton from '../../HeaderWithBackButton';
import CONST from '../../../CONST';
import SelectionListRadio from '../../SelectionListRadio';
import SelectionList from '../../SelectionList';
import Modal from '../../Modal';
import {radioListItemPropTypes} from '../../SelectionListRadio/selectionListRadioPropTypes';
import {radioListItemPropTypes} from '../../SelectionList/selectionListPropTypes';
import useLocalize from '../../../hooks/useLocalize';

const propTypes = {
Expand Down Expand Up @@ -62,7 +62,7 @@ function YearPickerModal(props) {
title={translate('yearPickerPage.year')}
onBackButtonPress={props.onClose}
/>
<SelectionListRadio
<SelectionList
shouldDelayFocus
textInputLabel={translate('yearPickerPage.selectYear')}
textInputValue={searchText}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,52 @@ import TextInput from '../TextInput';
import ArrowKeyFocusManager from '../ArrowKeyFocusManager';
import CONST from '../../CONST';
import variables from '../../styles/variables';
import {propTypes as selectionListRadioPropTypes, defaultProps as selectionListRadioDefaultProps} from './selectionListRadioPropTypes';
import {propTypes as selectionListPropTypes} from './selectionListPropTypes';
import RadioListItem from './RadioListItem';
import CheckboxListItem from './CheckboxListItem';
import useKeyboardShortcut from '../../hooks/useKeyboardShortcut';
import SafeAreaConsumer from '../SafeAreaConsumer';
import withKeyboardState, {keyboardStatePropTypes} from '../withKeyboardState';
import Checkbox from '../Checkbox';
import PressableWithFeedback from '../Pressable/PressableWithFeedback';
import FixedFooter from '../FixedFooter';
import Button from '../Button';
import useLocalize from '../../hooks/useLocalize';
import Log from '../../libs/Log';

const propTypes = {
...keyboardStatePropTypes,
...selectionListRadioPropTypes,
...selectionListPropTypes,
};

function BaseSelectionListRadio(props) {
function BaseSelectionList({
sections,
canSelectMultiple = false,
onSelectRow,
onSelectAll,
onDismissError,
textInputLabel = '',
textInputPlaceholder = '',
textInputValue = '',
textInputMaxLength,
thiagobrez marked this conversation as resolved.
Show resolved Hide resolved
keyboardType = CONST.KEYBOARD_TYPE.DEFAULT,
onChangeText,
initiallyFocusedOptionKey = '',
shouldDelayFocus = false,
onScroll,
onScrollBeginDrag,
headerMessage = '',
confirmButtonText = '',
onConfirm,
isKeyboardShown = false,
}) {
const {translate} = useLocalize();
const listRef = useRef(null);
const textInputRef = useRef(null);
const focusTimeoutRef = useRef(null);
const shouldShowTextInput = Boolean(props.textInputLabel);
const shouldShowTextInput = Boolean(textInputLabel);
const shouldShowSelectAll = Boolean(onSelectAll);
const shouldShowConfirmButton = Boolean(onConfirm);

/**
* Iterates through the sections and items inside each section, and builds 3 arrays along the way:
Expand All @@ -44,23 +74,23 @@ function BaseSelectionListRadio(props) {
let offset = 0;
const itemLayouts = [{length: 0, offset}];

_.each(props.sections, (section, sectionIndex) => {
// We're not rendering any section header, but we need to push to the array
// because React Native accounts for it in getItemLayout
const sectionHeaderHeight = 0;
let selectedCount = 0;

_.each(sections, (section, sectionIndex) => {
const sectionHeaderHeight = variables.optionsListSectionHeaderHeight;
itemLayouts.push({length: sectionHeaderHeight, offset});
offset += sectionHeaderHeight;

_.each(section.data, (option, optionIndex) => {
_.each(section.data, (item, optionIndex) => {
// Add item to the general flattened array
allOptions.push({
...option,
...item,
sectionIndex,
index: optionIndex,
});

// If disabled, add to the disabled indexes array
if (section.isDisabled || option.isDisabled) {
if (section.isDisabled || item.isDisabled) {
disabledOptionsIndexes.push(disabledIndex);
}
disabledIndex += 1;
Expand All @@ -69,6 +99,10 @@ function BaseSelectionListRadio(props) {
const fullItemHeight = variables.optionRowHeight;
itemLayouts.push({length: fullItemHeight, offset});
offset += fullItemHeight;

if (item.isSelected) {
selectedCount++;
}
});

// We're not rendering any section footer, but we need to push to the array
Expand All @@ -80,10 +114,17 @@ function BaseSelectionListRadio(props) {
// because React Native accounts for it in getItemLayout
itemLayouts.push({length: 0, offset});

if (selectedCount > 1 && !canSelectMultiple) {
Log.alert(
'Dev error: SelectionList - multiple items are selected but prop `canSelectMultiple` is false. Please enable `canSelectMultiple` or make your list have only 1 item with `isSelected: true`.',
);
}
thiagobrez marked this conversation as resolved.
Show resolved Hide resolved

return {
allOptions,
disabledOptionsIndexes,
itemLayouts,
allSelected: selectedCount > 0 && selectedCount === allOptions.length - disabledOptionsIndexes.length,
};
};

Expand All @@ -92,7 +133,7 @@ function BaseSelectionListRadio(props) {
const [focusedIndex, setFocusedIndex] = useState(() => {
const defaultIndex = 0;

const indexOfInitiallyFocusedOption = _.findIndex(flattenedSections.allOptions, (option) => option.keyForList === props.initiallyFocusedOptionKey);
const indexOfInitiallyFocusedOption = _.findIndex(flattenedSections.allOptions, (option) => option.keyForList === initiallyFocusedOptionKey);

if (indexOfInitiallyFocusedOption >= 0) {
return indexOfInitiallyFocusedOption;
Expand Down Expand Up @@ -122,7 +163,7 @@ function BaseSelectionListRadio(props) {
// Otherwise, it will cause an index-out-of-bounds error and crash the app.
let adjustedSectionIndex = sectionIndex;
for (let i = 0; i < sectionIndex; i++) {
if (_.isEmpty(lodashGet(props.sections, `[${i}].data`))) {
if (_.isEmpty(lodashGet(sections, `[${i}].data`))) {
adjustedSectionIndex--;
}
}
Expand Down Expand Up @@ -156,22 +197,49 @@ function BaseSelectionListRadio(props) {
};
};

const renderSectionHeader = ({section}) => {
if (!section.title || _.isEmpty(section.data)) {
return null;
}

return (
// Note: The `optionsListSectionHeader` style provides an explicit height to section headers.
// We do this so that we can reference the height in `getItemLayout` –
// we need to know the heights of all list items up-front in order to synchronously compute the layout of any given list item.
// So be aware that if you adjust the content of the section header (for example, change the font size), you may need to adjust this explicit height as well.
thiagobrez marked this conversation as resolved.
Show resolved Hide resolved
<View style={[styles.optionsListSectionHeader, styles.justifyContentCenter]}>
<Text style={[styles.ph5, styles.textLabelSupporting]}>{section.title}</Text>
</View>
);
};

const renderItem = ({item, index, section}) => {
const isFocused = focusedIndex === index + lodashGet(section, 'indexOffset', 0);

if (canSelectMultiple) {
return (
<CheckboxListItem
item={item}
isFocused={isFocused}
onSelectRow={onSelectRow}
onDismissError={onDismissError}
/>
);
}

return (
<RadioListItem
item={item}
isFocused={isFocused}
onSelectRow={props.onSelectRow}
onSelectRow={onSelectRow}
/>
);
};

/** Focuses the text input when the component mounts. If `props.shouldDelayFocus` is true, we wait for the animation to finish */
useEffect(() => {
if (shouldShowTextInput) {
if (props.shouldDelayFocus) {
if (shouldDelayFocus) {
focusTimeoutRef.current = setTimeout(() => textInputRef.current.focus(), CONST.ANIMATED_TRANSITION);
} else {
textInputRef.current.focus();
Expand All @@ -184,8 +252,9 @@ function BaseSelectionListRadio(props) {
}
clearTimeout(focusTimeoutRef.current);
};
}, [props.shouldDelayFocus, shouldShowTextInput]);
}, [shouldDelayFocus, shouldShowTextInput]);

/** Selects row when pressing enter */
useKeyboardShortcut(
CONST.KEYBOARD_SHORTCUTS.ENTER,
() => {
Expand All @@ -195,7 +264,7 @@ function BaseSelectionListRadio(props) {
return;
}

props.onSelectRow(focusedOption);
onSelectRow(focusedOption);
},
{
captureOnInputs: true,
Expand All @@ -215,56 +284,86 @@ function BaseSelectionListRadio(props) {
>
<SafeAreaConsumer>
{({safeAreaPaddingBottomStyle}) => (
<View style={[styles.flex1, !props.isKeyboardShown && safeAreaPaddingBottomStyle]}>
<View style={[styles.flex1, !isKeyboardShown && safeAreaPaddingBottomStyle]}>
{shouldShowTextInput && (
<View style={[styles.ph5, styles.pv5]}>
<TextInput
ref={textInputRef}
label={props.textInputLabel}
accessibilityLabel={props.textInputLabel}
label={textInputLabel}
accessibilityLabel={textInputLabel}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT}
value={props.textInputValue}
placeholder={props.textInputPlaceholder}
maxLength={props.textInputMaxLength}
onChangeText={props.onChangeText}
keyboardType={props.keyboardType}
value={textInputValue}
placeholder={textInputPlaceholder}
maxLength={textInputMaxLength}
onChangeText={onChangeText}
keyboardType={keyboardType}
selectTextOnFocus
/>
</View>
)}
{Boolean(props.headerMessage) && (
{Boolean(headerMessage) && (
<View style={[styles.ph5, styles.pb5]}>
<Text style={[styles.textLabel, styles.colorMuted]}>{props.headerMessage}</Text>
<Text style={[styles.textLabel, styles.colorMuted]}>{headerMessage}</Text>
</View>
)}
{!headerMessage && canSelectMultiple && shouldShowSelectAll && (
<PressableWithFeedback
style={[styles.peopleRow, styles.userSelectNone, styles.ph5, styles.pb3]}
onPress={onSelectAll}
accessibilityLabel={translate('workspace.people.selectAll')}
accessibilityRole="button"
accessibilityState={{checked: flattenedSections.allSelected}}
>
<Checkbox
accessibilityLabel={translate('workspace.people.selectAll')}
isChecked={flattenedSections.allSelected}
onPress={onSelectAll}
/>
<View style={[styles.flex1]}>
<Text style={[styles.textStrong, styles.ph5]}>{translate('workspace.people.selectAll')}</Text>
</View>
</PressableWithFeedback>
)}
<SectionList
ref={listRef}
sections={props.sections}
sections={sections}
renderSectionHeader={renderSectionHeader}
renderItem={renderItem}
getItemLayout={getItemLayout}
onScroll={props.onScroll}
onScrollBeginDrag={props.onScrollBeginDrag}
onScroll={onScroll}
onScrollBeginDrag={onScrollBeginDrag}
keyExtractor={(item) => item.keyForList}
onLayout={() => scrollToIndex(focusedIndex, false)}
extraData={focusedIndex}
indicatorStyle="white"
keyboardShouldPersistTaps="always"
showsVerticalScrollIndicator={false}
OP
initialNumToRender={12}
maxToRenderPerBatch={5}
windowSize={5}
viewabilityConfig={{viewAreaCoveragePercentThreshold: 95}}
onLayout={() => scrollToIndex(focusedIndex, false)}
testID="selection-list"
/>
{shouldShowConfirmButton && (
<FixedFooter>
<Button
success
style={[styles.w100]}
text={confirmButtonText || translate('common.confirm')}
onPress={onConfirm}
pressOnEnter
enterKeyEventListenerPriority={1}
/>
</FixedFooter>
)}
</View>
)}
</SafeAreaConsumer>
</ArrowKeyFocusManager>
);
}

BaseSelectionListRadio.displayName = 'BaseSelectionListRadio';
BaseSelectionListRadio.propTypes = propTypes;
BaseSelectionListRadio.defaultProps = selectionListRadioDefaultProps;
BaseSelectionList.displayName = 'BaseSelectionList';
BaseSelectionList.propTypes = propTypes;

export default withKeyboardState(BaseSelectionListRadio);
export default withKeyboardState(BaseSelectionList);
Loading