Skip to content

Commit 8f5f609

Browse files
authored
Merge pull request #49192 from ikevin127/ikevin127-searchAnimateHighlight
Search - Highlight newly added expense
2 parents 98ac9ac + 52c515a commit 8f5f609

File tree

12 files changed

+293
-17
lines changed

12 files changed

+293
-17
lines changed

src/components/Search/index.tsx

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
1414
import useNetwork from '@hooks/useNetwork';
1515
import usePrevious from '@hooks/usePrevious';
1616
import useResponsiveLayout from '@hooks/useResponsiveLayout';
17+
import useSearchHighlightAndScroll from '@hooks/useSearchHighlightAndScroll';
1718
import useThemeStyles from '@hooks/useThemeStyles';
1819
import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
1920
import * as SearchActions from '@libs/actions/Search';
@@ -49,19 +50,26 @@ function mapTransactionItemToSelectedEntry(item: TransactionListItemType): [stri
4950
return [item.keyForList, {isSelected: true, canDelete: item.canDelete, canHold: item.canHold, canUnhold: item.canUnhold, action: item.action}];
5051
}
5152

52-
function mapToTransactionItemWithSelectionInfo(item: TransactionListItemType, selectedTransactions: SelectedTransactions, canSelectMultiple: boolean) {
53-
return {...item, isSelected: selectedTransactions[item.keyForList]?.isSelected && canSelectMultiple};
53+
function mapToTransactionItemWithSelectionInfo(item: TransactionListItemType, selectedTransactions: SelectedTransactions, canSelectMultiple: boolean, shouldAnimateInHighlight: boolean) {
54+
return {...item, shouldAnimateInHighlight, isSelected: selectedTransactions[item.keyForList]?.isSelected && canSelectMultiple};
5455
}
5556

56-
function mapToItemWithSelectionInfo(item: TransactionListItemType | ReportListItemType | ReportActionListItemType, selectedTransactions: SelectedTransactions, canSelectMultiple: boolean) {
57+
function mapToItemWithSelectionInfo(
58+
item: TransactionListItemType | ReportListItemType | ReportActionListItemType,
59+
selectedTransactions: SelectedTransactions,
60+
canSelectMultiple: boolean,
61+
shouldAnimateInHighlight: boolean,
62+
) {
5763
if (SearchUtils.isReportActionListItemType(item)) {
5864
return item;
5965
}
66+
6067
return SearchUtils.isTransactionListItemType(item)
61-
? mapToTransactionItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple)
68+
? mapToTransactionItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight)
6269
: {
6370
...item,
64-
transactions: item.transactions?.map((transaction) => mapToTransactionItemWithSelectionInfo(transaction, selectedTransactions, canSelectMultiple)),
71+
shouldAnimateInHighlight,
72+
transactions: item.transactions?.map((transaction) => mapToTransactionItemWithSelectionInfo(transaction, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight)),
6573
isSelected: item.transactions.every((transaction) => selectedTransactions[transaction.keyForList]?.isSelected && canSelectMultiple),
6674
};
6775
}
@@ -90,6 +98,8 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
9098
const {type, status, sortBy, sortOrder, hash} = queryJSON;
9199

92100
const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`);
101+
const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
102+
const previousTransactions = usePrevious(transactions);
93103

94104
const canSelectMultiple = isSmallScreenWidth ? !!selectionMode?.isEnabled : true;
95105

@@ -117,7 +127,6 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
117127
}
118128

119129
SearchActions.search({queryJSON, offset});
120-
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
121130
}, [isOffline, offset, queryJSON]);
122131

123132
const getItemHeight = useCallback(
@@ -156,6 +165,14 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
156165

157166
const searchResults = currentSearchResults?.data ? currentSearchResults : lastSearchResultsRef.current;
158167

168+
const {newSearchResultKey, handleSelectionListScroll} = useSearchHighlightAndScroll({
169+
searchResults,
170+
transactions,
171+
previousTransactions,
172+
queryJSON,
173+
offset,
174+
});
175+
159176
// There's a race condition in Onyx which makes it return data from the previous Search, so in addition to checking that the data is loaded
160177
// we also need to check that the searchResults matches the type and status of the current search
161178
const isDataLoaded = searchResults?.data !== undefined && searchResults?.search?.type === type && searchResults?.search?.status === status;
@@ -193,7 +210,20 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
193210
const ListItem = SearchUtils.getListItem(type, status);
194211
const data = SearchUtils.getSections(type, status, searchResults.data, searchResults.search);
195212
const sortedData = SearchUtils.getSortedSections(type, status, data, sortBy, sortOrder);
196-
const sortedSelectedData = sortedData.map((item) => mapToItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple));
213+
const sortedSelectedData = sortedData.map((item) => {
214+
const baseKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${(item as TransactionListItemType).transactionID}`;
215+
// Check if the base key matches the newSearchResultKey (TransactionListItemType)
216+
const isBaseKeyMatch = baseKey === newSearchResultKey;
217+
// Check if any transaction within the transactions array (ReportListItemType) matches the newSearchResultKey
218+
const isAnyTransactionMatch = (item as ReportListItemType)?.transactions?.some((transaction) => {
219+
const transactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`;
220+
return transactionKey === newSearchResultKey;
221+
});
222+
// Determine if either the base key or any transaction key matches
223+
const shouldAnimateInHighlight = isBaseKeyMatch || isAnyTransactionMatch;
224+
225+
return mapToItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight);
226+
});
197227

198228
const shouldShowEmptyState = !isDataLoaded || data.length === 0;
199229

@@ -299,6 +329,7 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
299329

300330
return (
301331
<SelectionListWithModal<ReportListItemType | TransactionListItemType | ReportActionListItemType>
332+
ref={handleSelectionListScroll(sortedSelectedData)}
302333
sections={[{data: sortedSelectedData, isDisabled: false}]}
303334
turnOnSelectionModeOnLongPress={type !== CONST.SEARCH.DATA_TYPES.CHAT}
304335
onTurnOnSelectionMode={(item) => item && toggleTransaction(item)}

src/components/SelectionList/BaseListItem.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import Icon from '@components/Icon';
44
import * as Expensicons from '@components/Icon/Expensicons';
55
import OfflineWithFeedback from '@components/OfflineWithFeedback';
66
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
7+
import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
78
import useHover from '@hooks/useHover';
89
import {useMouseContext} from '@hooks/useMouseContext';
910
import useSyncFocus from '@hooks/useSyncFocus';
1011
import useTheme from '@hooks/useTheme';
1112
import useThemeStyles from '@hooks/useThemeStyles';
13+
import variables from '@styles/variables';
1214
import CONST from '@src/CONST';
1315
import type {BaseListItemProps, ListItem} from './types';
1416

@@ -34,6 +36,7 @@ function BaseListItem<TItem extends ListItem>({
3436
onFocus = () => {},
3537
hoverStyle,
3638
onLongPressRow,
39+
hasAnimateInHighlightStyle = false,
3740
}: BaseListItemProps<TItem>) {
3841
const theme = useTheme();
3942
const styles = useThemeStyles();
@@ -61,6 +64,13 @@ function BaseListItem<TItem extends ListItem>({
6164
return rightHandSideComponent;
6265
};
6366

67+
const animatedHighlightStyle = useAnimatedHighlightStyle({
68+
borderRadius: variables.componentBorderRadius,
69+
shouldHighlight: item?.shouldAnimateInHighlight ?? false,
70+
highlightColor: theme.messageHighlightBG,
71+
backgroundColor: theme.highlightBG,
72+
});
73+
6474
return (
6575
<OfflineWithFeedback
6676
onClose={() => onDismissError(item)}
@@ -99,6 +109,7 @@ function BaseListItem<TItem extends ListItem>({
99109
onFocus={onFocus}
100110
onMouseLeave={handleMouseLeave}
101111
tabIndex={item.tabIndex}
112+
wrapperStyle={hasAnimateInHighlightStyle ? [styles.mh5, animatedHighlightStyle] : []}
102113
>
103114
<View style={wrapperStyle}>
104115
{typeof children === 'function' ? children(hovered) : children}

src/components/SelectionList/BaseSelectionList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,7 @@ function BaseSelectionList<TItem extends ListItem>(
619619
[flattenedSections.allOptions, setFocusedIndex, updateAndScrollToFocusedIndex],
620620
);
621621

622-
useImperativeHandle(ref, () => ({scrollAndHighlightItem, clearInputAfterSelect}), [scrollAndHighlightItem, clearInputAfterSelect]);
622+
useImperativeHandle(ref, () => ({scrollAndHighlightItem, clearInputAfterSelect, scrollToIndex}), [scrollAndHighlightItem, clearInputAfterSelect, scrollToIndex]);
623623

624624
/** Selects row when pressing Enter */
625625
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, {

src/components/SelectionList/Search/ReportListItem.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ function ReportListItem<TItem extends ListItem>({
7777
styles.overflowHidden,
7878
item.isSelected && styles.activeComponentBG,
7979
isFocused && styles.sidebarLinkActive,
80+
// Removing some of the styles because they are added to the parent OpacityView via animatedHighlightStyle
81+
{backgroundColor: 'unset'},
82+
styles.mh0,
8083
];
8184

8285
const handleOnButtonPress = () => {
@@ -140,6 +143,7 @@ function ReportListItem<TItem extends ListItem>({
140143
onFocus={onFocus}
141144
shouldSyncFocus={shouldSyncFocus}
142145
hoverStyle={item.isSelected && styles.activeComponentBG}
146+
hasAnimateInHighlightStyle
143147
>
144148
<View style={styles.flex1}>
145149
{!isLargeScreenWidth && (

src/components/SelectionList/Search/TransactionListItem.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,16 @@ function TransactionListItem<TItem extends ListItem>({
2323

2424
const {isLargeScreenWidth} = useResponsiveLayout();
2525

26-
const listItemPressableStyle = [styles.selectionListPressableItemWrapper, styles.pv3, styles.ph3, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive];
26+
const listItemPressableStyle = [
27+
styles.selectionListPressableItemWrapper,
28+
styles.pv3,
29+
styles.ph3,
30+
item.isSelected && styles.activeComponentBG,
31+
isFocused && styles.sidebarLinkActive,
32+
// Removing some of the styles because they are added to the parent OpacityView via animatedHighlightStyle
33+
{backgroundColor: 'unset'},
34+
styles.mh0,
35+
];
2736

2837
const listItemWrapperStyle = [
2938
styles.flex1,
@@ -50,6 +59,7 @@ function TransactionListItem<TItem extends ListItem>({
5059
onLongPressRow={onLongPressRow}
5160
shouldSyncFocus={shouldSyncFocus}
5261
hoverStyle={item.isSelected && styles.activeComponentBG}
62+
hasAnimateInHighlightStyle
5363
>
5464
<TransactionListItemRow
5565
item={transactionItem}

src/components/SelectionList/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ type ListItem = {
175175

176176
/** The style to override the cursor appearance */
177177
cursorStyle?: CursorStyles[keyof CursorStyles];
178+
179+
/** Determines whether the newly added item should animate in / highlight */
180+
shouldAnimateInHighlight?: boolean;
178181
};
179182

180183
type TransactionListItemType = ListItem &
@@ -288,6 +291,8 @@ type BaseListItemProps<TItem extends ListItem> = CommonListItemProps<TItem> & {
288291
children?: ReactElement<ListItemProps<TItem>> | ((hovered: boolean) => ReactElement<ListItemProps<TItem>>);
289292
shouldSyncFocus?: boolean;
290293
hoverStyle?: StyleProp<ViewStyle>;
294+
hasAnimateInHighlightStyle?: boolean;
295+
/** Errors that this user may contain */
291296
shouldDisplayRBR?: boolean;
292297
};
293298

@@ -565,6 +570,7 @@ type BaseSelectionListProps<TItem extends ListItem> = Partial<ChildrenProps> & {
565570
type SelectionListHandle = {
566571
scrollAndHighlightItem?: (items: string[], timeout: number) => void;
567572
clearInputAfterSelect?: () => void;
573+
scrollToIndex: (index: number, animated?: boolean) => void;
568574
};
569575

570576
type ItemLayout = {

src/hooks/useAnimatedHighlightStyle/index.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ type Props = {
1010
borderRadius: number;
1111

1212
/** Height of the item that is to be faded */
13-
height: number;
13+
height?: number;
1414

1515
/** Delay before the highlighted item enters */
1616
itemEnterDelay?: number;
@@ -32,6 +32,15 @@ type Props = {
3232

3333
/** Whether the item should be highlighted */
3434
shouldHighlight: boolean;
35+
36+
/** The base backgroundColor used for the highlight animation, defaults to theme.appBG
37+
* @default theme.appBG
38+
*/
39+
backgroundColor?: string;
40+
/** The base highlightColor used for the highlight animation, defaults to theme.border
41+
* @default theme.border
42+
*/
43+
highlightColor?: string;
3544
};
3645

3746
/**
@@ -47,6 +56,8 @@ export default function useAnimatedHighlightStyle({
4756
highlightEndDelay = CONST.ANIMATED_HIGHLIGHT_END_DELAY,
4857
highlightEndDuration = CONST.ANIMATED_HIGHLIGHT_END_DURATION,
4958
height,
59+
highlightColor,
60+
backgroundColor,
5061
}: Props) {
5162
const [startHighlight, setStartHighlight] = useState(false);
5263
const repeatableProgress = useSharedValue(0);
@@ -55,8 +66,8 @@ export default function useAnimatedHighlightStyle({
5566
const theme = useTheme();
5667

5768
const highlightBackgroundStyle = useAnimatedStyle(() => ({
58-
backgroundColor: interpolateColor(repeatableProgress.value, [0, 1], [theme.appBG, theme.border]),
59-
height: interpolate(nonRepeatableProgress.value, [0, 1], [0, height]),
69+
backgroundColor: interpolateColor(repeatableProgress.value, [0, 1], [backgroundColor ?? theme.appBG, highlightColor ?? theme.border]),
70+
height: height ? interpolate(nonRepeatableProgress.value, [0, 1], [0, height]) : 'auto',
6071
opacity: interpolate(nonRepeatableProgress.value, [0, 1], [0, 1]),
6172
borderRadius,
6273
}));

0 commit comments

Comments
 (0)