diff --git a/src/components/SelectionList/BaseListItem.js b/src/components/SelectionList/BaseListItem.js new file mode 100644 index 000000000000..d198979fdf8e --- /dev/null +++ b/src/components/SelectionList/BaseListItem.js @@ -0,0 +1,100 @@ +import React from 'react'; +import {View} from 'react-native'; +import lodashGet from 'lodash/get'; +import PressableWithFeedback from '../Pressable/PressableWithFeedback'; +import styles from '../../styles/styles'; +import Icon from '../Icon'; +import * as Expensicons from '../Icon/Expensicons'; +import themeColors from '../../styles/themes/default'; +import {baseListItemPropTypes} from './selectionListPropTypes'; +import * as StyleUtils from '../../styles/StyleUtils'; +import UserListItem from './UserListItem'; +import RadioListItem from './RadioListItem'; +import OfflineWithFeedback from '../OfflineWithFeedback'; +import CONST from '../../CONST'; + +function BaseListItem({item, isFocused = false, isDisabled = false, showTooltip, canSelectMultiple = false, onSelectRow, onDismissError = () => {}}) { + const isUserItem = lodashGet(item, 'icons.length', 0) > 0; + const ListItem = isUserItem ? UserListItem : RadioListItem; + + return ( + onDismissError(item)} + pendingAction={item.pendingAction} + errors={item.errors} + errorRowStyles={styles.ph5} + > + onSelectRow(item)} + disabled={isDisabled} + accessibilityLabel={item.text} + accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + hoverDimmingValue={1} + hoverStyle={styles.hoveredComponentBG} + dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} + > + + {canSelectMultiple && ( + + + {item.isSelected && ( + + )} + + + )} + + + + {!canSelectMultiple && item.isSelected && ( + + + + + + )} + + + + ); +} + +BaseListItem.displayName = 'BaseListItem'; +BaseListItem.propTypes = baseListItemPropTypes; + +export default BaseListItem; diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.js index 8d894e4c983a..ebb95475bcd9 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.js @@ -7,12 +7,9 @@ import SectionList from '../SectionList'; import Text from '../Text'; import styles from '../../styles/styles'; import TextInput from '../TextInput'; -import ArrowKeyFocusManager from '../ArrowKeyFocusManager'; import CONST from '../../CONST'; import variables from '../../styles/variables'; import {propTypes as selectionListPropTypes} from './selectionListPropTypes'; -import RadioListItem from './RadioListItem'; -import UserListItem from './UserListItem'; import useKeyboardShortcut from '../../hooks/useKeyboardShortcut'; import SafeAreaConsumer from '../SafeAreaConsumer'; import withKeyboardState, {keyboardStatePropTypes} from '../withKeyboardState'; @@ -24,6 +21,9 @@ import useLocalize from '../../hooks/useLocalize'; import Log from '../../libs/Log'; import OptionsListSkeletonView from '../OptionsListSkeletonView'; import useActiveElement from '../../hooks/useActiveElement'; +import BaseListItem from './BaseListItem'; +import themeColors from '../../styles/themes/default'; +import ArrowKeyFocusManager from '../ArrowKeyFocusManager'; const propTypes = { ...keyboardStatePropTypes, @@ -48,10 +48,13 @@ function BaseSelectionList({ headerMessage = '', confirmButtonText = '', onConfirm, + footerContent, showScrollIndicator = false, showLoadingPlaceholder = false, showConfirmButton = false, isKeyboardShown = false, + disableKeyboardShortcuts = false, + children, }) { const {translate} = useLocalize(); const firstLayoutRef = useRef(true); @@ -136,19 +139,19 @@ function BaseSelectionList({ }; }, [canSelectMultiple, sections]); - // Disable `Enter` hotkey if the active element is a button or checkbox - const shouldDisableHotkeys = activeElement && [CONST.ACCESSIBILITY_ROLE.BUTTON, CONST.ACCESSIBILITY_ROLE.CHECKBOX].includes(activeElement.role); - // If `initiallyFocusedOptionKey` is not passed, we fall back to `-1`, to avoid showing the highlight on the first member const [focusedIndex, setFocusedIndex] = useState(() => _.findIndex(flattenedSections.allOptions, (option) => option.keyForList === initiallyFocusedOptionKey)); + // Disable `Enter` shortcut if the active element is a button or checkbox + const disableEnterShortcut = activeElement && [CONST.ACCESSIBILITY_ROLE.BUTTON, CONST.ACCESSIBILITY_ROLE.CHECKBOX].includes(activeElement.role); + /** * Scrolls to the desired item index in the section list * * @param {Number} index - the index of the item to scroll to * @param {Boolean} animated - whether to animate the scroll */ - const scrollToIndex = (index, animated) => { + const scrollToIndex = useCallback((index, animated = true) => { const item = flattenedSections.allOptions[index]; if (!listRef.current || !item) { @@ -169,7 +172,10 @@ function BaseSelectionList({ } listRef.current.scrollToLocation({sectionIndex: adjustedSectionIndex, itemIndex, animated, viewOffset: variables.contentHeaderHeight}); - }; + + // If we don't disable dependencies here, we would need to make sure that the `sections` prop is stable in every usage of this component. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); /** * Logic to run when a row is selected, either with click/press or keyboard hotkeys. @@ -234,6 +240,14 @@ function BaseSelectionList({ const getItemLayout = (data, flatDataArrayIndex) => { const targetItem = flattenedSections.itemLayouts[flatDataArrayIndex]; + if (!targetItem) { + return { + length: 0, + offset: 0, + index: flatDataArrayIndex, + }; + } + return { length: targetItem.length, offset: targetItem.offset, @@ -259,33 +273,40 @@ function BaseSelectionList({ const renderItem = ({item, index, section}) => { const normalizedIndex = index + lodashGet(section, 'indexOffset', 0); - const isDisabled = section.isDisabled; + const isDisabled = section.isDisabled || item.isDisabled; const isItemFocused = !isDisabled && focusedIndex === normalizedIndex; // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. const showTooltip = normalizedIndex < 10; - if (canSelectMultiple) { - return ( - selectRow(item, true)} - onDismissError={onDismissError} - showTooltip={showTooltip} - /> - ); - } - return ( - selectRow(item, true)} + onDismissError={onDismissError} /> ); }; + const scrollToFocusedIndexOnFirstRender = useCallback(() => { + if (!firstLayoutRef.current) { + return; + } + scrollToIndex(focusedIndex, false); + firstLayoutRef.current = false; + }, [focusedIndex, scrollToIndex]); + + const updateAndScrollToFocusedIndex = useCallback( + (newFocusedIndex) => { + setFocusedIndex(newFocusedIndex); + scrollToIndex(newFocusedIndex, true); + }, + [scrollToIndex], + ); + /** Focuses the text input when the component comes into focus and after any navigation animations finish. */ useFocusEffect( useCallback(() => { @@ -305,14 +326,14 @@ function BaseSelectionList({ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, { captureOnInputs: true, shouldBubble: () => !flattenedSections.allOptions[focusedIndex], - isActive: !shouldDisableHotkeys && isFocused, + isActive: !disableKeyboardShortcuts && !disableEnterShortcut && isFocused, }); /** Calls confirm action when pressing CTRL (CMD) + Enter */ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, onConfirm, { captureOnInputs: true, shouldBubble: () => !flattenedSections.allOptions[focusedIndex], - isActive: Boolean(onConfirm) && isFocused, + isActive: !disableKeyboardShortcuts && Boolean(onConfirm) && isFocused, }); return ( @@ -320,10 +341,7 @@ function BaseSelectionList({ disabledIndexes={flattenedSections.disabledOptionsIndexes} focusedIndex={focusedIndex} maxIndex={flattenedSections.allOptions.length - 1} - onFocusedIndexChanged={(newFocusedIndex) => { - setFocusedIndex(newFocusedIndex); - scrollToIndex(newFocusedIndex, true); - }} + onFocusedIndexChanged={updateAndScrollToFocusedIndex} > {({safeAreaPaddingBottomStyle}) => ( @@ -360,7 +378,7 @@ function BaseSelectionList({ style={[styles.peopleRow, styles.userSelectNone, styles.ph5, styles.pb3]} onPress={onSelectAll} accessibilityLabel={translate('workspace.people.selectAll')} - accessibilityRole="button" + accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityState={{checked: flattenedSections.allSelected}} disabled={flattenedSections.allOptions.length === flattenedSections.disabledOptionsIndexes.length} dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} @@ -387,7 +405,7 @@ function BaseSelectionList({ onScrollBeginDrag={onScrollBeginDrag} keyExtractor={(item) => item.keyForList} extraData={focusedIndex} - indicatorStyle="white" + indicatorStyle={themeColors.selectionListIndicatorColor} keyboardShouldPersistTaps="always" showsVerticalScrollIndicator={showScrollIndicator} initialNumToRender={12} @@ -395,18 +413,14 @@ function BaseSelectionList({ windowSize={5} viewabilityConfig={{viewAreaCoveragePercentThreshold: 95}} testID="selection-list" - onLayout={() => { - if (!firstLayoutRef.current) { - return; - } - scrollToIndex(focusedIndex, false); - firstLayoutRef.current = false; - }} + style={[styles.flexGrow0]} + onLayout={scrollToFocusedIndexOnFirstRender} /> + {children} )} {showConfirmButton && ( - +