diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 5fcf1fe7442b..29fd0c2700dc 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -18,13 +18,13 @@ import MessagesRow from './MessagesRow'; type OfflineWithFeedbackProps = ChildrenProps & { /** The type of action that's pending */ - pendingAction: OnyxCommon.PendingAction; + pendingAction?: OnyxCommon.PendingAction; /** Determine whether to hide the component's children if deletion is pending */ shouldHideOnDelete?: boolean; /** The errors to display */ - errors?: OnyxCommon.Errors; + errors?: OnyxCommon.Errors | null; /** Whether we should show the error messages */ shouldShowErrorMessages?: boolean; @@ -56,7 +56,7 @@ type OfflineWithFeedbackProps = ChildrenProps & { type StrikethroughProps = Partial & {style: Array}; -function omitBy(obj: Record | undefined, predicate: (value: T) => boolean) { +function omitBy(obj: Record | undefined | null, predicate: (value: T) => boolean) { // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars return Object.fromEntries(Object.entries(obj ?? {}).filter(([_, value]) => !predicate(value))); } diff --git a/src/components/OptionRow.js b/src/components/OptionRow.tsx similarity index 57% rename from src/components/OptionRow.js rename to src/components/OptionRow.tsx index c31ed7af1e90..5a2f6902c4c0 100644 --- a/src/components/OptionRow.js +++ b/src/components/OptionRow.tsx @@ -1,13 +1,13 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import lodashIsEqual from 'lodash/isEqual'; import React, {useEffect, useRef, useState} from 'react'; -import {InteractionManager, StyleSheet, View} from 'react-native'; -import _ from 'underscore'; +import {InteractionManager, StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import type {OptionData} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import Button from './Button'; import DisplayNames from './DisplayNames'; @@ -16,159 +16,156 @@ import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import MultipleAvatars from './MultipleAvatars'; import OfflineWithFeedback from './OfflineWithFeedback'; -import optionPropTypes from './optionPropTypes'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; import SelectCircle from './SelectCircle'; import SubscriptAvatar from './SubscriptAvatar'; import Text from './Text'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; -const propTypes = { +type OptionRowProps = { /** Style for hovered state */ - // eslint-disable-next-line react/forbid-prop-types - hoverStyle: PropTypes.object, + hoverStyle?: StyleProp; /** Option to allow the user to choose from can be type 'report' or 'user' */ - option: optionPropTypes.isRequired, + option: OptionData; /** Whether this option is currently in focus so we can modify its style */ - optionIsFocused: PropTypes.bool, + optionIsFocused?: boolean; /** A function that is called when an option is selected. Selected option is passed as a param */ - onSelectRow: PropTypes.func, + onSelectRow?: (option: OptionData, refElement: View | HTMLDivElement | null) => void | Promise; /** Whether we should show the selected state */ - showSelectedState: PropTypes.bool, + showSelectedState?: boolean; /** Whether to show a button pill instead of a tickbox */ - shouldShowSelectedStateAsButton: PropTypes.bool, + shouldShowSelectedStateAsButton?: boolean; /** Text for button pill */ - selectedStateButtonText: PropTypes.string, + selectedStateButtonText?: string; /** Callback to fire when the multiple selector (tickbox or button) is clicked */ - onSelectedStatePressed: PropTypes.func, + onSelectedStatePressed?: (option: OptionData) => void; /** Whether we highlight selected option */ - highlightSelected: PropTypes.bool, + highlightSelected?: boolean; /** Whether this item is selected */ - isSelected: PropTypes.bool, + isSelected?: boolean; /** Display the text of the option in bold font style */ - boldStyle: PropTypes.bool, + boldStyle?: boolean; /** Whether to show the title tooltip */ - showTitleTooltip: PropTypes.bool, + showTitleTooltip?: boolean; /** Whether this option should be disabled */ - isDisabled: PropTypes.bool, + isDisabled?: boolean; /** Whether to show a line separating options in list */ - shouldHaveOptionSeparator: PropTypes.bool, + shouldHaveOptionSeparator?: boolean; /** Whether to remove the lateral padding and align the content with the margins */ - shouldDisableRowInnerPadding: PropTypes.bool, + shouldDisableRowInnerPadding?: boolean; /** Whether to prevent default focusing on select */ - shouldPreventDefaultFocusOnSelectRow: PropTypes.bool, + shouldPreventDefaultFocusOnSelectRow?: boolean; /** Whether to wrap large text up to 2 lines */ - isMultilineSupported: PropTypes.bool, + isMultilineSupported?: boolean; - /** Key used internally by React */ - keyForList: PropTypes.string, - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + /** Display name and alternate text style */ + style?: StyleProp; - ...withLocalizePropTypes, -}; + /** Hovered background color */ + backgroundColor?: string; -const defaultProps = { - hoverStyle: undefined, - showSelectedState: false, - shouldShowSelectedStateAsButton: false, - selectedStateButtonText: 'Select', - onSelectedStatePressed: () => {}, - highlightSelected: false, - isSelected: false, - boldStyle: false, - showTitleTooltip: false, - onSelectRow: undefined, - isDisabled: false, - optionIsFocused: false, - isMultilineSupported: false, - style: null, - shouldHaveOptionSeparator: false, - shouldDisableRowInnerPadding: false, - shouldPreventDefaultFocusOnSelectRow: false, - keyForList: undefined, + /** Key used internally by React */ + keyForList?: string; }; -function OptionRow(props) { +function OptionRow({ + option, + onSelectRow, + style, + hoverStyle, + selectedStateButtonText, + keyForList, + isDisabled: isOptionDisabled = false, + isMultilineSupported = false, + shouldShowSelectedStateAsButton = false, + highlightSelected = false, + shouldHaveOptionSeparator = false, + showTitleTooltip = false, + optionIsFocused = false, + boldStyle = false, + onSelectedStatePressed = () => {}, + backgroundColor, + isSelected = false, + showSelectedState = false, + shouldDisableRowInnerPadding = false, + shouldPreventDefaultFocusOnSelectRow = false, +}: OptionRowProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const pressableRef = useRef(null); - const [isDisabled, setIsDisabled] = useState(props.isDisabled); + const {translate} = useLocalize(); + const pressableRef = useRef(null); + const [isDisabled, setIsDisabled] = useState(isOptionDisabled); useEffect(() => { - setIsDisabled(props.isDisabled); - }, [props.isDisabled]); + setIsDisabled(isOptionDisabled); + }, [isOptionDisabled]); - const text = lodashGet(props.option, 'text', ''); - const fullTitle = props.isMultilineSupported ? text.trimStart() : text; + const text = option.text ?? ''; + const fullTitle = isMultilineSupported ? text.trimStart() : text; const indentsLength = text.length - fullTitle.length; const paddingLeft = Math.floor(indentsLength / CONST.INDENTS.length) * styles.ml3.marginLeft; - const textStyle = props.optionIsFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; - const textUnreadStyle = props.boldStyle || props.option.boldStyle ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; - const displayNameStyle = StyleUtils.combineStyles( + const textStyle = optionIsFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; + const textUnreadStyle = boldStyle || option.boldStyle ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; + const displayNameStyle: StyleProp = [ styles.optionDisplayName, textUnreadStyle, - props.style, + style, styles.pre, isDisabled ? styles.optionRowDisabled : {}, - props.isMultilineSupported ? {paddingLeft} : {}, - ); - const alternateTextStyle = StyleUtils.combineStyles( + isMultilineSupported ? {paddingLeft} : {}, + ]; + const alternateTextStyle: StyleProp = [ textStyle, styles.optionAlternateText, styles.textLabelSupporting, - props.style, - lodashGet(props.option, 'alternateTextMaxLines', 1) === 1 ? styles.pre : styles.preWrap, - ); + style, + (option.alternateTextMaxLines ?? 1) === 1 ? styles.pre : styles.preWrap, + ]; const contentContainerStyles = [styles.flex1]; const sidebarInnerRowStyle = StyleSheet.flatten([styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRow, styles.justifyContentCenter]); - const hoveredBackgroundColor = - (props.hoverStyle || styles.sidebarLinkHover) && (props.hoverStyle || styles.sidebarLinkHover).backgroundColor - ? (props.hoverStyle || styles.sidebarLinkHover).backgroundColor - : props.backgroundColor; + const flattenHoverStyle = StyleSheet.flatten(hoverStyle); + const hoveredStyle = hoverStyle ? flattenHoverStyle : styles.sidebarLinkHover; + const hoveredBackgroundColor = hoveredStyle?.backgroundColor ? (hoveredStyle.backgroundColor as string) : backgroundColor; const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; - const isMultipleParticipant = lodashGet(props.option, 'participantsList.length', 0) > 1; + const isMultipleParticipant = (option.participantsList?.length ?? 0) > 1; // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips( - (props.option.participantsList || (props.option.accountID ? [props.option] : [])).slice(0, 10), - isMultipleParticipant, - ); + const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips((option.participantsList ?? (option.accountID ? [option] : [])).slice(0, 10), isMultipleParticipant); let subscriptColor = theme.appBG; - if (props.optionIsFocused) { + if (optionIsFocused) { subscriptColor = focusedBackgroundColor; } return ( {(hovered) => ( (pressableRef.current = el)} + nativeID={keyForList} + ref={pressableRef} onPress={(e) => { - if (!props.onSelectRow) { + if (!onSelectRow) { return; } @@ -176,12 +173,13 @@ function OptionRow(props) { if (e) { e.preventDefault(); } - let result = props.onSelectRow(props.option, pressableRef.current); + let result = onSelectRow(option, pressableRef.current); if (!(result instanceof Promise)) { result = Promise.resolve(); } + InteractionManager.runAfterInteractions(() => { - result.finally(() => setIsDisabled(props.isDisabled)); + result?.finally(() => setIsDisabled(isOptionDisabled)); }); }} disabled={isDisabled} @@ -190,68 +188,64 @@ function OptionRow(props) { styles.alignItemsCenter, styles.justifyContentBetween, styles.sidebarLink, - !props.isDisabled && styles.cursorPointer, - props.shouldDisableRowInnerPadding ? null : styles.sidebarLinkInner, - props.optionIsFocused ? styles.sidebarLinkActive : null, - props.shouldHaveOptionSeparator && styles.borderTop, - !props.onSelectRow && !props.isDisabled ? styles.cursorDefault : null, + !isOptionDisabled && styles.cursorPointer, + shouldDisableRowInnerPadding ? null : styles.sidebarLinkInner, + optionIsFocused ? styles.sidebarLinkActive : null, + shouldHaveOptionSeparator && styles.borderTop, + !onSelectRow && !isOptionDisabled ? styles.cursorDefault : null, ]} - accessibilityLabel={props.option.text} + accessibilityLabel={option.text} role={CONST.ROLE.BUTTON} hoverDimmingValue={1} - hoverStyle={!props.optionIsFocused ? props.hoverStyle || styles.sidebarLinkHover : undefined} - needsOffscreenAlphaCompositing={lodashGet(props.option, 'icons.length', 0) >= 2} - onMouseDown={props.shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined} + hoverStyle={!optionIsFocused ? hoverStyle ?? styles.sidebarLinkHover : undefined} + needsOffscreenAlphaCompositing={(option.icons?.length ?? 0) >= 2} + onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (event) => event.preventDefault() : undefined} > - {!_.isEmpty(props.option.icons) && - (props.option.shouldShowSubscript ? ( + {!!option.icons?.length && + (option.shouldShowSubscript ? ( ) : ( ))} - {props.option.alternateText ? ( + {option.alternateText ? ( - {props.option.alternateText} + {option.alternateText} ) : null} - {props.option.descriptiveText ? ( + {option.descriptiveText ? ( - {props.option.descriptiveText} + {option.descriptiveText} ) : null} - {props.option.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && ( + {option.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && ( )} - {props.showSelectedState && ( + {showSelectedState && ( <> - {props.shouldShowSelectedStateAsButton && !props.isSelected ? ( + {shouldShowSelectedStateAsButton && !isSelected ? (