From 3d9a009e5d1aceaca3d70d9624f3d2045dde8e82 Mon Sep 17 00:00:00 2001 From: aswin-s Date: Mon, 22 Jun 2026 12:20:19 +0530 Subject: [PATCH 1/6] Fix split highlight leaking to Inbox when splitting from Spend page Scope pendingNewTransactionIDs registration to the dismissModalWithReport path so it only runs when the expense report is actually opened. Splitting from Spend > Expenses navigates back to Search and never mounts the expense report, so the previously-registered flags were never consumed/cleared and incorrectly highlighted rows when the report was later opened from Inbox. --- .../actions/IOU/SplitTransactionUpdate.ts | 28 ++++++++++--------- tests/actions/IOU/SplitReportTotalsTest.ts | 25 +++++++++++++++++ 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/libs/actions/IOU/SplitTransactionUpdate.ts b/src/libs/actions/IOU/SplitTransactionUpdate.ts index 45b1f71b6466..27e80806a74b 100644 --- a/src/libs/actions/IOU/SplitTransactionUpdate.ts +++ b/src/libs/actions/IOU/SplitTransactionUpdate.ts @@ -1895,19 +1895,6 @@ function updateSplitTransactionsFromSplitExpensesFlow(params: UpdateSplitTransac const targetReportID = params.expenseReport?.reportID ?? String(CONST.DEFAULT_NUMBER_ID); - // Register newly created split transaction IDs so they briefly highlight in the expense list. - // We skip existing transactions (already in allChildTransactions), reverse splits (no new transactions are created), - // and the last-transaction case (the report navigates away before the highlight renders). - if (params.expenseReport?.reportID && !isReverseSplitOperation && !isLastTransactionInReport) { - const existingChildTransactionIDs = new Set(allChildTransactions.map((tx) => tx?.transactionID).filter(Boolean)); - for (const splitExpense of splitExpenses) { - if (!splitExpense.transactionID || existingChildTransactionIDs.has(splitExpense.transactionID)) { - continue; - } - addPendingNewTransactionIDs(targetReportID, splitExpense.transactionID); - } - } - if (isSearchPageTopmostFullScreenRoute || !params.transactionReport?.parentReportID) { if (!isSelfDMSplit) { Navigation.navigateBackToLastSuperWideRHPScreen(); @@ -1944,6 +1931,21 @@ function updateSplitTransactionsFromSplitExpensesFlow(params: UpdateSplitTransac return; } + // Register newly created split transaction IDs so they briefly highlight in the expense list. + // This only runs on the path that opens the expense report (dismissModalWithReport), so the highlight + // flags are consumed and cleared on mount. We skip existing transactions (already in allChildTransactions) + // and reverse splits (no new transactions are created). The Search/Spend page and last-transaction cases + // return earlier above, so they never pollute REPORT_METADATA with flags that would never be cleared. + if (params.expenseReport?.reportID && !isReverseSplitOperation && !isLastTransactionInReport) { + const existingChildTransactionIDs = new Set(allChildTransactions.map((tx) => tx?.transactionID).filter(Boolean)); + for (const splitExpense of splitExpenses) { + if (!splitExpense.transactionID || existingChildTransactionIDs.has(splitExpense.transactionID)) { + continue; + } + addPendingNewTransactionIDs(targetReportID, splitExpense.transactionID); + } + } + if (isTracking()) { setPendingSubmitFollowUpAction(CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_AND_OPEN_REPORT, targetReportID); } diff --git a/tests/actions/IOU/SplitReportTotalsTest.ts b/tests/actions/IOU/SplitReportTotalsTest.ts index 4b9ad2090c9a..8e0768448ae9 100644 --- a/tests/actions/IOU/SplitReportTotalsTest.ts +++ b/tests/actions/IOU/SplitReportTotalsTest.ts @@ -8,6 +8,7 @@ import {createSplitsAndOnyxData} from '@libs/actions/IOU/Split'; import {updateSplitTransactionsFromSplitExpensesFlow} from '@libs/actions/IOU/SplitTransactionUpdate'; import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; import isReportTopmostSplitNavigator from '@libs/Navigation/helpers/isReportTopmostSplitNavigator'; +import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import {rand64} from '@libs/NumberUtils'; import type * as PolicyUtils from '@libs/PolicyUtils'; import CONST from '@src/CONST'; @@ -873,5 +874,29 @@ describe('actions/IOU', () => { // Then nothing is registered — the list navigates away before any highlight could render expect(addPendingNewTransactionIDs).not.toHaveBeenCalled(); }); + + it('skips registration when splitting from the Search/Spend page', async () => { + // Given the user is on the Search (Spend > Expenses) page, where the expense report is never opened + jest.mocked(isSearchTopmostFullScreenRoute).mockReturnValue(true); + const params = buildBaseParams({ + transactionData: { + reportID: EXPENSE_REPORT_ID, + originalTransactionID: ORIGINAL_TX_ID, + splitExpenses: [ + {transactionID: 'new-tx-1', reportID: EXPENSE_REPORT_ID, statusNum: 0, amount: 500, created: '2024-01-01'}, + {transactionID: 'new-tx-2', reportID: EXPENSE_REPORT_ID, statusNum: 0, amount: 500, created: '2024-01-01'}, + ], + splitExpensesTotal: 1000, + }, + }); + + // When saving the split from the Search page + updateSplitTransactionsFromSplitExpensesFlow(params); + await waitForBatchedUpdates(); + + // Then nothing is registered — the report is never mounted, so the flags would never be cleared + // and would incorrectly highlight rows when the report is later opened from the Inbox. + expect(addPendingNewTransactionIDs).not.toHaveBeenCalled(); + }); }); }); From bb4556406ecf4064f892a349ec0c79ad08a67779 Mon Sep 17 00:00:00 2001 From: aswin-s Date: Wed, 1 Jul 2026 23:56:01 +0530 Subject: [PATCH 2/6] Fix split highlight not showing on Spend page in offline mode Splitting an expense from the Spend/Search page relied on the auto re-search path in useSearchHighlightAndScroll, which is skipped while offline, so the new split rows never highlighted offline. Register the new split transaction IDs via mergeTransactionIdsHighlightOnSearchRoute (TRANSACTION_IDS_HIGHLIGHT_ON_SEARCH_ROUTE) on the Search-page branch. This highlights the rows optimistically without a server re-search, so it works offline too. Reverse splits and existing children are skipped. --- .../actions/IOU/SplitTransactionUpdate.ts | 24 ++++++++++ tests/actions/IOU/SplitReportTotalsTest.ts | 48 +++++++++++++++++-- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/IOU/SplitTransactionUpdate.ts b/src/libs/actions/IOU/SplitTransactionUpdate.ts index b7a4e48af833..63326a886c91 100644 --- a/src/libs/actions/IOU/SplitTransactionUpdate.ts +++ b/src/libs/actions/IOU/SplitTransactionUpdate.ts @@ -41,9 +41,11 @@ import { navigateBackOnDeleteTransaction, updateOptimisticParentReportAction, } from '@libs/ReportUtils'; +import {getCurrentSearchQueryJSON} from '@libs/SearchQueryUtils'; import {isTracking, setPendingSubmitFollowUpAction} from '@libs/telemetry/submitFollowUpAction'; import {getChildTransactions, isDistanceRequest as isDistanceRequestTransactionUtils, isOnHold, isPerDiemRequest as isPerDiemRequestTransactionUtils} from '@libs/TransactionUtils'; import {setDeleteTransactionNavigateBackUrl} from '@userActions/Report'; +import {mergeTransactionIdsHighlightOnSearchRoute} from '@userActions/Transaction'; import {removeDraftSplitTransaction} from '@userActions/TransactionEdit'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; @@ -1902,6 +1904,28 @@ function updateSplitTransactionsFromSplitExpensesFlow(params: UpdateSplitTransac const targetReportID = params.expenseReport?.reportID ?? String(CONST.DEFAULT_NUMBER_ID); if (isSearchPageTopmostFullScreenRoute || !params.transactionReport?.parentReportID) { + // Register newly created split transaction IDs so they briefly highlight on the Search/Spend page. + // The Search page reads TRANSACTION_IDS_HIGHLIGHT_ON_SEARCH_ROUTE, which highlights matching rows + // optimistically without waiting for a server re-search. Unlike the auto-detect path in + // useSearchHighlightAndScroll (skipped while offline), this makes the highlight work offline too. + // Reverse splits create no new transactions, and existing children are already in the list, so both are skipped. + if (isSearchPageTopmostFullScreenRoute && !isReverseSplitOperation) { + const currentSearchType = getCurrentSearchQueryJSON()?.type; + if (currentSearchType) { + const existingChildTransactionIDs = new Set(allChildTransactions.map((tx) => tx?.transactionID).filter(Boolean)); + const newTransactionIDsToHighlight: Record = {}; + for (const splitExpense of splitExpenses) { + if (!splitExpense.transactionID || existingChildTransactionIDs.has(splitExpense.transactionID)) { + continue; + } + newTransactionIDsToHighlight[splitExpense.transactionID] = true; + } + if (!isEmptyObject(newTransactionIDsToHighlight)) { + mergeTransactionIdsHighlightOnSearchRoute(currentSearchType, newTransactionIDsToHighlight); + } + } + } + if (!isSelfDMSplit) { Navigation.navigateBackToLastSuperWideRHPScreen(); } diff --git a/tests/actions/IOU/SplitReportTotalsTest.ts b/tests/actions/IOU/SplitReportTotalsTest.ts index 368d05eef5e4..076aa8e4af1a 100644 --- a/tests/actions/IOU/SplitReportTotalsTest.ts +++ b/tests/actions/IOU/SplitReportTotalsTest.ts @@ -876,9 +876,10 @@ describe('actions/IOU', () => { expect(addPendingNewTransactionIDs).not.toHaveBeenCalled(); }); - it('skips registration when splitting from the Search/Spend page', async () => { + it('registers the search-route highlight (not report metadata) when splitting from the Search/Spend page', async () => { // Given the user is on the Search (Spend > Expenses) page, where the expense report is never opened jest.mocked(isSearchTopmostFullScreenRoute).mockReturnValue(true); + const spyOnMergeTransactionIdsHighlightOnSearchRoute = jest.spyOn(require('@libs/actions/Transaction'), 'mergeTransactionIdsHighlightOnSearchRoute'); const params = buildBaseParams({ transactionData: { reportID: EXPENSE_REPORT_ID, @@ -895,9 +896,50 @@ describe('actions/IOU', () => { updateSplitTransactionsFromSplitExpensesFlow(params); await waitForBatchedUpdates(); - // Then nothing is registered — the report is never mounted, so the flags would never be cleared - // and would incorrectly highlight rows when the report is later opened from the Inbox. + // Then the report-metadata highlight is skipped — the report is never mounted, so those flags would + // never be cleared and would incorrectly highlight rows when the report is later opened from the Inbox. expect(addPendingNewTransactionIDs).not.toHaveBeenCalled(); + + // And instead the new IDs are registered on the search-route highlight, keyed by the current search type. + // This mechanism highlights optimistically without a server re-search, so it works offline too. + expect(spyOnMergeTransactionIdsHighlightOnSearchRoute).toHaveBeenCalledWith( + 'expense', + Object.fromEntries([ + ['new-tx-1', true], + ['new-tx-2', true], + ]), + ); + + spyOnMergeTransactionIdsHighlightOnSearchRoute.mockRestore(); + }); + + it('skips the search-route highlight during a reverse split from the Search/Spend page', async () => { + // Given the user is on the Search page and this is a reverse split (1 expense, existing child present) + jest.mocked(isSearchTopmostFullScreenRoute).mockReturnValue(true); + const spyOnMergeTransactionIdsHighlightOnSearchRoute = jest.spyOn(require('@libs/actions/Transaction'), 'mergeTransactionIdsHighlightOnSearchRoute'); + const existingChildTx = { + transactionID: 'child-tx-1', + reportID: EXPENSE_REPORT_ID, + comment: {originalTransactionID: ORIGINAL_TX_ID, source: CONST.IOU.TYPE.SPLIT}, + }; + const params = buildBaseParams({ + allTransactionsList: {[`${ONYXKEYS.COLLECTION.TRANSACTION}child-tx-1`]: existingChildTx}, + transactionData: { + reportID: EXPENSE_REPORT_ID, + originalTransactionID: ORIGINAL_TX_ID, + splitExpenses: [{transactionID: 'new-merged-tx', reportID: EXPENSE_REPORT_ID, statusNum: 0, amount: 1000, created: '2024-01-01'}], + splitExpensesTotal: 1000, + }, + }); + + // When saving the reverse split + updateSplitTransactionsFromSplitExpensesFlow(params); + await waitForBatchedUpdates(); + + // Then nothing is highlighted — reverse splits create no new transactions + expect(spyOnMergeTransactionIdsHighlightOnSearchRoute).not.toHaveBeenCalled(); + + spyOnMergeTransactionIdsHighlightOnSearchRoute.mockRestore(); }); }); }); From 221512586ab72b3861ebf14ce6d219dc427ae14c Mon Sep 17 00:00:00 2001 From: aswin-s Date: Thu, 9 Jul 2026 16:45:05 +0530 Subject: [PATCH 3/6] refactor: extract getNewSplitTransactionIDs and registerSearchRouteHighlight helpers Addresses review feedback: dedupe the new-split-ID filtering into a shared getNewSplitTransactionIDs helper reused by both the search-route and report-metadata highlight paths, and flatten the deeply nested search-branch highlight into registerSearchRouteHighlight with early returns. --- .../actions/IOU/SplitTransactionUpdate.ts | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/src/libs/actions/IOU/SplitTransactionUpdate.ts b/src/libs/actions/IOU/SplitTransactionUpdate.ts index f80b6668729c..0e3fc4679074 100644 --- a/src/libs/actions/IOU/SplitTransactionUpdate.ts +++ b/src/libs/actions/IOU/SplitTransactionUpdate.ts @@ -1857,6 +1857,12 @@ function updateSplitTransactionsFromSplitExpensesFlow(params: UpdateSplitTransac const hasEditableSplitExpensesLeft = splitExpenses.some((expense) => (expense.statusNum ?? 0) < CONST.REPORT.STATUS_NUM.SUBMITTED); const isReverseSplitOperation = splitExpenses.length === 1 && originalChildTransactions.length > 0 && hasEditableSplitExpensesLeft && allChildTransactions.length === originalChildTransactions.length; + + // Newly created split transaction IDs, excluding ones already present in allChildTransactions. + function getNewSplitTransactionIDs(): string[] { + const existingChildTransactionIDs = new Set(allChildTransactions.map((tx) => tx?.transactionID).filter(Boolean)); + return splitExpenses.map((splitExpense) => splitExpense.transactionID).filter((transactionID) => transactionID && !existingChildTransactionIDs.has(transactionID)); + } const expenseReportID = params.expenseReport?.reportID; // Detect whether the expense report the user is editing from will be emptied by this save. @@ -1921,28 +1927,31 @@ function updateSplitTransactionsFromSplitExpensesFlow(params: UpdateSplitTransac const targetReportID = params.expenseReport?.reportID ?? String(CONST.DEFAULT_NUMBER_ID); - if (isSearchPageTopmostFullScreenRoute || !params.transactionReport?.parentReportID) { - // Register newly created split transaction IDs so they briefly highlight on the Search/Spend page. - // The Search page reads TRANSACTION_IDS_HIGHLIGHT_ON_SEARCH_ROUTE, which highlights matching rows - // optimistically without waiting for a server re-search. Unlike the auto-detect path in - // useSearchHighlightAndScroll (skipped while offline), this makes the highlight work offline too. - // Reverse splits create no new transactions, and existing children are already in the list, so both are skipped. - if (isSearchPageTopmostFullScreenRoute && !isReverseSplitOperation) { - const currentSearchType = getCurrentSearchQueryJSON()?.type; - if (currentSearchType) { - const existingChildTransactionIDs = new Set(allChildTransactions.map((tx) => tx?.transactionID).filter(Boolean)); - const newTransactionIDsToHighlight: Record = {}; - for (const splitExpense of splitExpenses) { - if (!splitExpense.transactionID || existingChildTransactionIDs.has(splitExpense.transactionID)) { - continue; - } - newTransactionIDsToHighlight[splitExpense.transactionID] = true; - } - if (!isEmptyObject(newTransactionIDsToHighlight)) { - mergeTransactionIdsHighlightOnSearchRoute(currentSearchType, newTransactionIDsToHighlight); - } - } + // Register newly created split transaction IDs so they briefly highlight on the Search/Spend page. + // The Search page reads TRANSACTION_IDS_HIGHLIGHT_ON_SEARCH_ROUTE, which highlights matching rows + // optimistically without waiting for a server re-search. Unlike the auto-detect path in + // useSearchHighlightAndScroll (skipped while offline), this makes the highlight work offline too. + // Reverse splits create no new transactions, and existing children are already in the list, so both are skipped. + function registerSearchRouteHighlight() { + if (!isSearchPageTopmostFullScreenRoute || isReverseSplitOperation) { + return; + } + const currentSearchType = getCurrentSearchQueryJSON()?.type; + if (!currentSearchType) { + return; + } + const newTransactionIDsToHighlight: Record = {}; + for (const transactionID of getNewSplitTransactionIDs()) { + newTransactionIDsToHighlight[transactionID] = true; } + if (isEmptyObject(newTransactionIDsToHighlight)) { + return; + } + mergeTransactionIdsHighlightOnSearchRoute(currentSearchType, newTransactionIDsToHighlight); + } + + if (isSearchPageTopmostFullScreenRoute || !params.transactionReport?.parentReportID) { + registerSearchRouteHighlight(); if (!isSelfDMSplit) { Navigation.navigateBackToLastSuperWideRHPScreen(); @@ -1985,12 +1994,8 @@ function updateSplitTransactionsFromSplitExpensesFlow(params: UpdateSplitTransac // and reverse splits (no new transactions are created). The Search/Spend page and last-transaction cases // return earlier above, so they never pollute REPORT_METADATA with flags that would never be cleared. if (params.expenseReport?.reportID && !isReverseSplitOperation && !isLastTransactionInReport) { - const existingChildTransactionIDs = new Set(allChildTransactions.map((tx) => tx?.transactionID).filter(Boolean)); - for (const splitExpense of splitExpenses) { - if (!splitExpense.transactionID || existingChildTransactionIDs.has(splitExpense.transactionID)) { - continue; - } - addPendingNewTransactionIDs(targetReportID, splitExpense.transactionID); + for (const transactionID of getNewSplitTransactionIDs()) { + addPendingNewTransactionIDs(targetReportID, transactionID); } } From 4e6a393c5fa8719f0a8590cd7f24e02ce69bb27a Mon Sep 17 00:00:00 2001 From: aswin-s Date: Sat, 18 Jul 2026 19:16:30 +0530 Subject: [PATCH 4/6] Fix split expense highlight hidden by focus background on Search page After splitting an expense, the newly-created row that receives focus on the Search/Spend page never appeared highlighted on the mobile (narrow) layout. The highlight animation renders on the row wrapper, but a focused row paints an opaque background on the pressable in front of it, covering the highlight. Suppress the opaque focus background for the duration of the post-create highlight animation so the highlight is visible on the focused row too. --- .../TransactionListItemNarrow.tsx | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx b/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx index 9781c015bda0..843df933b9b4 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx @@ -13,11 +13,13 @@ import useSyncFocus from '@hooks/useSyncFocus'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import durationHighlightItem from '@libs/Navigation/helpers/getDurationHighlightItem'; + import CONST from '@src/CONST'; import type {View} from 'react-native'; -import React, {useRef} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import type {TransactionListItemNarrowProps} from './types'; @@ -76,6 +78,31 @@ function TransactionListItemNarrow({ shouldApplyOtherStyles: true, }); + // The highlight animation is applied to the row wrapper, which sits behind this pressable. A focused + // row paints an opaque background on the pressable itself, which would cover the highlight - so after + // splitting an expense the newly-created row that receives focus never appears highlighted. Suppress + // the opaque focus background for the full highlight animation so the highlight shows. shouldAnimateInHighlight + // only stays true for the brief queue window, so latch it for durationHighlightItem. + const shouldAnimateInHighlight = !!item?.shouldAnimateInHighlight; + const [isHighlighting, setIsHighlighting] = useState(false); + const [wasAnimatingHighlight, setWasAnimatingHighlight] = useState(shouldAnimateInHighlight); + // Start the latch during render (React's "storing information from previous renders" pattern) to avoid + // calling setState synchronously inside an effect. The effect below only clears it via an async timer. + if (shouldAnimateInHighlight !== wasAnimatingHighlight) { + setWasAnimatingHighlight(shouldAnimateInHighlight); + if (shouldAnimateInHighlight) { + setIsHighlighting(true); + } + } + useEffect(() => { + if (!isHighlighting) { + return; + } + const timer = setTimeout(() => setIsHighlighting(false), durationHighlightItem); + return () => clearTimeout(timer); + }, [isHighlighting]); + const shouldShowFocusBackground = !!isFocused && !isHighlighting; + return ( ({ sentryLabel={CONST.SENTRY_LABEL.SEARCH.TRANSACTION_LIST_ITEM} style={[ pressableStyle, - isFocused && StyleUtils.getItemBackgroundColorStyle(isSelected, !!isFocused, !!item.isDisabled, theme.activeComponentBG, theme.hoverComponentBG), + shouldShowFocusBackground && StyleUtils.getItemBackgroundColorStyle(isSelected, !!isFocused, !!item.isDisabled, theme.activeComponentBG, theme.hoverComponentBG), isDeletedTransaction && styles.cursorDefault, ]} onFocus={onFocus} From 38346609560910c8bef8614219a3ee118f28f361 Mon Sep 17 00:00:00 2001 From: aswin-s Date: Sat, 18 Jul 2026 20:17:38 +0530 Subject: [PATCH 5/6] Latch split-row highlight on first mount Initialize the highlight latch from item.shouldAnimateInHighlight so a row that mounts already flagged for highlight suppresses the focus background immediately, instead of only when the flag transitions during a later render. Prevents the opaque focus background from covering the highlight on first mount. --- .../TransactionListItem/TransactionListItemNarrow.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx b/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx index 843df933b9b4..6904e02bbea7 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx @@ -84,7 +84,9 @@ function TransactionListItemNarrow({ // the opaque focus background for the full highlight animation so the highlight shows. shouldAnimateInHighlight // only stays true for the brief queue window, so latch it for durationHighlightItem. const shouldAnimateInHighlight = !!item?.shouldAnimateInHighlight; - const [isHighlighting, setIsHighlighting] = useState(false); + // Initialize from the prop so a row that mounts already flagged (the split/search highlight case this + // fixes) latches immediately - otherwise the render-time guard below never fires on first mount. + const [isHighlighting, setIsHighlighting] = useState(shouldAnimateInHighlight); const [wasAnimatingHighlight, setWasAnimatingHighlight] = useState(shouldAnimateInHighlight); // Start the latch during render (React's "storing information from previous renders" pattern) to avoid // calling setState synchronously inside an effect. The effect below only clears it via an async timer. From 38ecf474d553abd650b05ed17b5a8be47a339a4d Mon Sep 17 00:00:00 2001 From: Aswin S Date: Fri, 24 Jul 2026 11:47:01 +0530 Subject: [PATCH 6/6] Update src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- .../ListItem/TransactionListItem/TransactionListItemNarrow.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx b/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx index 6904e02bbea7..9981a9360482 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx @@ -84,10 +84,12 @@ function TransactionListItemNarrow({ // the opaque focus background for the full highlight animation so the highlight shows. shouldAnimateInHighlight // only stays true for the brief queue window, so latch it for durationHighlightItem. const shouldAnimateInHighlight = !!item?.shouldAnimateInHighlight; + // Initialize from the prop so a row that mounts already flagged (the split/search highlight case this // fixes) latches immediately - otherwise the render-time guard below never fires on first mount. const [isHighlighting, setIsHighlighting] = useState(shouldAnimateInHighlight); const [wasAnimatingHighlight, setWasAnimatingHighlight] = useState(shouldAnimateInHighlight); + // Start the latch during render (React's "storing information from previous renders" pattern) to avoid // calling setState synchronously inside an effect. The effect below only clears it via an async timer. if (shouldAnimateInHighlight !== wasAnimatingHighlight) {