diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index 6bda82e668cb..2f105594cf98 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -53,9 +53,6 @@ type CheckboxProps = Partial & { /** Whether the checkbox should be selected when pressing Enter key */ shouldSelectOnPressEnter?: boolean; - - /** Additional styles to add to checkbox wrapper */ - wrapperStyle?: StyleProp; }; function Checkbox( @@ -75,7 +72,6 @@ function Checkbox( accessibilityLabel, shouldStopMouseDownPropagation, shouldSelectOnPressEnter, - wrapperStyle, }: CheckboxProps, ref: ForwardedRef, ) { @@ -127,7 +123,6 @@ function Checkbox( aria-checked={isIndeterminate ? 'mixed' : isChecked} accessibilityLabel={accessibilityLabel} pressDimmingValue={1} - wrapperStyle={wrapperStyle} > {children ?? ( - - {hasComments || isLoadingReportActions ? translate('common.comments') : ''} - - {!isEmptyTransactions && ( - - {translate('common.total')} - - {convertToDisplayString(totalDisplaySpend, report?.currency)} - - - )} - - ); -} - -MoneyRequestReportTotalSpend.displayName = 'MoneyRequestReportTotalSpend'; - -export default MoneyRequestReportTotalSpend; diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx deleted file mode 100644 index b4b7473e2b8f..000000000000 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionItem.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import React, {useEffect, useRef} from 'react'; -import type {View} from 'react-native'; -import {getButtonRole} from '@components/Button/utils'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import {PressableWithFeedback} from '@components/Pressable'; -import type {TableColumnSize} from '@components/Search/types'; -import TransactionItemRow from '@components/TransactionItemRow'; -import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; -import useLocalize from '@hooks/useLocalize'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import ControlSelection from '@libs/ControlSelection'; -import canUseTouchScreen from '@libs/DeviceCapabilities/canUseTouchScreen'; -import {getTransactionPendingAction, isTransactionPendingDelete} from '@libs/TransactionUtils'; -import variables from '@styles/variables'; -import CONST from '@src/CONST'; -import type {TransactionWithOptionalHighlight} from './MoneyRequestReportTransactionList'; - -const allReportColumns = [ - CONST.REPORT.TRANSACTION_LIST.COLUMNS.RECEIPT, - CONST.REPORT.TRANSACTION_LIST.COLUMNS.TYPE, - CONST.REPORT.TRANSACTION_LIST.COLUMNS.DATE, - CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT, - CONST.REPORT.TRANSACTION_LIST.COLUMNS.CATEGORY, - CONST.REPORT.TRANSACTION_LIST.COLUMNS.TAG, - CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS, - CONST.REPORT.TRANSACTION_LIST.COLUMNS.TOTAL_AMOUNT, -]; - -type MoneyRequestReportTransactionItemProps = { - /** The transaction that is being displayed */ - transaction: TransactionWithOptionalHighlight; - - /** Whether the mobile selection mode is enabled */ - isSelectionModeEnabled: boolean; - - /** Callback function triggered upon pressing a transaction checkbox. */ - toggleTransaction: (transactionID: string) => void; - - /** Callback function triggered upon pressing a transaction. */ - handleOnPress: (transactionID: string) => void; - - /** Callback function triggered upon long pressing a transaction. */ - handleLongPress: (transactionID: string) => void; - - /** Whether the transaction is selected */ - isSelected: boolean; - - /** The size of the date column */ - dateColumnSize: TableColumnSize; - - /** The size of the amount column */ - amountColumnSize: TableColumnSize; - - /** The size of the tax amount column */ - taxAmountColumnSize: TableColumnSize; - - /** Callback function that scrolls to this transaction in case it is newly added */ - scrollToNewTransaction?: (offset: number) => void; -}; - -function MoneyRequestReportTransactionItem({ - transaction, - isSelectionModeEnabled, - toggleTransaction, - isSelected, - handleOnPress, - handleLongPress, - dateColumnSize, - amountColumnSize, - taxAmountColumnSize, - scrollToNewTransaction, -}: MoneyRequestReportTransactionItemProps) { - const {translate} = useLocalize(); - const styles = useThemeStyles(); - // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const {isSmallScreenWidth, isMediumScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); - const theme = useTheme(); - const isPendingDelete = isTransactionPendingDelete(transaction); - const pendingAction = getTransactionPendingAction(transaction); - - const viewRef = useRef(null); - - // This useEffect scrolls to this transaction when it is newly added to the report - useEffect(() => { - if (!transaction.shouldBeHighlighted || !scrollToNewTransaction) { - return; - } - viewRef?.current?.measure((x, y, width, height, pageX, pageY) => { - scrollToNewTransaction?.(pageY); - }); - }, [scrollToNewTransaction, transaction.shouldBeHighlighted]); - - const animatedHighlightStyle = useAnimatedHighlightStyle({ - borderRadius: variables.componentBorderRadius, - shouldHighlight: transaction.shouldBeHighlighted ?? false, - highlightColor: theme.messageHighlightBG, - backgroundColor: theme.highlightBG, - }); - - return ( - - { - handleOnPress(transaction.transactionID); - }} - accessibilityLabel={translate('iou.viewDetails')} - role={getButtonRole(true)} - isNested - id={transaction.transactionID} - style={[styles.transactionListItemStyle]} - hoverStyle={[!isPendingDelete && styles.hoveredComponentBG, isSelected && styles.activeComponentBG]} - dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} - onPressIn={() => canUseTouchScreen() && ControlSelection.block()} - onPressOut={() => ControlSelection.unblock()} - onLongPress={() => { - handleLongPress(transaction.transactionID); - }} - disabled={isTransactionPendingDelete(transaction)} - ref={viewRef} - wrapperStyle={[animatedHighlightStyle, styles.userSelectNone]} - > - - - - ); -} - -MoneyRequestReportTransactionItem.displayName = 'MoneyRequestReportTransactionItem'; - -export default MoneyRequestReportTransactionItem; diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index 5fb04f36f229..9dc6982e09b1 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -1,24 +1,32 @@ -import {useFocusEffect} from '@react-navigation/native'; +import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import isEmpty from 'lodash/isEmpty'; import React, {memo, useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; +import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'; import type {TupleToUnion} from 'type-fest'; +import {getButtonRole} from '@components/Button/utils'; import Checkbox from '@components/Checkbox'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import Modal from '@components/Modal'; +import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import {useSearchContext} from '@components/Search/SearchContext'; import type {SortOrder} from '@components/Search/types'; import Text from '@components/Text'; +import TransactionItemRow from '@components/TransactionItemRow'; import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; +import useHover from '@hooks/useHover'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; +import {useMouseContext} from '@hooks/useMouseContext'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import {setActiveTransactionThreadIDs} from '@libs/actions/TransactionThreadNavigation'; +import ControlSelection from '@libs/ControlSelection'; import {convertToDisplayString} from '@libs/CurrencyUtils'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import {getThreadReportIDsForTransactions} from '@libs/MoneyRequestReportUtils'; import {navigationRef} from '@libs/Navigation/Navigation'; import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils'; @@ -33,8 +41,6 @@ import NAVIGATORS from '@src/NAVIGATORS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import MoneyRequestReportTableHeader from './MoneyRequestReportTableHeader'; -import MoneyRequestReportTotalSpend from './MoneyRequestReportTotalSpend'; -import MoneyRequestReportTransactionItem from './MoneyRequestReportTransactionItem'; import SearchMoneyRequestReportEmptyState from './SearchMoneyRequestReportEmptyState'; type MoneyRequestReportTransactionListProps = { @@ -72,6 +78,17 @@ const sortableColumnNames = [ CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT, ]; +const allReportColumns = [ + CONST.REPORT.TRANSACTION_LIST.COLUMNS.RECEIPT, + CONST.REPORT.TRANSACTION_LIST.COLUMNS.TYPE, + CONST.REPORT.TRANSACTION_LIST.COLUMNS.DATE, + CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT, + CONST.REPORT.TRANSACTION_LIST.COLUMNS.CATEGORY, + CONST.REPORT.TRANSACTION_LIST.COLUMNS.TAG, + CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS, + CONST.REPORT.TRANSACTION_LIST.COLUMNS.TOTAL_AMOUNT, +]; + type SortableColumnName = TupleToUnion; type SortedTransactions = { @@ -99,6 +116,7 @@ function MoneyRequestReportTransactionList({ const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); + const isFocused = useIsFocused(); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout(); const [isModalVisible, setIsModalVisible] = useState(false); @@ -110,9 +128,13 @@ function MoneyRequestReportTransactionList({ const shouldShowBreakdown = !!nonReimbursableSpend && !!reimbursableSpend; const transactionsWithoutPendingDelete = useMemo(() => transactions.filter((t) => !isTransactionPendingDelete(t)), [transactions]); - const hasPendingAction = useMemo(() => { - return transactions.some(getTransactionPendingAction); - }, [transactions]); + const pendingActionsOpacity = useMemo(() => { + const pendingAction = transactions.some(getTransactionPendingAction); + return pendingAction && styles.opacitySemiTransparent; + }, [styles.opacitySemiTransparent, transactions]); + + const {bind} = useHover(); + const {isMouseDownOnInput, setMouseUp} = useMouseContext(); const {selectedTransactionIDs, setSelectedTransactions, clearSelectedTransactions} = useSearchContext(); const isMobileSelectionModeEnabled = useMobileSelectionMode(); @@ -143,6 +165,12 @@ function MoneyRequestReportTransactionList({ }, [clearSelectedTransactions]), ); + const handleMouseLeave = (e: React.MouseEvent) => { + bind.onMouseLeave(); + e.stopPropagation(); + setMouseUp(); + }; + const [sortConfig, setSortConfig] = useState({ sortBy: CONST.SEARCH.TABLE_COLUMNS.DATE, sortOrder: CONST.SEARCH.SORT_ORDER.ASC, @@ -160,8 +188,8 @@ function MoneyRequestReportTransactionList({ }, [newTransactions, sortBy, sortOrder, transactions]); const navigateToTransaction = useCallback( - (activeTransactionID: string) => { - const iouAction = getIOUActionForTransactionID(reportActions, activeTransactionID); + (activeTransaction: OnyxTypes.Transaction) => { + const iouAction = getIOUActionForTransactionID(reportActions, activeTransaction.transactionID); const reportIDToNavigate = iouAction?.childReportID; if (!reportIDToNavigate) { return; @@ -190,161 +218,173 @@ function MoneyRequestReportTransactionList({ }; }, [transactions]); + const pressableStyle = [styles.overflowHidden]; const isEmptyTransactions = isEmpty(transactions); - const handleLongPress = useCallback( - (transactionID: string) => { - if (!isSmallScreenWidth) { - return; - } - if (isMobileSelectionModeEnabled) { - toggleTransaction(transactionID); - return; - } - setSelectedTransactionID(transactionID); - setIsModalVisible(true); - }, - [isSmallScreenWidth, isMobileSelectionModeEnabled, toggleTransaction, setSelectedTransactionID, setIsModalVisible], - ); - - const handleOnPress = useCallback( - (transactionID: string) => { - if (isMobileSelectionModeEnabled) { - toggleTransaction(transactionID); - return; - } - - navigateToTransaction(transactionID); - }, - [isMobileSelectionModeEnabled, toggleTransaction, navigateToTransaction], - ); - const listHorizontalPadding = styles.ph5; - - if (isEmptyTransactions) { - return ( - <> - - - - ); - } - return ( <> - {!shouldUseNarrowLayout && ( - - - { - if (selectedTransactionIDs.length !== 0) { - clearSelectedTransactions(true); - } else { - setSelectedTransactions(transactionsWithoutPendingDelete.map((t) => t.transactionID)); - } - }} - accessibilityLabel={CONST.ROLE.CHECKBOX} - isIndeterminate={selectedTransactionIDs.length > 0 && selectedTransactionIDs.length !== transactionsWithoutPendingDelete.length} - isChecked={selectedTransactionIDs.length > 0 && selectedTransactionIDs.length === transactionsWithoutPendingDelete.length} - /> - {isMediumScreenWidth && {translate('workspace.people.selectAll')}} + {!isEmptyTransactions ? ( + <> + {!shouldUseNarrowLayout && ( + + + { + if (selectedTransactionIDs.length !== 0) { + clearSelectedTransactions(true); + } else { + setSelectedTransactions(transactionsWithoutPendingDelete.map((t) => t.transactionID)); + } + }} + accessibilityLabel={CONST.ROLE.CHECKBOX} + isIndeterminate={selectedTransactionIDs.length > 0 && selectedTransactionIDs.length !== transactionsWithoutPendingDelete.length} + isChecked={selectedTransactionIDs.length > 0 && selectedTransactionIDs.length === transactionsWithoutPendingDelete.length} + /> + {isMediumScreenWidth && {translate('workspace.people.selectAll')}} + + {!isMediumScreenWidth && ( + { + if (!isSortableColumnName(selectedSortBy)) { + return; + } + + setSortConfig((prevState) => ({...prevState, sortBy: selectedSortBy, sortOrder: selectedSortOrder})); + }} + isIOUReport={isIOUReport(report)} + /> + )} + + )} + + {sortedTransactions.map((transaction) => { + return ( + { + if (isMouseDownOnInput) { + e?.stopPropagation(); + return; + } + + if (isMobileSelectionModeEnabled) { + toggleTransaction(transaction.transactionID); + return; + } + + navigateToTransaction(transaction); + }} + accessibilityLabel={translate('iou.viewDetails')} + role={getButtonRole(true)} + isNested + hoverDimmingValue={1} + onMouseDown={(e) => e.preventDefault()} + id={transaction.transactionID} + style={[pressableStyle, styles.userSelectNone]} + dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} + onMouseLeave={handleMouseLeave} + onPressIn={() => canUseTouchScreen() && ControlSelection.block()} + onPressOut={() => ControlSelection.unblock()} + onLongPress={() => { + if (!isSmallScreenWidth) { + return; + } + if (isMobileSelectionModeEnabled) { + toggleTransaction(transaction.transactionID); + return; + } + setSelectedTransactionID(transaction.transactionID); + setIsModalVisible(true); + }} + disabled={isTransactionPendingDelete(transaction)} + > + + + ); + })} - {!isMediumScreenWidth && ( - { - if (!isSortableColumnName(selectedSortBy)) { - return; + {shouldShowBreakdown && ( + + {[ + {text: translate('cardTransactions.outOfPocket'), value: formattedOutOfPocketAmount}, + {text: translate('cardTransactions.companySpend'), value: formattedCompanySpendAmount}, + ].map(({text, value}) => ( + + + {text} + + + {value} + + + ))} + + )} + setIsModalVisible(false)} + shouldPreventScrollOnFocus + > + { + if (!isMobileSelectionModeEnabled) { + turnOnMobileSelectionMode(); } - - setSortConfig((prevState) => ({...prevState, sortBy: selectedSortBy, sortOrder: selectedSortOrder})); + toggleTransaction(selectedTransactionID); + setIsModalVisible(false); }} - isIOUReport={isIOUReport(report)} /> - )} - + + + ) : ( + )} - - {sortedTransactions.map((transaction) => { - return ( - - ); - })} + + + {hasComments || isLoadingReportActions ? translate('common.comments') : ''} + + {!isEmptyTransactions && ( + + {translate('common.total')} + + {convertToDisplayString(totalDisplaySpend, report?.currency)} + + + )} - {shouldShowBreakdown && ( - - {[ - {text: translate('cardTransactions.outOfPocket'), value: formattedOutOfPocketAmount}, - {text: translate('cardTransactions.companySpend'), value: formattedCompanySpendAmount}, - ].map(({text, value}) => ( - - - {text} - - - {value} - - - ))} - - )} - - setIsModalVisible(false)} - shouldPreventScrollOnFocus - > - { - if (!isMobileSelectionModeEnabled) { - turnOnMobileSelectionMode(); - } - toggleTransaction(selectedTransactionID); - setIsModalVisible(false); - }} - /> - ); } diff --git a/src/components/Search/SearchPageHeader/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader/SearchPageHeader.tsx index f89be887c298..f14f52478edc 100644 --- a/src/components/Search/SearchPageHeader/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader/SearchPageHeader.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import {View} from 'react-native'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import {useSearchContext} from '@components/Search/SearchContext'; import type {SearchQueryJSON} from '@components/Search/types'; @@ -36,10 +37,12 @@ function SearchPageHeader({ if (shouldUseNarrowLayout && isMobileSelectionModeEnabled) { return ( - + + + ); } diff --git a/src/components/SelectionList/Search/TransactionGroupListItem.tsx b/src/components/SelectionList/Search/TransactionGroupListItem.tsx index ed780b348be6..710784c08428 100644 --- a/src/components/SelectionList/Search/TransactionGroupListItem.tsx +++ b/src/components/SelectionList/Search/TransactionGroupListItem.tsx @@ -17,6 +17,7 @@ import type { import Text from '@components/Text'; import TransactionItemRow from '@components/TransactionItemRow'; import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; +import useHover from '@hooks/useHover'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -147,6 +148,7 @@ function TransactionGroupListItem({ }, [groupItem, policy, onSelectRow, onCheckboxPress, isDisabledOrEmpty, isFocused, canSelectMultiple, groupBy]); const StyleUtils = useStyleUtils(); + const {hovered, bind} = useHover(); const pressableRef = useRef(null); const onPress = useCallback(() => { @@ -162,6 +164,8 @@ function TransactionGroupListItem({ return ( ({ onButtonPress={() => { openReportInRHP(transaction); }} + isParentHovered={hovered} columnWrapperStyles={[styles.ph3, styles.pv1Half]} isReportItemChild isInSingleTransactionReport={groupItem.transactions.length === 1} diff --git a/src/components/SelectionList/Search/TransactionListItem.tsx b/src/components/SelectionList/Search/TransactionListItem.tsx index b4cd616902a5..c01de802fc42 100644 --- a/src/components/SelectionList/Search/TransactionListItem.tsx +++ b/src/components/SelectionList/Search/TransactionListItem.tsx @@ -8,6 +8,7 @@ import {useSearchContext} from '@components/Search/SearchContext'; import type {ListItem, TransactionListItemProps, TransactionListItemType} from '@components/SelectionList/types'; import TransactionItemRow from '@components/TransactionItemRow'; import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; +import useHover from '@hooks/useHover'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useSyncFocus from '@hooks/useSyncFocus'; @@ -95,6 +96,7 @@ function TransactionListItem({ }, [item, onLongPressRow]); const StyleUtils = useStyleUtils(); + const {hovered, bind} = useHover(); const pressableRef = useRef(null); useSyncFocus(pressableRef, !!isFocused, shouldSyncFocus); @@ -102,6 +104,8 @@ function TransactionListItem({ return ( ({ isFocused && StyleUtils.getItemBackgroundColorStyle(!!item.isSelected, !!isFocused, !!item.isDisabled, theme.activeComponentBG, theme.hoverComponentBG), ]} onFocus={onFocus} - wrapperStyle={[styles.mb2, styles.mh5, styles.flex1, animatedHighlightStyle, styles.userSelectNone]} + wrapperStyle={[styles.mb2, styles.mh5, animatedHighlightStyle, styles.userSelectNone]} > {!isLargeScreenWidth && ( ({ onCheckboxPress={handleCheckboxPress} shouldUseNarrowLayout={!isLargeScreenWidth} columns={columns} + isParentHovered={hovered} isActionLoading={isLoading ?? transactionItem.isActionLoading} isSelected={!!transactionItem.isSelected} dateColumnSize={dateColumnSize} diff --git a/src/components/SelectionList/Search/UserInfoAndActionButtonRow.tsx b/src/components/SelectionList/Search/UserInfoAndActionButtonRow.tsx index 14e321613725..ef12633d27a8 100644 --- a/src/components/SelectionList/Search/UserInfoAndActionButtonRow.tsx +++ b/src/components/SelectionList/Search/UserInfoAndActionButtonRow.tsx @@ -28,20 +28,21 @@ function UserInfoAndActionButtonRow({ return ( - {shouldShowUserInfo && ( - - )} + + {shouldShowUserInfo && ( + + )} + ; textStyle?: TextStyle; avatarStyle?: ViewStyle; }; -function UserInfoCell({avatar, accountID, displayName, avatarSize, containerStyle, textStyle, avatarStyle}: UserInfoCellProps) { +function UserInfoCell({avatar, accountID, displayName, avatarSize, textStyle, avatarStyle}: UserInfoCellProps) { const styles = useThemeStyles(); const {isLargeScreenWidth} = useResponsiveLayout(); @@ -29,7 +28,7 @@ function UserInfoCell({avatar, accountID, displayName, avatarSize, containerStyl } return ( - + ; avatarSize?: AvatarSizeName; infoCellsTextStyle?: TextStyle; infoCellsAvatarStyle?: ViewStyle; @@ -42,16 +40,17 @@ function UserInfoCellsWithArrow({ } return ( - - + <> + + + {shouldShowToRecipient && ( <> - + + + )} - + ); } diff --git a/src/components/TransactionItemRow/DataCells/ReceiptCell.tsx b/src/components/TransactionItemRow/DataCells/ReceiptCell.tsx index 8a38fb3c3bea..91840db16dc1 100644 --- a/src/components/TransactionItemRow/DataCells/ReceiptCell.tsx +++ b/src/components/TransactionItemRow/DataCells/ReceiptCell.tsx @@ -1,7 +1,6 @@ import {Str} from 'expensify-common'; import React from 'react'; import {View} from 'react-native'; -import type {ViewStyle} from 'react-native'; import {Receipt} from '@components/Icon/Expensicons'; import ReceiptImage from '@components/ReceiptImage'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -14,7 +13,7 @@ import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import variables from '@styles/variables'; import type {Transaction} from '@src/types/onyx'; -function ReceiptCell({transactionItem, isSelected, style}: {transactionItem: Transaction; isSelected: boolean; style?: ViewStyle}) { +function ReceiptCell({transactionItem, isSelected}: {transactionItem: Transaction; isSelected: boolean}) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -36,7 +35,6 @@ function ReceiptCell({transactionItem, isSelected, style}: {transactionItem: Tra StyleUtils.getBorderRadiusStyle(variables.componentBorderRadiusSmall), styles.overflowHidden, backgroundStyles, - style, ]} > >; onButtonPress?: () => void; + isParentHovered?: boolean; columnWrapperStyles?: ViewStyle[]; + scrollToNewTransaction?: ((offset: number) => void) | undefined; isReportItemChild?: boolean; isActionLoading?: boolean; + isInReportTableView?: boolean; isInSingleTransactionReport?: boolean; - isDisabled?: boolean; }; /** If merchant name is empty or (none), then it falls back to description if screen is narrow */ @@ -126,15 +136,21 @@ function TransactionItemRow({ shouldShowCheckbox = false, columns, onButtonPress = () => {}, + isParentHovered, columnWrapperStyles, + scrollToNewTransaction, isReportItemChild = false, isActionLoading, + isInReportTableView = false, isInSingleTransactionReport = false, - isDisabled = false, }: TransactionItemRowProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const StyleUtils = useStyleUtils(); + const theme = useTheme(); + const pendingAction = getTransactionPendingAction(transactionItem); + const isPendingDelete = isTransactionPendingDelete(transactionItem); + const viewRef = useRef(null); const hasCategoryOrTag = !isCategoryMissing(transactionItem?.category) || !!transactionItem.tag; const createdAt = getTransactionCreated(transactionItem); @@ -143,12 +159,23 @@ function TransactionItemRow({ const isAmountColumnWide = amountColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE; const isTaxAmountColumnWide = taxAmountColumnSize === CONST.SEARCH.TABLE_COLUMN_SIZES.WIDE; + const animatedHighlightStyle = useAnimatedHighlightStyle({ + shouldHighlight: transactionItem.shouldBeHighlighted ?? false, + borderRadius: variables.componentBorderRadius, + highlightColor: theme.messageHighlightBG, + backgroundColor: theme.highlightBG, + }); + + const {hovered, bind: bindHover} = useHover(); const bgActiveStyles = useMemo(() => { - if (!isSelected) { - return []; + if (isSelected) { + return styles.activeComponentBG; + } + + if (hovered || isParentHovered) { + return styles.hoveredComponentBG; } - return styles.activeComponentBG; - }, [isSelected, styles.activeComponentBG]); + }, [hovered, isParentHovered, isSelected, styles.activeComponentBG, styles.hoveredComponentBG]); const merchantOrDescriptionName = useMemo(() => getMerchantNameWithFallback(transactionItem, translate, shouldUseNarrowLayout), [shouldUseNarrowLayout, transactionItem, translate]); const missingFieldError = useMemo(() => { @@ -172,6 +199,15 @@ function TransactionItemRow({ } }, [transactionItem, translate]); + useEffect(() => { + if (!transactionItem.shouldBeHighlighted || !scrollToNewTransaction) { + return; + } + viewRef?.current?.measure((x, y, width, height, pageX, pageY) => { + scrollToNewTransaction?.(pageY); + }); + }, [scrollToNewTransaction, transactionItem.shouldBeHighlighted]); + const columnComponent: ColumnComponents = useMemo( () => ({ [CONST.REPORT.TRANSACTION_LIST.COLUMNS.TYPE]: ( @@ -350,119 +386,131 @@ function TransactionItemRow({ return columns?.includes(CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS) ?? false; }, [columns]); - if (shouldUseNarrowLayout) { - return ( - - - {shouldShowCheckbox && ( - { - onCheckboxPress(transactionItem.transactionID); - }} - accessibilityLabel={CONST.ROLE.CHECKBOX} - isChecked={isSelected} - style={styles.mr3} - wrapperStyle={styles.justifyContentCenter} - /> - )} - - - - - - - {!merchantOrDescriptionName && ( - - + + {shouldUseNarrowLayout ? ( + + + + {shouldShowCheckbox && ( + + { + onCheckboxPress(transactionItem.transactionID); + }} + accessibilityLabel={CONST.ROLE.CHECKBOX} + isChecked={isSelected} + /> + + )} + + - )} - - {!!merchantOrDescriptionName && ( - - - + + + + + + {!merchantOrDescriptionName && ( + + + + )} + + {!!merchantOrDescriptionName && ( + + + + + )} + - )} - - - - - {hasCategoryOrTag && ( - - - + + + {hasCategoryOrTag && ( + + + + + )} + + + {shouldRenderChatBubbleCell && ( + + )} - )} - - - {shouldRenderChatBubbleCell && ( - - )} - - - ); - } - - return ( - - - { - onCheckboxPress(transactionItem.transactionID); - }} - accessibilityLabel={CONST.ROLE.CHECKBOX} - isChecked={isSelected} - style={styles.mr1} - wrapperStyle={styles.justifyContentCenter} - /> - {columns?.map((column) => columnComponent[column])} - - + + + ) : ( + + + + + { + onCheckboxPress(transactionItem.transactionID); + }} + accessibilityLabel={CONST.ROLE.CHECKBOX} + isChecked={isSelected} + /> + + {columns?.map((column) => columnComponent[column])} + + + + + )} + ); } diff --git a/src/stories/TransactionItemRow.stories.tsx b/src/stories/TransactionItemRow.stories.tsx index f21d81fcee20..35e326bb987c 100644 --- a/src/stories/TransactionItemRow.stories.tsx +++ b/src/stories/TransactionItemRow.stories.tsx @@ -32,6 +32,7 @@ type TransactionItemRowProps = { shouldShowTooltip: boolean; shouldShowCheckbox: boolean; columns?: Array>; + isParentHovered?: boolean; }; const story: Meta = { @@ -61,6 +62,9 @@ const story: Meta = { shouldShowCheckbox: { control: 'boolean', }, + isParentHovered: { + control: 'boolean', + }, columns: { control: { @@ -75,7 +79,7 @@ const story: Meta = { }; function Template( - {transactionItem, shouldUseNarrowLayout, isSelected, shouldShowTooltip, shouldShowCheckbox, columns}: TransactionItemRowProps, + {transactionItem, shouldUseNarrowLayout, isSelected, shouldShowTooltip, shouldShowCheckbox, columns, isParentHovered}: TransactionItemRowProps, {parameters}: {parameters: {useLightTheme?: boolean}}, ) { const theme = parameters.useLightTheme ? CONST.THEME.LIGHT : CONST.THEME.DARK; @@ -95,6 +99,7 @@ function Template( onCheckboxPress={() => {}} shouldShowCheckbox={shouldShowCheckbox} columns={columns} + isParentHovered={isParentHovered} onButtonPress={() => {}} /> diff --git a/tests/ui/TransactionItemRowRBRTest.tsx b/tests/ui/TransactionItemRowRBRTest.tsx index a9b802c5a160..2e7a125f2011 100644 --- a/tests/ui/TransactionItemRowRBRTest.tsx +++ b/tests/ui/TransactionItemRowRBRTest.tsx @@ -38,13 +38,14 @@ const defaultProps = { }; // Helper function to render TransactionItemRow with providers -const renderTransactionItemRow = (transactionItem: TransactionWithOptionalSearchFields) => { +const renderTransactionItemRow = (transactionItem: TransactionWithOptionalSearchFields, isInReportTableView = true) => { return render( , ); @@ -291,7 +292,7 @@ describe('TransactionItemRowRBR', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${MOCK_TRANSACTION_ID}`, mockViolations); // When rendering the transaction item row - renderTransactionItemRow(mockTransaction); + renderTransactionItemRow(mockTransaction, false); await waitForBatchedUpdates(); // Then the RBR message should be displayed @@ -315,7 +316,7 @@ describe('TransactionItemRowRBR', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${MOCK_TRANSACTION_ID}`, mockViolations); // When rendering the transaction item row - renderTransactionItemRow(mockTransaction); + renderTransactionItemRow(mockTransaction, false); await waitForBatchedUpdates(); // Then the RBR message should be displayed with both violations @@ -345,7 +346,7 @@ describe('TransactionItemRowRBR', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${MOCK_TRANSACTION_ID}`, mockViolations); // When rendering the transaction item row - renderTransactionItemRow(mockTransaction); + renderTransactionItemRow(mockTransaction, false); await waitForBatchedUpdates(); // Then the RBR message should be displayed with missing merchant error and violations @@ -367,7 +368,7 @@ describe('TransactionItemRowRBR', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${MOCK_REPORT_ID}`, mockReport); // When rendering the transaction item row - renderTransactionItemRow(mockTransaction); + renderTransactionItemRow(mockTransaction, false); await waitForBatchedUpdates(); // Then the RBR message should be displayed with missing merchant error @@ -381,7 +382,7 @@ describe('TransactionItemRowRBR', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${MOCK_TRANSACTION_ID}`, []); // When rendering the transaction item row - renderTransactionItemRow(mockTransaction); + renderTransactionItemRow(mockTransaction, false); await waitForBatchedUpdates(); // Then the RBR message should not be displayed