diff --git a/src/components/SectionList/index.android.tsx b/src/components/SectionList/index.android.tsx index 1aa9b501146c..c119e8e9bcf3 100644 --- a/src/components/SectionList/index.android.tsx +++ b/src/components/SectionList/index.android.tsx @@ -1,19 +1,22 @@ import React, {forwardRef} from 'react'; +import type {ForwardedRef} from 'react'; import {SectionList as RNSectionList} from 'react-native'; -import type ForwardedSectionList from './types'; +import type {SectionListProps} from 'react-native'; // eslint-disable-next-line react/function-component-definition -const SectionListWithRef: ForwardedSectionList = (props, ref) => ( - -); +function SectionListWithRef(props: SectionListProps, ref: ForwardedRef>) { + return ( + + ); +} SectionListWithRef.displayName = 'SectionListWithRef'; diff --git a/src/components/SectionList/index.tsx b/src/components/SectionList/index.tsx index 4af7ad33705c..1129b2bdbb8f 100644 --- a/src/components/SectionList/index.tsx +++ b/src/components/SectionList/index.tsx @@ -1,16 +1,17 @@ import React, {forwardRef} from 'react'; +import type {ForwardedRef} from 'react'; import {SectionList as RNSectionList} from 'react-native'; -import type ForwardedSectionList from './types'; +import type {SectionListProps} from 'react-native'; // eslint-disable-next-line react/function-component-definition -const SectionList: ForwardedSectionList = (props, ref) => ( - -); - -SectionList.displayName = 'SectionList'; +function SectionList(props: SectionListProps, ref: ForwardedRef>) { + return ( + + ); +} export default forwardRef(SectionList); diff --git a/src/components/SectionList/types.ts b/src/components/SectionList/types.ts deleted file mode 100644 index 4648172aabfd..000000000000 --- a/src/components/SectionList/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type {ForwardedRef} from 'react'; -import type {SectionList, SectionListProps} from 'react-native'; - -type ForwardedSectionList = { - (props: SectionListProps, ref: ForwardedRef): React.ReactNode; - displayName: string; -}; - -export default ForwardedSectionList; diff --git a/src/components/SelectionList/BaseListItem.js b/src/components/SelectionList/BaseListItem.tsx similarity index 73% rename from src/components/SelectionList/BaseListItem.js rename to src/components/SelectionList/BaseListItem.tsx index 6a067ea0fe3d..59a1c4dd08ce 100644 --- a/src/components/SelectionList/BaseListItem.js +++ b/src/components/SelectionList/BaseListItem.tsx @@ -1,4 +1,3 @@ -import lodashGet from 'lodash/get'; import React from 'react'; import {View} from 'react-native'; import Icon from '@components/Icon'; @@ -12,10 +11,10 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import RadioListItem from './RadioListItem'; -import {baseListItemPropTypes} from './selectionListPropTypes'; +import type {BaseListItemProps, RadioItem, User} from './types'; import UserListItem from './UserListItem'; -function BaseListItem({ +function BaseListItem({ item, isFocused = false, isDisabled = false, @@ -26,13 +25,12 @@ function BaseListItem({ onDismissError = () => {}, rightHandSideComponent, keyForList, -}) { +}: BaseListItemProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - const isUserItem = lodashGet(item, 'icons.length', 0) > 0; - const ListItem = isUserItem ? UserListItem : RadioListItem; + const isRadioItem = item.rightElement === undefined; const rightHandSideComponentRender = () => { if (canSelectMultiple || !rightHandSideComponent) { @@ -70,7 +68,7 @@ function BaseListItem({ styles.justifyContentBetween, styles.sidebarLinkInner, styles.userSelectNone, - isUserItem ? styles.peopleRow : styles.optionRow, + isRadioItem ? styles.optionRow : styles.peopleRow, isFocused && styles.sidebarLinkActive, ]} > @@ -100,20 +98,32 @@ function BaseListItem({ )} - + + {isRadioItem ? ( + onSelectRow(item)} + showTooltip={showTooltip} + /> + ) : ( + onSelectRow(item)} + showTooltip={showTooltip} + /> + )} {!canSelectMultiple && item.isSelected && !rightHandSideComponent && ( - {Boolean(item.invitedSecondaryLogin) && ( + {!!item.invitedSecondaryLogin && ( {translate('workspace.people.invitedBySecondaryLogin', {secondaryLogin: item.invitedSecondaryLogin})} @@ -140,6 +150,5 @@ function BaseListItem({ } BaseListItem.displayName = 'BaseListItem'; -BaseListItem.propTypes = baseListItemPropTypes; export default BaseListItem; diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.tsx similarity index 76% rename from src/components/SelectionList/BaseSelectionList.js rename to src/components/SelectionList/BaseSelectionList.tsx index 960618808fd9..cc55b8e4fc17 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -1,8 +1,8 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import type {ForwardedRef} from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; +import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, SectionListRenderItemInfo} from 'react-native'; import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager'; import Button from '@components/Button'; import Checkbox from '@components/Checkbox'; @@ -13,69 +13,60 @@ import SafeAreaConsumer from '@components/SafeAreaConsumer'; import SectionList from '@components/SectionList'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withKeyboardState, {keyboardStatePropTypes} from '@components/withKeyboardState'; import useActiveElementRole from '@hooks/useActiveElementRole'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Log from '@libs/Log'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import BaseListItem from './BaseListItem'; -import {propTypes as selectionListPropTypes} from './selectionListPropTypes'; - -const propTypes = { - ...keyboardStatePropTypes, - ...selectionListPropTypes, -}; - -function BaseSelectionList({ - sections, - canSelectMultiple = false, - onSelectRow, - onSelectAll, - onDismissError, - textInputLabel = '', - textInputPlaceholder = '', - textInputValue = '', - textInputHint = '', - textInputMaxLength, - inputMode = CONST.INPUT_MODE.TEXT, - onChangeText, - initiallyFocusedOptionKey = '', - onScroll, - onScrollBeginDrag, - headerMessage = '', - confirmButtonText = '', - onConfirm, - headerContent, - footerContent, - showScrollIndicator = false, - showLoadingPlaceholder = false, - showConfirmButton = false, - shouldPreventDefaultFocusOnSelectRow = false, - isKeyboardShown = false, - containerStyle = [], - disableInitialFocusOptionStyle = false, - inputRef = null, - disableKeyboardShortcuts = false, - children, - shouldStopPropagation = false, - shouldShowTooltips = true, - shouldUseDynamicMaxToRenderPerBatch = false, - rightHandSideComponent, -}) { - const theme = useTheme(); +import type {BaseSelectionListProps, ButtonOrCheckBoxRoles, FlattenedSectionsReturn, RadioItem, Section, SectionListDataType, User} from './types'; + +function BaseSelectionList( + { + sections, + canSelectMultiple = false, + onSelectRow, + onSelectAll, + onDismissError, + textInputLabel = '', + textInputPlaceholder = '', + textInputValue = '', + textInputHint, + textInputMaxLength, + inputMode = CONST.INPUT_MODE.TEXT, + onChangeText, + initiallyFocusedOptionKey = '', + onScroll, + onScrollBeginDrag, + headerMessage = '', + confirmButtonText = '', + onConfirm = () => {}, + headerContent, + footerContent, + showScrollIndicator = false, + showLoadingPlaceholder = false, + showConfirmButton = false, + shouldPreventDefaultFocusOnSelectRow = false, + containerStyle, + isKeyboardShown = false, + disableKeyboardShortcuts = false, + children, + shouldStopPropagation = false, + shouldShowTooltips = true, + shouldUseDynamicMaxToRenderPerBatch = false, + rightHandSideComponent, + }: BaseSelectionListProps, + inputRef: ForwardedRef, +) { const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - const listRef = useRef(null); - const textInputRef = useRef(null); - const focusTimeoutRef = useRef(null); - const shouldShowTextInput = Boolean(textInputLabel); - const shouldShowSelectAll = Boolean(onSelectAll); + const listRef = useRef>>(null); + const textInputRef = useRef(null); + const focusTimeoutRef = useRef(null); + const shouldShowTextInput = !!textInputLabel; + const shouldShowSelectAll = !!onSelectAll; const activeElementRole = useActiveElementRole(); const isFocused = useIsFocused(); const [maxToRenderPerBatch, setMaxToRenderPerBatch] = useState(shouldUseDynamicMaxToRenderPerBatch ? 0 : CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT); @@ -87,26 +78,24 @@ function BaseSelectionList({ * - `disabledOptionsIndexes`: Contains the indexes of all the disabled items in the list, to be used by the ArrowKeyFocusManager * - `itemLayouts`: Contains the layout information for each item, header and footer in the list, * so we can calculate the position of any given item when scrolling programmatically - * - * @return {{itemLayouts: [{offset: number, length: number}], disabledOptionsIndexes: *[], allOptions: *[]}} */ - const flattenedSections = useMemo(() => { - const allOptions = []; + const flattenedSections = useMemo>(() => { + const allOptions: TItem[] = []; - const disabledOptionsIndexes = []; + const disabledOptionsIndexes: number[] = []; let disabledIndex = 0; let offset = 0; const itemLayouts = [{length: 0, offset}]; - const selectedOptions = []; + const selectedOptions: TItem[] = []; - _.each(sections, (section, sectionIndex) => { + sections.forEach((section, sectionIndex) => { const sectionHeaderHeight = variables.optionsListSectionHeaderHeight; itemLayouts.push({length: sectionHeaderHeight, offset}); offset += sectionHeaderHeight; - _.each(section.data, (item, optionIndex) => { + section.data.forEach((item, optionIndex) => { // Add item to the general flattened array allOptions.push({ ...item, @@ -115,7 +104,7 @@ function BaseSelectionList({ }); // If disabled, add to the disabled indexes array - if (section.isDisabled || item.isDisabled) { + if (!!section.isDisabled || item.isDisabled) { disabledOptionsIndexes.push(disabledIndex); } disabledIndex += 1; @@ -155,19 +144,19 @@ function BaseSelectionList({ }, [canSelectMultiple, sections]); // 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)); + const [focusedIndex, setFocusedIndex] = useState(() => flattenedSections.allOptions.findIndex((option) => option.keyForList === initiallyFocusedOptionKey)); // Disable `Enter` shortcut if the active element is a button or checkbox - const disableEnterShortcut = activeElementRole && [CONST.ROLE.BUTTON, CONST.ROLE.CHECKBOX].includes(activeElementRole); + const disableEnterShortcut = activeElementRole && [CONST.ROLE.BUTTON, CONST.ROLE.CHECKBOX].includes(activeElementRole as ButtonOrCheckBoxRoles); /** * 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 + * @param index - the index of the item to scroll to + * @param animated - whether to animate the scroll */ const scrollToIndex = useCallback( - (index, animated = true) => { + (index: number, animated = true) => { const item = flattenedSections.allOptions[index]; if (!listRef.current || !item) { @@ -182,7 +171,7 @@ function BaseSelectionList({ // 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(sections, `[${i}].data`))) { + if (sections[i].data) { adjustedSectionIndex--; } } @@ -197,10 +186,10 @@ function BaseSelectionList({ /** * Logic to run when a row is selected, either with click/press or keyboard hotkeys. * - * @param {Object} item - the list item - * @param {Boolean} shouldUnfocusRow - flag to decide if we should unfocus all rows. True when selecting a row with click or press (not keyboard) + * @param item - the list item + * @param shouldUnfocusRow - flag to decide if we should unfocus all rows. True when selecting a row with click or press (not keyboard) */ - const selectRow = (item, shouldUnfocusRow = false) => { + const selectRow = (item: TItem, shouldUnfocusRow = false) => { // In single-selection lists we don't care about updating the focused index, because the list is closed after selecting an item if (canSelectMultiple) { if (sections.length > 1) { @@ -233,15 +222,15 @@ function BaseSelectionList({ }; const selectAllRow = () => { - onSelectAll(); + onSelectAll?.(); + if (shouldShowTextInput && shouldPreventDefaultFocusOnSelectRow && textInputRef.current) { textInputRef.current.focus(); } }; - const selectFocusedOption = (e) => { - const focusedItemKey = lodashGet(e, ['target', 'attributes', 'id', 'value']); - const focusedOption = focusedItemKey ? _.find(flattenedSections.allOptions, (option) => option.keyForList === focusedItemKey) : flattenedSections.allOptions[focusedIndex]; + const selectFocusedOption = () => { + const focusedOption = flattenedSections.allOptions[focusedIndex]; if (!focusedOption || focusedOption.isDisabled) { return; @@ -254,8 +243,8 @@ function BaseSelectionList({ * This function is used to compute the layout of any given item in our list. * We need to implement it so that we can programmatically scroll to items outside the virtual render window of the SectionList. * - * @param {Array} data - This is the same as the data we pass into the component - * @param {Number} flatDataArrayIndex - This index is provided by React Native, and refers to a flat array with data from all the sections. This flat array has some quirks: + * @param data - This is the same as the data we pass into the component + * @param flatDataArrayIndex - This index is provided by React Native, and refers to a flat array with data from all the sections. This flat array has some quirks: * * 1. It ALWAYS includes a list header and a list footer, even if we don't provide/render those. * 2. Each section includes a header, even if we don't provide/render one. @@ -263,10 +252,8 @@ function BaseSelectionList({ * For example, given a list with two sections, two items in each section, no header, no footer, and no section headers, the flat array might look something like this: * * [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}] - * - * @returns {Object} */ - const getItemLayout = (data, flatDataArrayIndex) => { + const getItemLayout = (data: Array> | null, flatDataArrayIndex: number) => { const targetItem = flattenedSections.itemLayouts[flatDataArrayIndex]; if (!targetItem) { @@ -284,8 +271,8 @@ function BaseSelectionList({ }; }; - const renderSectionHeader = ({section}) => { - if (!section.title || _.isEmpty(section.data)) { + const renderSectionHeader = ({section}: {section: SectionListDataType}) => { + if (!section.title || !section.data) { return null; } @@ -300,9 +287,10 @@ function BaseSelectionList({ ); }; - const renderItem = ({item, index, section}) => { - const normalizedIndex = index + lodashGet(section, 'indexOffset', 0); - const isDisabled = section.isDisabled || item.isDisabled; + const renderItem = ({item, index, section}: SectionListRenderItemInfo>) => { + const indexOffset = section.indexOffset ? section.indexOffset : 0; + const normalizedIndex = index + indexOffset; + 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 = shouldShowTooltips && normalizedIndex < 10; @@ -312,11 +300,9 @@ function BaseSelectionList({ item={item} isFocused={isItemFocused} isDisabled={isDisabled} - isHide={!maxToRenderPerBatch} showTooltip={showTooltip} canSelectMultiple={canSelectMultiple} onSelectRow={() => selectRow(item, true)} - disableIsFocusStyle={disableInitialFocusOptionStyle} onDismissError={onDismissError} shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} rightHandSideComponent={rightHandSideComponent} @@ -326,11 +312,10 @@ function BaseSelectionList({ }; const scrollToFocusedIndexOnFirstRender = useCallback( - ({nativeEvent}) => { + (nativeEvent: LayoutChangeEvent) => { if (shouldUseDynamicMaxToRenderPerBatch) { - const listHeight = lodashGet(nativeEvent, 'layout.height', 0); - const itemHeight = lodashGet(nativeEvent, 'layout.y', 0); - + const listHeight = nativeEvent.nativeEvent.layout.height; + const itemHeight = nativeEvent.nativeEvent.layout.y; setMaxToRenderPerBatch((Math.ceil(listHeight / itemHeight) || 0) + CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT); } @@ -344,7 +329,7 @@ function BaseSelectionList({ ); const updateAndScrollToFocusedIndex = useCallback( - (newFocusedIndex) => { + (newFocusedIndex: number) => { setFocusedIndex(newFocusedIndex); scrollToIndex(newFocusedIndex, true); }, @@ -355,7 +340,12 @@ function BaseSelectionList({ useFocusEffect( useCallback(() => { if (shouldShowTextInput) { - focusTimeoutRef.current = setTimeout(() => textInputRef.current.focus(), CONST.ANIMATED_TRANSITION); + focusTimeoutRef.current = setTimeout(() => { + if (!textInputRef.current) { + return; + } + textInputRef.current.focus(); + }, CONST.ANIMATED_TRANSITION); } return () => { if (!focusTimeoutRef.current) { @@ -382,7 +372,7 @@ function BaseSelectionList({ /** Selects row when pressing Enter */ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, { captureOnInputs: true, - shouldBubble: () => !flattenedSections.allOptions[focusedIndex], + shouldBubble: !flattenedSections.allOptions[focusedIndex], shouldStopPropagation, isActive: !disableKeyboardShortcuts && !disableEnterShortcut && isFocused, }); @@ -390,8 +380,8 @@ function BaseSelectionList({ /** Calls confirm action when pressing CTRL (CMD) + Enter */ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, onConfirm, { captureOnInputs: true, - shouldBubble: () => !flattenedSections.allOptions[focusedIndex], - isActive: !disableKeyboardShortcuts && Boolean(onConfirm) && isFocused, + shouldBubble: !flattenedSections.allOptions[focusedIndex], + isActive: !disableKeyboardShortcuts && !!onConfirm && isFocused, }); return ( @@ -401,19 +391,22 @@ function BaseSelectionList({ maxIndex={flattenedSections.allOptions.length - 1} onFocusedIndexChanged={updateAndScrollToFocusedIndex} > - {/* */} {({safeAreaPaddingBottomStyle}) => ( - + {shouldShowTextInput && ( { - if (inputRef) { - // eslint-disable-next-line no-param-reassign - inputRef.current = el; + ref={(element) => { + textInputRef.current = element as RNTextInput; + + if (!inputRef) { + return; + } + + if (typeof inputRef === 'function') { + inputRef(element as RNTextInput); } - textInputRef.current = el; }} label={textInputLabel} accessibilityLabel={textInputLabel} @@ -427,16 +420,16 @@ function BaseSelectionList({ selectTextOnFocus spellCheck={false} onSubmitEditing={selectFocusedOption} - blurOnSubmit={Boolean(flattenedSections.allOptions.length)} + blurOnSubmit={!!flattenedSections.allOptions.length} /> )} - {Boolean(headerMessage) && ( + {!!headerMessage && ( {headerMessage} )} - {Boolean(headerContent) && headerContent} + {!!headerContent && headerContent} {flattenedSections.allOptions.length === 0 && showLoadingPlaceholder ? ( ) : ( @@ -472,9 +465,9 @@ function BaseSelectionList({ getItemLayout={getItemLayout} onScroll={onScroll} onScrollBeginDrag={onScrollBeginDrag} - keyExtractor={(item) => item.keyForList} + keyExtractor={(item: TItem) => item.keyForList} extraData={focusedIndex} - indicatorStyle={theme.white} + indicatorStyle="white" keyboardShouldPersistTaps="always" showsVerticalScrollIndicator={showScrollIndicator} initialNumToRender={12} @@ -500,7 +493,7 @@ function BaseSelectionList({ /> )} - {Boolean(footerContent) && {footerContent}} + {!!footerContent && {footerContent}} )} @@ -509,6 +502,5 @@ function BaseSelectionList({ } BaseSelectionList.displayName = 'BaseSelectionList'; -BaseSelectionList.propTypes = propTypes; -export default withKeyboardState(BaseSelectionList); +export default forwardRef(BaseSelectionList); diff --git a/src/components/SelectionList/RadioListItem.js b/src/components/SelectionList/RadioListItem.tsx similarity index 87% rename from src/components/SelectionList/RadioListItem.js rename to src/components/SelectionList/RadioListItem.tsx index 2de0c96932ea..769eaa80df4b 100644 --- a/src/components/SelectionList/RadioListItem.js +++ b/src/components/SelectionList/RadioListItem.tsx @@ -3,10 +3,11 @@ import {View} from 'react-native'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import useThemeStyles from '@hooks/useThemeStyles'; -import {radioListItemPropTypes} from './selectionListPropTypes'; +import type {RadioListItemProps} from './types'; -function RadioListItem({item, showTooltip, textStyles, alternateTextStyles}) { +function RadioListItem({item, showTooltip, textStyles, alternateTextStyles}: RadioListItemProps) { const styles = useThemeStyles(); + return ( - {Boolean(item.alternateText) && ( + {!!item.alternateText && ( - {Boolean(item.icons) && ( + {!!item.icons && ( )} @@ -26,19 +23,19 @@ function UserListItem({item, textStyles, alternateTextStyles, showTooltip, style text={item.text} > {item.text} - {Boolean(item.alternateText) && ( + {!!item.alternateText && ( {item.alternateText} @@ -46,12 +43,11 @@ function UserListItem({item, textStyles, alternateTextStyles, showTooltip, style )} - {Boolean(item.rightElement) && item.rightElement} + {!!item.rightElement && item.rightElement} ); } UserListItem.displayName = 'UserListItem'; -UserListItem.propTypes = userListItemPropTypes; export default UserListItem; diff --git a/src/components/SelectionList/index.android.js b/src/components/SelectionList/index.android.js deleted file mode 100644 index 53d5b6bbce06..000000000000 --- a/src/components/SelectionList/index.android.js +++ /dev/null @@ -1,17 +0,0 @@ -import React, {forwardRef} from 'react'; -import {Keyboard} from 'react-native'; -import BaseSelectionList from './BaseSelectionList'; - -const SelectionList = forwardRef((props, ref) => ( - Keyboard.dismiss()} - /> -)); - -SelectionList.displayName = 'SelectionList'; - -export default SelectionList; diff --git a/src/components/SelectionList/index.android.tsx b/src/components/SelectionList/index.android.tsx new file mode 100644 index 000000000000..8487c6e2cc67 --- /dev/null +++ b/src/components/SelectionList/index.android.tsx @@ -0,0 +1,22 @@ +import React, {forwardRef} from 'react'; +import type {ForwardedRef} from 'react'; +import {Keyboard} from 'react-native'; +import type {TextInput} from 'react-native'; +import BaseSelectionList from './BaseSelectionList'; +import type {BaseSelectionListProps, RadioItem, User} from './types'; + +function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) { + return ( + Keyboard.dismiss()} + /> + ); +} + +SelectionList.displayName = 'SelectionList'; + +export default forwardRef(SelectionList); diff --git a/src/components/SelectionList/index.ios.js b/src/components/SelectionList/index.ios.js deleted file mode 100644 index 7f2a282aeb89..000000000000 --- a/src/components/SelectionList/index.ios.js +++ /dev/null @@ -1,16 +0,0 @@ -import React, {forwardRef} from 'react'; -import {Keyboard} from 'react-native'; -import BaseSelectionList from './BaseSelectionList'; - -const SelectionList = forwardRef((props, ref) => ( - Keyboard.dismiss()} - /> -)); - -SelectionList.displayName = 'SelectionList'; - -export default SelectionList; diff --git a/src/components/SelectionList/index.ios.tsx b/src/components/SelectionList/index.ios.tsx new file mode 100644 index 000000000000..9c32d38314e2 --- /dev/null +++ b/src/components/SelectionList/index.ios.tsx @@ -0,0 +1,21 @@ +import React, {forwardRef} from 'react'; +import type {ForwardedRef} from 'react'; +import {Keyboard} from 'react-native'; +import type {TextInput} from 'react-native'; +import BaseSelectionList from './BaseSelectionList'; +import type {BaseSelectionListProps, RadioItem, User} from './types'; + +function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) { + return ( + + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + ref={ref} + onScrollBeginDrag={() => Keyboard.dismiss()} + /> + ); +} + +SelectionList.displayName = 'SelectionList'; + +export default forwardRef(SelectionList); diff --git a/src/components/SelectionList/index.js b/src/components/SelectionList/index.tsx similarity index 82% rename from src/components/SelectionList/index.js rename to src/components/SelectionList/index.tsx index 24ea60d29be5..93754926cacb 100644 --- a/src/components/SelectionList/index.js +++ b/src/components/SelectionList/index.tsx @@ -1,9 +1,12 @@ import React, {forwardRef, useEffect, useState} from 'react'; +import type {ForwardedRef} from 'react'; import {Keyboard} from 'react-native'; +import type {TextInput} from 'react-native'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import BaseSelectionList from './BaseSelectionList'; +import type {BaseSelectionListProps, RadioItem, User} from './types'; -const SelectionList = forwardRef((props, ref) => { +function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) { const [isScreenTouched, setIsScreenTouched] = useState(false); const touchStart = () => setIsScreenTouched(true); @@ -39,8 +42,8 @@ const SelectionList = forwardRef((props, ref) => { }} /> ); -}); +} SelectionList.displayName = 'SelectionList'; -export default SelectionList; +export default forwardRef(SelectionList); diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts new file mode 100644 index 000000000000..5c28a139903d --- /dev/null +++ b/src/components/SelectionList/types.ts @@ -0,0 +1,277 @@ +import type {ReactElement, ReactNode} from 'react'; +import type {GestureResponderEvent, InputModeOptions, SectionListData, StyleProp, TextStyle, ViewStyle} from 'react-native'; +import type {SubAvatar} from '@components/SubscriptAvatar'; +import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; + +type CommonListItemProps = { + /** Whether this item is focused (for arrow key controls) */ + isFocused?: boolean; + + /** Style to be applied to Text */ + textStyles?: StyleProp; + + /** Style to be applied on the alternate text */ + alternateTextStyles?: StyleProp; + + /** Whether this item is disabled */ + isDisabled?: boolean; + + /** Whether this item should show Tooltip */ + showTooltip: boolean; + + /** Whether to use the Checkbox (multiple selection) instead of the Checkmark (single selection) */ + canSelectMultiple?: boolean; + + /** Callback to fire when the item is pressed */ + onSelectRow: (item: TItem) => void; + + /** Callback to fire when an error is dismissed */ + onDismissError?: (item: TItem) => void; + + /** Component to display on the right side */ + rightHandSideComponent?: ((item: TItem) => ReactElement) | ReactElement | null; +}; + +type User = { + /** Text to display */ + text: string; + + /** Alternate text to display */ + alternateText?: string; + + /** Key used internally by React */ + keyForList: string; + + /** Whether this option is selected */ + isSelected?: boolean; + + /** Whether this option is disabled for selection */ + isDisabled?: boolean; + + /** User accountID */ + accountID?: number; + + /** User login */ + login?: string; + + /** Element to show on the right side of the item */ + rightElement: ReactElement; + + /** Icons for the user (can be multiple if it's a Workspace) */ + icons?: SubAvatar[]; + + /** Errors that this user may contain */ + errors?: Errors; + + /** The type of action that's pending */ + pendingAction?: PendingAction; + + invitedSecondaryLogin?: string; + + /** Represents the index of the section it came from */ + sectionIndex: number; + + /** Represents the index of the option within the section it came from */ + index: number; +}; + +type UserListItemProps = CommonListItemProps & { + /** The section list item */ + item: User; + + /** Additional styles to apply to text */ + style?: StyleProp; +}; + +type RadioItem = { + /** Text to display */ + text: string; + + /** Alternate text to display */ + alternateText?: string; + + /** Key used internally by React */ + keyForList: string; + + /** Whether this option is selected */ + isSelected?: boolean; + + /** Element to show on the right side of the item */ + rightElement?: undefined; + + /** Whether this option is disabled for selection */ + isDisabled?: undefined; + + invitedSecondaryLogin?: undefined; + + /** Errors that this user may contain */ + errors?: undefined; + + /** The type of action that's pending */ + pendingAction?: undefined; + + /** Represents the index of the section it came from */ + sectionIndex: number; + + /** Represents the index of the option within the section it came from */ + index: number; +}; + +type RadioListItemProps = CommonListItemProps & { + /** The section list item */ + item: RadioItem; +}; + +type BaseListItemProps = CommonListItemProps & { + item: TItem; + shouldPreventDefaultFocusOnSelectRow?: boolean; + keyForList?: string; +}; + +type Section = { + /** Title of the section */ + title?: string; + + /** The initial index of this section given the total number of options in each section's data array */ + indexOffset?: number; + + /** Array of options */ + data: TItem[]; + + /** Whether this section items disabled for selection */ + isDisabled?: boolean; +}; + +type BaseSelectionListProps = Partial & { + /** Sections for the section list */ + sections: Array>; + + /** Whether this is a multi-select list */ + canSelectMultiple?: boolean; + + /** Callback to fire when a row is pressed */ + onSelectRow: (item: TItem) => void; + + /** Callback to fire when "Select All" checkbox is pressed. Only use along with `canSelectMultiple` */ + onSelectAll?: () => void; + + /** Callback to fire when an error is dismissed */ + onDismissError?: () => void; + + /** Label for the text input */ + textInputLabel?: string; + + /** Placeholder for the text input */ + textInputPlaceholder?: string; + + /** Hint for the text input */ + textInputHint?: string; + + /** Value for the text input */ + textInputValue?: string; + + /** Max length for the text input */ + textInputMaxLength?: number; + + /** Callback to fire when the text input changes */ + onChangeText?: (text: string) => void; + + /** Input mode for the text input */ + inputMode?: InputModeOptions; + + /** Item `keyForList` to focus initially */ + initiallyFocusedOptionKey?: string; + + /** Callback to fire when the list is scrolled */ + onScroll?: () => void; + + /** Callback to fire when the list is scrolled and the user begins dragging */ + onScrollBeginDrag?: () => void; + + /** Message to display at the top of the list */ + headerMessage?: string; + + /** Text to display on the confirm button */ + confirmButtonText?: string; + + /** Callback to fire when the confirm button is pressed */ + onConfirm?: (e?: GestureResponderEvent | KeyboardEvent | undefined) => void; + + /** Whether to show the vertical scroll indicator */ + showScrollIndicator?: boolean; + + /** Whether to show the loading placeholder */ + showLoadingPlaceholder?: boolean; + + /** Whether to show the default confirm button */ + showConfirmButton?: boolean; + + /** Whether tooltips should be shown */ + shouldShowTooltips?: boolean; + + /** Whether to stop automatic form submission on pressing enter key or not */ + shouldStopPropagation?: boolean; + + /** Whether to prevent default focusing of options and focus the textinput when selecting an option */ + shouldPreventDefaultFocusOnSelectRow?: boolean; + + /** Custom content to display in the header */ + headerContent?: ReactNode; + + /** Custom content to display in the footer */ + footerContent?: ReactNode; + + /** Whether to use dynamic maxToRenderPerBatch depending on the visible number of elements */ + shouldUseDynamicMaxToRenderPerBatch?: boolean; + + /** Whether keyboard shortcuts should be disabled */ + disableKeyboardShortcuts?: boolean; + + /** Whether to disable initial styling for focused option */ + disableInitialFocusOptionStyle?: boolean; + + /** Styles to apply to SelectionList container */ + containerStyle?: ViewStyle; + + /** Whether keyboard is visible on the screen */ + isKeyboardShown?: boolean; + + /** Whether focus event should be delayed */ + shouldDelayFocus?: boolean; + + /** Component to display on the right side of each child */ + rightHandSideComponent?: ((item: TItem) => ReactElement) | ReactElement | null; +}; + +type ItemLayout = { + length: number; + offset: number; +}; + +type FlattenedSectionsReturn = { + allOptions: TItem[]; + selectedOptions: TItem[]; + disabledOptionsIndexes: number[]; + itemLayouts: ItemLayout[]; + allSelected: boolean; +}; + +type ButtonOrCheckBoxRoles = 'button' | 'checkbox'; + +type SectionListDataType = SectionListData>; + +export type { + BaseSelectionListProps, + CommonListItemProps, + UserListItemProps, + Section, + RadioListItemProps, + BaseListItemProps, + User, + RadioItem, + FlattenedSectionsReturn, + ItemLayout, + ButtonOrCheckBoxRoles, + SectionListDataType, +}; diff --git a/src/components/SubscriptAvatar.tsx b/src/components/SubscriptAvatar.tsx index 00cf248ad838..2e2ae6d06e0f 100644 --- a/src/components/SubscriptAvatar.tsx +++ b/src/components/SubscriptAvatar.tsx @@ -104,3 +104,4 @@ function SubscriptAvatar({mainAvatar = {}, secondaryAvatar = {}, size = CONST.AV SubscriptAvatar.displayName = 'SubscriptAvatar'; export default memo(SubscriptAvatar); +export type {SubAvatar}; diff --git a/src/hooks/useKeyboardShortcut.ts b/src/hooks/useKeyboardShortcut.ts index 6bf8b2c52bc3..1c5bbc426ef2 100644 --- a/src/hooks/useKeyboardShortcut.ts +++ b/src/hooks/useKeyboardShortcut.ts @@ -26,7 +26,7 @@ type KeyboardShortcutConfig = { * Register a keyboard shortcut handler. * Recommendation: To ensure stability, wrap the `callback` function with the useCallback hook before using it with this hook. */ -export default function useKeyboardShortcut(shortcut: Shortcut, callback: (e?: GestureResponderEvent | KeyboardEvent) => void, config: KeyboardShortcutConfig | Record = {}) { +export default function useKeyboardShortcut(shortcut: Shortcut, callback: (e?: GestureResponderEvent | KeyboardEvent) => void, config: KeyboardShortcutConfig = {}) { const { captureOnInputs = true, shouldBubble = false,