@@ -5,7 +5,7 @@ import React, {forwardRef, useCallback, useContext, useEffect, useImperativeHand
55import type { ForwardedRef } from 'react' ;
66import { View } from 'react-native' ;
77import type { NativeSyntheticEvent , StyleProp , ViewStyle } from 'react-native' ;
8- import Animated from 'react-native-reanimated' ;
8+ import Animated , { Easing , FadeOutUp , LinearTransition } from 'react-native-reanimated' ;
99import Checkbox from '@components/Checkbox' ;
1010import * as Expensicons from '@components/Icon/Expensicons' ;
1111import MenuItem from '@components/MenuItem' ;
@@ -25,6 +25,7 @@ import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
2525import useKeyboardState from '@hooks/useKeyboardState' ;
2626import useLocalize from '@hooks/useLocalize' ;
2727import useOnyx from '@hooks/useOnyx' ;
28+ import usePrevious from '@hooks/usePrevious' ;
2829import useResponsiveLayout from '@hooks/useResponsiveLayout' ;
2930import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings' ;
3031import useThemeStyles from '@hooks/useThemeStyles' ;
@@ -39,6 +40,8 @@ import {createItemHeightCalculator} from './itemHeightCalculator';
3940import ITEM_HEIGHTS from './itemHeights' ;
4041import type { SearchQueryJSON } from './types' ;
4142
43+ const easing = Easing . bezier ( 0.76 , 0.0 , 0.24 , 1.0 ) ;
44+
4245const AnimatedFlashListComponent = Animated . createAnimatedComponent ( FlashList < SearchListItem > ) ;
4346
4447type SearchListItem = TransactionListItemType | TransactionGroupListItemType | ReportActionListItemType | TaskListItemType ;
@@ -78,6 +81,9 @@ type SearchListProps = Pick<FlashListProps<SearchListItem>, 'onScroll' | 'conten
7881 /** Whether to prevent long press of options */
7982 shouldPreventLongPressRow ?: boolean ;
8083
84+ /** Whether to animate the items in the list */
85+ shouldAnimate ?: boolean ;
86+
8187 /** The search query */
8288 queryJSON : SearchQueryJSON ;
8389
@@ -127,6 +133,7 @@ function SearchList(
127133 queryJSON,
128134 onViewableItemsChanged,
129135 onLayout,
136+ shouldAnimate,
130137 estimatedItemSize = ITEM_HEIGHTS . NARROW_WITHOUT_DRAWER . STANDARD ,
131138 isMobileSelectionModeEnabled,
132139 } : SearchListProps ,
@@ -163,6 +170,7 @@ function SearchList(
163170 const itemFocusTimeoutRef = useRef < NodeJS . Timeout | null > ( null ) ;
164171 const { isKeyboardShown} = useKeyboardState ( ) ;
165172 const { safeAreaPaddingBottomStyle} = useSafeAreaPaddings ( ) ;
173+ const prevDataLength = usePrevious ( data . length ) ;
166174 // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout here because there is a race condition that causes shouldUseNarrowLayout to change indefinitely in this component
167175 // See https://github.com/Expensify/App/issues/48675 for more details
168176 // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
@@ -177,6 +185,7 @@ function SearchList(
177185
178186 const [ allReports ] = useOnyx ( ONYXKEYS . COLLECTION . REPORT , { canBeMissing : false } ) ;
179187
188+ const hasItemsBeingRemoved = prevDataLength && prevDataLength > data . length ;
180189 const personalDetails = usePersonalDetails ( ) ;
181190
182191 const [ userWalletTierName ] = useOnyx ( ONYXKEYS . USER_WALLET , { selector : ( wallet ) => wallet ?. tierName , canBeMissing : false } ) ;
@@ -316,62 +325,72 @@ function SearchList(
316325 const isDisabled = item . pendingAction === CONST . RED_BRICK_ROAD_PENDING_ACTION . DELETE ;
317326
318327 return (
319- < ListItem
320- showTooltip
321- isFocused = { isItemFocused }
322- onSelectRow = { onSelectRow }
323- onFocus = { ( event : NativeSyntheticEvent < ExtendedTargetedEvent > ) => {
324- // Prevent unexpected scrolling on mobile Chrome after the context menu closes by ignoring programmatic focus not triggered by direct user interaction.
325- if ( isMobileChrome ( ) && event . nativeEvent ) {
326- if ( ! event . nativeEvent . sourceCapabilities ) {
327- return ;
328- }
329- // Ignore the focus if it's caused by a touch event on mobile chrome.
330- // For example, a long press will trigger a focus event on mobile chrome
331- if ( event . nativeEvent . sourceCapabilities . firesTouchEvents ) {
332- return ;
328+ < Animated . View
329+ exiting = { shouldAnimate ? FadeOutUp . duration ( CONST . SEARCH . EXITING_ANIMATION_DURATION ) . easing ( easing ) : undefined }
330+ entering = { undefined }
331+ style = { styles . overflowHidden }
332+ layout = { shouldAnimate && hasItemsBeingRemoved ? LinearTransition . easing ( easing ) . duration ( CONST . SEARCH . EXITING_ANIMATION_DURATION ) : undefined }
333+ >
334+ < ListItem
335+ showTooltip
336+ isFocused = { isItemFocused }
337+ onSelectRow = { onSelectRow }
338+ onFocus = { ( event : NativeSyntheticEvent < ExtendedTargetedEvent > ) => {
339+ // Prevent unexpected scrolling on mobile Chrome after the context menu closes by ignoring programmatic focus not triggered by direct user interaction.
340+ if ( isMobileChrome ( ) && event . nativeEvent ) {
341+ if ( ! event . nativeEvent . sourceCapabilities ) {
342+ return ;
343+ }
344+ // Ignore the focus if it's caused by a touch event on mobile chrome.
345+ // For example, a long press will trigger a focus event on mobile chrome
346+ if ( event . nativeEvent . sourceCapabilities . firesTouchEvents ) {
347+ return ;
348+ }
333349 }
334- }
335- setFocusedIndex ( index ) ;
336- } }
337- onLongPressRow = { handleLongPressRow }
338- onCheckboxPress = { onCheckboxPress }
339- canSelectMultiple = { canSelectMultiple }
340- item = { {
341- shouldAnimateInHighlight : isItemHighlighted ,
342- ... item ,
343- } }
344- shouldPreventDefaultFocusOnSelectRow = { shouldPreventDefaultFocusOnSelectRow }
345- queryJSONHash = { hash }
346- policies = { policies }
347- isDisabled = { isDisabled }
348- allReports = { allReports }
349- groupBy = { groupBy }
350- userWalletTierName = { userWalletTierName }
351- isUserValidated = { isUserValidated }
352- personalDetails = { personalDetails }
353- userBillingFundID = { userBillingFundID }
354- / >
350+ setFocusedIndex ( index ) ;
351+ } }
352+ onLongPressRow = { handleLongPressRow }
353+ onCheckboxPress = { onCheckboxPress }
354+ canSelectMultiple = { canSelectMultiple }
355+ item = { {
356+ shouldAnimateInHighlight : isItemHighlighted ,
357+ ... item ,
358+ } }
359+ shouldPreventDefaultFocusOnSelectRow = { shouldPreventDefaultFocusOnSelectRow }
360+ queryJSONHash = { hash }
361+ policies = { policies }
362+ isDisabled = { isDisabled }
363+ allReports = { allReports }
364+ groupBy = { groupBy }
365+ userWalletTierName = { userWalletTierName }
366+ isUserValidated = { isUserValidated }
367+ personalDetails = { personalDetails }
368+ userBillingFundID = { userBillingFundID }
369+ />
370+ </ Animated . View >
355371 ) ;
356372 } ,
357373 [
358- ListItem ,
359- canSelectMultiple ,
360374 focusedIndex ,
361375 itemsToHighlight ,
376+ shouldAnimate ,
377+ styles . overflowHidden ,
378+ hasItemsBeingRemoved ,
379+ ListItem ,
380+ onSelectRow ,
362381 handleLongPressRow ,
363382 onCheckboxPress ,
364- onSelectRow ,
365- policies ,
366- hash ,
367- groupBy ,
368- setFocusedIndex ,
383+ canSelectMultiple ,
369384 shouldPreventDefaultFocusOnSelectRow ,
385+ hash ,
386+ policies ,
370387 allReports ,
388+ groupBy ,
371389 userWalletTierName ,
372390 isUserValidated ,
373391 personalDetails ,
374392 userBillingFundID ,
393+ setFocusedIndex ,
375394 ] ,
376395 ) ;
377396
0 commit comments