diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.tsx similarity index 70% rename from src/components/OptionsList/BaseOptionsList.js rename to src/components/OptionsList/BaseOptionsList.tsx index bd3695eb7aa9..c1e4562a0c2d 100644 --- a/src/components/OptionsList/BaseOptionsList.js +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -1,89 +1,73 @@ -import PropTypes from 'prop-types'; +import isEqual from 'lodash/isEqual'; +import type {ForwardedRef} from 'react'; import React, {forwardRef, memo, useEffect, useRef} from 'react'; +import type {SectionListRenderItem} from 'react-native'; import {View} from 'react-native'; -import _ from 'underscore'; import OptionRow from '@components/OptionRow'; import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; import SectionList from '@components/SectionList'; import Text from '@components/Text'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {OptionData} from '@libs/ReportUtils'; +import StringUtils from '@libs/StringUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import {defaultProps as optionsListDefaultProps, propTypes as optionsListPropTypes} from './optionsListPropTypes'; +import type {BaseOptionListProps, OptionsList, OptionsListData, Section} from './types'; -const propTypes = { - /** Determines whether the keyboard gets dismissed in response to a drag */ - keyboardDismissMode: PropTypes.string, - - /** Called when the user begins to drag the scroll view. Only used for the native component */ - onScrollBeginDrag: PropTypes.func, - - /** Callback executed on scroll. Only used for web/desktop component */ - onScroll: PropTypes.func, - - /** List styles for SectionList */ - listStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - - ...optionsListPropTypes, -}; - -const defaultProps = { - keyboardDismissMode: 'none', - onScrollBeginDrag: () => {}, - onScroll: () => {}, - listStyles: [], - ...optionsListDefaultProps, -}; - -function BaseOptionsList({ - keyboardDismissMode, - onScrollBeginDrag, - onScroll, - listStyles, - focusedIndex, - selectedOptions, - headerMessage, - isLoading, - sections, - onLayout, - hideSectionHeaders, - shouldHaveOptionSeparator, - showTitleTooltip, - optionHoveredStyle, - contentContainerStyles, - sectionHeaderStyle, - showScrollIndicator, - listContainerStyles: listContainerStylesProp, - shouldDisableRowInnerPadding, - shouldPreventDefaultFocusOnSelectRow, - disableFocusOptions, - canSelectMultipleOptions, - shouldShowMultipleOptionSelectorAsButton, - multipleOptionSelectorButtonText, - onAddToSelection, - highlightSelectedOptions, - onSelectRow, - boldStyle, - isDisabled, - innerRef, - isRowMultilineSupported, - isLoadingNewOptions, - nestedScrollEnabled, - bounces, - renderFooterContent, -}) { +function BaseOptionsList( + { + keyboardDismissMode = 'none', + onScrollBeginDrag = () => {}, + onScroll = () => {}, + listStyles, + focusedIndex = 0, + selectedOptions = [], + headerMessage = '', + isLoading = false, + sections = [], + onLayout, + hideSectionHeaders = false, + shouldHaveOptionSeparator = false, + showTitleTooltip = false, + optionHoveredStyle, + contentContainerStyles, + sectionHeaderStyle, + showScrollIndicator = false, + listContainerStyles: listContainerStylesProp, + shouldDisableRowInnerPadding = false, + shouldPreventDefaultFocusOnSelectRow = false, + disableFocusOptions = false, + canSelectMultipleOptions = false, + shouldShowMultipleOptionSelectorAsButton, + multipleOptionSelectorButtonText, + onAddToSelection, + highlightSelectedOptions = false, + onSelectRow, + boldStyle = false, + isDisabled = false, + isRowMultilineSupported = false, + isLoadingNewOptions = false, + nestedScrollEnabled = true, + bounces = true, + renderFooterContent, + }: BaseOptionListProps, + ref: ForwardedRef, +) { const styles = useThemeStyles(); - const flattenedData = useRef(); - const previousSections = usePrevious(sections); + const flattenedData = useRef< + Array<{ + length: number; + offset: number; + }> + >([]); + const previousSections = usePrevious(sections); const didLayout = useRef(false); - const listContainerStyles = listContainerStylesProp || [styles.flex1]; + const listContainerStyles = listContainerStylesProp ?? [styles.flex1]; /** * This helper function is used to memoize the computation needed for getItemLayout. It is run whenever section data changes. - * - * @returns {Array} */ const buildFlatSectionArray = () => { let offset = 0; @@ -92,8 +76,7 @@ function BaseOptionsList({ const flatArray = [{length: 0, offset}]; // Build the flat array - for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { - const section = sections[sectionIndex]; + for (const section of sections) { // Add the section header const sectionHeaderHeight = section.title && !hideSectionHeaders ? variables.optionsListSectionHeaderHeight : 0; flatArray.push({length: sectionHeaderHeight, offset}); @@ -119,7 +102,7 @@ function BaseOptionsList({ }; useEffect(() => { - if (_.isEqual(sections, previousSections)) { + if (isEqual(sections, previousSections)) { return; } flattenedData.current = buildFlatSectionArray(); @@ -138,8 +121,8 @@ function BaseOptionsList({ * 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. @@ -147,14 +130,12 @@ function BaseOptionsList({ * 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) => { - if (!_.has(flattenedData.current, flatDataArrayIndex)) { + // eslint-disable-next-line @typescript-eslint/naming-convention + const getItemLayout = (_data: OptionsListData[] | null, flatDataArrayIndex: number) => { + if (!flattenedData.current[flatDataArrayIndex]) { flattenedData.current = buildFlatSectionArray(); } - const targetItem = flattenedData.current[flatDataArrayIndex]; return { length: targetItem.length, @@ -165,10 +146,8 @@ function BaseOptionsList({ /** * Returns the key used by the list - * @param {Object} option - * @return {String} */ - const extractKey = (option) => option.keyForList; + const extractKey = (option: OptionData) => option.keyForList ?? ''; /** * Function which renders a row in the list @@ -180,9 +159,10 @@ function BaseOptionsList({ * * @return {Component} */ - const renderItem = ({item, index, section}) => { - const isItemDisabled = isDisabled || section.isDisabled || !!item.isDisabled; - const isSelected = _.some(selectedOptions, (option) => { + + const renderItem: SectionListRenderItem = ({item, index, section}) => { + const isItemDisabled = isDisabled || !!section.isDisabled || !!item.isDisabled; + const isSelected = selectedOptions?.some((option) => { if (option.accountID && option.accountID === item.accountID) { return true; } @@ -191,7 +171,7 @@ function BaseOptionsList({ return true; } - if (_.isEmpty(option.name)) { + if (!option.name || StringUtils.isEmptyString(option.name)) { return false; } @@ -200,7 +180,7 @@ function BaseOptionsList({ return ( { + const renderSectionHeader = ({section: {title, shouldShow}}: {section: OptionsListData}) => { if (!title && shouldShow && !hideSectionHeaders && sectionHeaderStyle) { return ; } @@ -265,8 +238,8 @@ function BaseOptionsList({ {headerMessage} ) : null} - + ref={ref} style={listStyles} indicatorStyle="white" keyboardShouldPersistTaps="always" @@ -299,23 +272,15 @@ function BaseOptionsList({ ); } -BaseOptionsList.propTypes = propTypes; -BaseOptionsList.defaultProps = defaultProps; BaseOptionsList.displayName = 'BaseOptionsList'; // using memo to avoid unnecessary rerenders when parents component rerenders (thus causing this component to rerender because shallow comparison is used for some props). export default memo( - forwardRef((props, ref) => ( - - )), + forwardRef(BaseOptionsList), (prevProps, nextProps) => nextProps.focusedIndex === prevProps.focusedIndex && - nextProps.selectedOptions.length === prevProps.selectedOptions.length && + nextProps?.selectedOptions?.length === prevProps?.selectedOptions?.length && nextProps.headerMessage === prevProps.headerMessage && nextProps.isLoading === prevProps.isLoading && - _.isEqual(nextProps.sections, prevProps.sections), + isEqual(nextProps.sections, prevProps.sections), ); diff --git a/src/components/OptionsList/index.native.js b/src/components/OptionsList/index.native.js deleted file mode 100644 index ab2db4f20967..000000000000 --- a/src/components/OptionsList/index.native.js +++ /dev/null @@ -1,19 +0,0 @@ -import React, {forwardRef} from 'react'; -import {Keyboard} from 'react-native'; -import BaseOptionsList from './BaseOptionsList'; -import {defaultProps, propTypes} from './optionsListPropTypes'; - -const OptionsList = forwardRef((props, ref) => ( - Keyboard.dismiss()} - /> -)); - -OptionsList.propTypes = propTypes; -OptionsList.defaultProps = defaultProps; -OptionsList.displayName = 'OptionsList'; - -export default OptionsList; diff --git a/src/components/OptionsList/index.native.tsx b/src/components/OptionsList/index.native.tsx new file mode 100644 index 000000000000..bdcd0418a940 --- /dev/null +++ b/src/components/OptionsList/index.native.tsx @@ -0,0 +1,20 @@ +import React, {forwardRef} from 'react'; +import type {ForwardedRef} from 'react'; +import {Keyboard} from 'react-native'; +import BaseOptionsList from './BaseOptionsList'; +import type {OptionsListProps, OptionsList as OptionsListType} from './types'; + +function OptionsList(props: OptionsListProps, ref: ForwardedRef) { + return ( + Keyboard.dismiss()} + /> + ); +} + +OptionsList.displayName = 'OptionsList'; + +export default forwardRef(OptionsList); diff --git a/src/components/OptionsList/index.js b/src/components/OptionsList/index.tsx similarity index 69% rename from src/components/OptionsList/index.js rename to src/components/OptionsList/index.tsx index 36b8e7fccf12..d0c6cb31bf64 100644 --- a/src/components/OptionsList/index.js +++ b/src/components/OptionsList/index.tsx @@ -1,12 +1,11 @@ import React, {forwardRef, useCallback, useEffect, useRef} from 'react'; +import type {ForwardedRef} from 'react'; import {Keyboard} from 'react-native'; -import _ from 'underscore'; -import withWindowDimensions from '@components/withWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import BaseOptionsList from './BaseOptionsList'; -import {defaultProps, propTypes} from './optionsListPropTypes'; +import type {OptionsListProps, OptionsList as OptionsListType} from './types'; -function OptionsList(props) { +function OptionsList(props: OptionsListProps, ref: ForwardedRef) { const isScreenTouched = useRef(false); useEffect(() => { @@ -43,25 +42,13 @@ function OptionsList(props) { return ( ); } OptionsList.displayName = 'OptionsList'; -OptionsList.propTypes = propTypes; -OptionsList.defaultProps = defaultProps; -const OptionsListWithRef = forwardRef((props, ref) => ( - -)); - -OptionsListWithRef.displayName = 'OptionsListWithRef'; - -export default withWindowDimensions(OptionsListWithRef); +export default forwardRef(OptionsList); diff --git a/src/components/OptionsList/optionsListPropTypes.js b/src/components/OptionsList/optionsListPropTypes.js deleted file mode 100644 index 6008101ac1b6..000000000000 --- a/src/components/OptionsList/optionsListPropTypes.js +++ /dev/null @@ -1,138 +0,0 @@ -import PropTypes from 'prop-types'; -import optionPropTypes from '@components/optionPropTypes'; -import SectionList from '@components/SectionList'; -import stylePropTypes from '@styles/stylePropTypes'; - -const propTypes = { - /** option flexStyle for the options list container */ - listContainerStyles: PropTypes.arrayOf(PropTypes.object), - - /** Style for hovered state */ - // eslint-disable-next-line react/forbid-prop-types - optionHoveredStyle: PropTypes.object, - - /** Extra styles for the section list container */ - contentContainerStyles: PropTypes.arrayOf(PropTypes.object), - - /** Style for section headers */ - sectionHeaderStyle: stylePropTypes, - - /** Sections for the section list */ - sections: PropTypes.arrayOf( - PropTypes.shape({ - /** Title of the section */ - title: PropTypes.string, - - /** The initial index of this section given the total number of options in each section's data array */ - indexOffset: PropTypes.number, - - /** Array of options */ - data: PropTypes.arrayOf(optionPropTypes), - - /** Whether this section should show or not */ - shouldShow: PropTypes.bool, - }), - ), - - /** Index for option to focus on */ - focusedIndex: PropTypes.number, - - /** Array of already selected options */ - selectedOptions: PropTypes.arrayOf(optionPropTypes), - - /** Whether we can select multiple options or not */ - canSelectMultipleOptions: PropTypes.bool, - - /** Whether we highlight selected options */ - highlightSelectedOptions: PropTypes.bool, - - /** Whether to show headers above each section or not */ - hideSectionHeaders: PropTypes.bool, - - /** Whether to allow option focus or not */ - disableFocusOptions: PropTypes.bool, - - /** Display the text of the option in bold font style */ - boldStyle: PropTypes.bool, - - /** Callback to fire when a row is selected */ - onSelectRow: PropTypes.func, - - /** Optional header message */ - headerMessage: PropTypes.string, - - /** Passed via forwardRef so we can access the SectionList ref */ - innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(SectionList)})]), - - /** Whether to show the title tooltip */ - showTitleTooltip: PropTypes.bool, - - /** Whether to disable the interactivity of the list's option row(s) */ - isDisabled: PropTypes.bool, - - /** Whether the options list skeleton loading view should be displayed */ - isLoading: PropTypes.bool, - - /** Callback to execute when the SectionList lays out */ - onLayout: PropTypes.func, - - /** Whether to show a line separating options in list */ - shouldHaveOptionSeparator: PropTypes.bool, - - /** Whether to disable the inner padding in rows */ - shouldDisableRowInnerPadding: PropTypes.bool, - - /** Whether to prevent default focusing when selecting a row */ - shouldPreventDefaultFocusOnSelectRow: PropTypes.bool, - - /** Whether to show the scroll bar */ - showScrollIndicator: PropTypes.bool, - - /** Whether to wrap large text up to 2 lines */ - isRowMultilineSupported: PropTypes.bool, - - /** Whether we are loading new options */ - isLoadingNewOptions: PropTypes.bool, - - /** Whether nested scroll of options is enabled, true by default */ - nestedScrollEnabled: PropTypes.bool, - - /** Whether the list should have a bounce effect on iOS */ - bounces: PropTypes.bool, - - /** Custom content to display in the floating footer */ - renderFooterContent: PropTypes.func, -}; - -const defaultProps = { - optionHoveredStyle: undefined, - contentContainerStyles: [], - sectionHeaderStyle: undefined, - listContainerStyles: undefined, - sections: [], - focusedIndex: 0, - selectedOptions: [], - canSelectMultipleOptions: false, - highlightSelectedOptions: false, - hideSectionHeaders: false, - disableFocusOptions: false, - boldStyle: false, - onSelectRow: undefined, - headerMessage: '', - innerRef: null, - showTitleTooltip: false, - isDisabled: false, - isLoading: false, - onLayout: undefined, - shouldHaveOptionSeparator: false, - shouldDisableRowInnerPadding: false, - shouldPreventDefaultFocusOnSelectRow: false, - showScrollIndicator: false, - isRowMultilineSupported: false, - isLoadingNewOptions: false, - nestedScrollEnabled: true, - bounces: true, - renderFooterContent: undefined, -}; - -export {propTypes, defaultProps}; diff --git a/src/components/OptionsList/types.ts b/src/components/OptionsList/types.ts new file mode 100644 index 000000000000..12e44adb5800 --- /dev/null +++ b/src/components/OptionsList/types.ts @@ -0,0 +1,137 @@ +import type {RefObject} from 'react'; +import type {SectionList, SectionListData, StyleProp, View, ViewStyle} from 'react-native'; +import type {OptionData} from '@libs/ReportUtils'; + +type OptionsList = SectionList; +type OptionsListData = SectionListData; + +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: OptionData[]; + + /** Whether this section should show or not */ + shouldShow?: boolean; + + /** Whether this section is disabled or not */ + isDisabled?: boolean; +}; + +type OptionsListProps = { + /** option flexStyle for the options list container */ + listContainerStyles?: StyleProp; + + /** Style for hovered state */ + optionHoveredStyle?: StyleProp; + + /** Extra styles for the section list container */ + contentContainerStyles?: StyleProp; + + /** Style for section headers */ + sectionHeaderStyle?: StyleProp; + + /** Sections for the section list */ + sections: OptionsListData[]; + + /** Index for option to focus on */ + focusedIndex?: number; + + /** Array of already selected options */ + selectedOptions?: OptionData[]; + + /** Whether we can select multiple options or not */ + canSelectMultipleOptions?: boolean; + + /** Whether we highlight selected options */ + highlightSelectedOptions?: boolean; + + /** Whether to show headers above each section or not */ + hideSectionHeaders?: boolean; + + /** Whether to allow option focus or not */ + disableFocusOptions?: boolean; + + /** Display the text of the option in bold font style */ + boldStyle?: boolean; + + /** Callback to fire when a row is selected */ + onSelectRow?: (option: OptionData, refElement: View | HTMLDivElement | null) => void | Promise; + + /** Optional header message */ + headerMessage?: string; + + /** Passed via forwardRef so we can access the SectionList ref */ + innerRef?: RefObject | ((instance: SectionList | null) => void); + + /** Whether to show the title tooltip */ + showTitleTooltip?: boolean; + + /** Whether to disable the interactivity of the list's option row(s) */ + isDisabled?: boolean; + + /** Whether the options list skeleton loading view should be displayed */ + isLoading?: boolean; + + /** Callback to execute when the SectionList lays out */ + onLayout?: () => void; + + /** Whether to show a line separating options in list */ + shouldHaveOptionSeparator?: boolean; + + /** Whether to disable the inner padding in rows */ + shouldDisableRowInnerPadding?: boolean; + + /** Whether to prevent default focusing when selecting a row */ + shouldPreventDefaultFocusOnSelectRow?: boolean; + + /** Whether to show the scroll bar */ + showScrollIndicator?: boolean; + + /** Whether to wrap large text up to 2 lines */ + isRowMultilineSupported?: boolean; + + /** Whether we are loading new options */ + isLoadingNewOptions?: boolean; + + /** Whether nested scroll of options is enabled, true by default */ + nestedScrollEnabled?: boolean; + + /** Whether the list should have a bounce effect on iOS */ + bounces?: boolean; + + /** Custom content to display in the floating footer */ + renderFooterContent?: () => JSX.Element; + + /** Whether to show a button pill instead of a standard tickbox */ + shouldShowMultipleOptionSelectorAsButton: boolean; + + /** Text for button pill */ + multipleOptionSelectorButtonText: string; + + /** Callback to fire when the multiple selector (tickbox or button) is clicked */ + onAddToSelection: () => void; + + /** Safe area style */ + safeAreaPaddingBottomStyle?: StyleProp; +}; + +type BaseOptionListProps = OptionsListProps & { + /** Determines whether the keyboard gets dismissed in response to a drag */ + keyboardDismissMode?: 'none' | 'interactive' | 'on-drag'; + + /** Called when the user begins to drag the scroll view. Only used for the native component */ + onScrollBeginDrag?: () => void; + + /** Callback executed on scroll. Only used for web/desktop component */ + onScroll?: () => void; + + /** List styles for SectionList */ + listStyles?: StyleProp; +}; + +export type {OptionsListProps, BaseOptionListProps, Section, OptionsList, OptionsListData}; diff --git a/src/components/SectionList/index.android.tsx b/src/components/SectionList/index.android.tsx index 1aa9b501146c..04cdd70e9ee2 100644 --- a/src/components/SectionList/index.android.tsx +++ b/src/components/SectionList/index.android.tsx @@ -1,19 +1,20 @@ import React, {forwardRef} from 'react'; import {SectionList as RNSectionList} from 'react-native'; -import type ForwardedSectionList from './types'; +import type {SectionListProps, SectionListRef} from './types'; -// eslint-disable-next-line react/function-component-definition -const SectionListWithRef: ForwardedSectionList = (props, ref) => ( - -); +function SectionListWithRef(props: SectionListProps, ref: SectionListRef) { + return ( + + ); +} SectionListWithRef.displayName = 'SectionListWithRef'; diff --git a/src/components/SectionList/index.tsx b/src/components/SectionList/index.tsx index 4af7ad33705c..4f7b6e31f451 100644 --- a/src/components/SectionList/index.tsx +++ b/src/components/SectionList/index.tsx @@ -1,15 +1,16 @@ import React, {forwardRef} from 'react'; import {SectionList as RNSectionList} from 'react-native'; -import type ForwardedSectionList from './types'; +import type {SectionListProps, SectionListRef} from './types'; -// eslint-disable-next-line react/function-component-definition -const SectionList: ForwardedSectionList = (props, ref) => ( - -); +function SectionList(props: SectionListProps, ref: SectionListRef) { + return ( + + ); +} SectionList.displayName = 'SectionList'; diff --git a/src/components/SectionList/types.ts b/src/components/SectionList/types.ts index 4648172aabfd..9aef028559e6 100644 --- a/src/components/SectionList/types.ts +++ b/src/components/SectionList/types.ts @@ -1,9 +1,7 @@ import type {ForwardedRef} from 'react'; -import type {SectionList, SectionListProps} from 'react-native'; +import type {SectionList, SectionListProps as SectionListPropsRN} from 'react-native'; -type ForwardedSectionList = { - (props: SectionListProps, ref: ForwardedRef): React.ReactNode; - displayName: string; -}; +type SectionListProps = SectionListPropsRN; +type SectionListRef = ForwardedRef>; -export default ForwardedSectionList; +export type {SectionListProps, SectionListRef}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9e00646886fc..78086c354de0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -397,6 +397,8 @@ type OptionData = { isTaskReport?: boolean | null; parentReportAction?: ReportAction; displayNamesWithTooltips?: DisplayNameWithTooltips | null; + isDisabled?: boolean | null; + name?: string | null; } & Report; type OnyxDataTaskAssigneeChat = {