Skip to content

Commit

Permalink
Merge pull request Expensify#49192 from ikevin127/ikevin127-searchAni…
Browse files Browse the repository at this point in the history
…mateHighlight

Search - Highlight newly added expense
  • Loading branch information
luacmartins authored Sep 30, 2024
2 parents 98ac9ac + 52c515a commit 8f5f609
Show file tree
Hide file tree
Showing 12 changed files with 293 additions and 17 deletions.
45 changes: 38 additions & 7 deletions src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
import useNetwork from '@hooks/useNetwork';
import usePrevious from '@hooks/usePrevious';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSearchHighlightAndScroll from '@hooks/useSearchHighlightAndScroll';
import useThemeStyles from '@hooks/useThemeStyles';
import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
import * as SearchActions from '@libs/actions/Search';
Expand Down Expand Up @@ -49,19 +50,26 @@ function mapTransactionItemToSelectedEntry(item: TransactionListItemType): [stri
return [item.keyForList, {isSelected: true, canDelete: item.canDelete, canHold: item.canHold, canUnhold: item.canUnhold, action: item.action}];
}

function mapToTransactionItemWithSelectionInfo(item: TransactionListItemType, selectedTransactions: SelectedTransactions, canSelectMultiple: boolean) {
return {...item, isSelected: selectedTransactions[item.keyForList]?.isSelected && canSelectMultiple};
function mapToTransactionItemWithSelectionInfo(item: TransactionListItemType, selectedTransactions: SelectedTransactions, canSelectMultiple: boolean, shouldAnimateInHighlight: boolean) {
return {...item, shouldAnimateInHighlight, isSelected: selectedTransactions[item.keyForList]?.isSelected && canSelectMultiple};
}

function mapToItemWithSelectionInfo(item: TransactionListItemType | ReportListItemType | ReportActionListItemType, selectedTransactions: SelectedTransactions, canSelectMultiple: boolean) {
function mapToItemWithSelectionInfo(
item: TransactionListItemType | ReportListItemType | ReportActionListItemType,
selectedTransactions: SelectedTransactions,
canSelectMultiple: boolean,
shouldAnimateInHighlight: boolean,
) {
if (SearchUtils.isReportActionListItemType(item)) {
return item;
}

return SearchUtils.isTransactionListItemType(item)
? mapToTransactionItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple)
? mapToTransactionItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight)
: {
...item,
transactions: item.transactions?.map((transaction) => mapToTransactionItemWithSelectionInfo(transaction, selectedTransactions, canSelectMultiple)),
shouldAnimateInHighlight,
transactions: item.transactions?.map((transaction) => mapToTransactionItemWithSelectionInfo(transaction, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight)),
isSelected: item.transactions.every((transaction) => selectedTransactions[transaction.keyForList]?.isSelected && canSelectMultiple),
};
}
Expand Down Expand Up @@ -90,6 +98,8 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
const {type, status, sortBy, sortOrder, hash} = queryJSON;

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

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

Expand Down Expand Up @@ -117,7 +127,6 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
}

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

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

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

const {newSearchResultKey, handleSelectionListScroll} = useSearchHighlightAndScroll({
searchResults,
transactions,
previousTransactions,
queryJSON,
offset,
});

// 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
// we also need to check that the searchResults matches the type and status of the current search
const isDataLoaded = searchResults?.data !== undefined && searchResults?.search?.type === type && searchResults?.search?.status === status;
Expand Down Expand Up @@ -193,7 +210,20 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr
const ListItem = SearchUtils.getListItem(type, status);
const data = SearchUtils.getSections(type, status, searchResults.data, searchResults.search);
const sortedData = SearchUtils.getSortedSections(type, status, data, sortBy, sortOrder);
const sortedSelectedData = sortedData.map((item) => mapToItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple));
const sortedSelectedData = sortedData.map((item) => {
const baseKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${(item as TransactionListItemType).transactionID}`;
// Check if the base key matches the newSearchResultKey (TransactionListItemType)
const isBaseKeyMatch = baseKey === newSearchResultKey;
// Check if any transaction within the transactions array (ReportListItemType) matches the newSearchResultKey
const isAnyTransactionMatch = (item as ReportListItemType)?.transactions?.some((transaction) => {
const transactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`;
return transactionKey === newSearchResultKey;
});
// Determine if either the base key or any transaction key matches
const shouldAnimateInHighlight = isBaseKeyMatch || isAnyTransactionMatch;

return mapToItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight);
});

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

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

return (
<SelectionListWithModal<ReportListItemType | TransactionListItemType | ReportActionListItemType>
ref={handleSelectionListScroll(sortedSelectedData)}
sections={[{data: sortedSelectedData, isDisabled: false}]}
turnOnSelectionModeOnLongPress={type !== CONST.SEARCH.DATA_TYPES.CHAT}
onTurnOnSelectionMode={(item) => item && toggleTransaction(item)}
Expand Down
11 changes: 11 additions & 0 deletions src/components/SelectionList/BaseListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
import useHover from '@hooks/useHover';
import {useMouseContext} from '@hooks/useMouseContext';
import useSyncFocus from '@hooks/useSyncFocus';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import type {BaseListItemProps, ListItem} from './types';

Expand All @@ -34,6 +36,7 @@ function BaseListItem<TItem extends ListItem>({
onFocus = () => {},
hoverStyle,
onLongPressRow,
hasAnimateInHighlightStyle = false,
}: BaseListItemProps<TItem>) {
const theme = useTheme();
const styles = useThemeStyles();
Expand Down Expand Up @@ -61,6 +64,13 @@ function BaseListItem<TItem extends ListItem>({
return rightHandSideComponent;
};

const animatedHighlightStyle = useAnimatedHighlightStyle({
borderRadius: variables.componentBorderRadius,
shouldHighlight: item?.shouldAnimateInHighlight ?? false,
highlightColor: theme.messageHighlightBG,
backgroundColor: theme.highlightBG,
});

return (
<OfflineWithFeedback
onClose={() => onDismissError(item)}
Expand Down Expand Up @@ -99,6 +109,7 @@ function BaseListItem<TItem extends ListItem>({
onFocus={onFocus}
onMouseLeave={handleMouseLeave}
tabIndex={item.tabIndex}
wrapperStyle={hasAnimateInHighlightStyle ? [styles.mh5, animatedHighlightStyle] : []}
>
<View style={wrapperStyle}>
{typeof children === 'function' ? children(hovered) : children}
Expand Down
2 changes: 1 addition & 1 deletion src/components/SelectionList/BaseSelectionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,7 @@ function BaseSelectionList<TItem extends ListItem>(
[flattenedSections.allOptions, setFocusedIndex, updateAndScrollToFocusedIndex],
);

useImperativeHandle(ref, () => ({scrollAndHighlightItem, clearInputAfterSelect}), [scrollAndHighlightItem, clearInputAfterSelect]);
useImperativeHandle(ref, () => ({scrollAndHighlightItem, clearInputAfterSelect, scrollToIndex}), [scrollAndHighlightItem, clearInputAfterSelect, scrollToIndex]);

/** Selects row when pressing Enter */
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, {
Expand Down
4 changes: 4 additions & 0 deletions src/components/SelectionList/Search/ReportListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ function ReportListItem<TItem extends ListItem>({
styles.overflowHidden,
item.isSelected && styles.activeComponentBG,
isFocused && styles.sidebarLinkActive,
// Removing some of the styles because they are added to the parent OpacityView via animatedHighlightStyle
{backgroundColor: 'unset'},
styles.mh0,
];

const handleOnButtonPress = () => {
Expand Down Expand Up @@ -140,6 +143,7 @@ function ReportListItem<TItem extends ListItem>({
onFocus={onFocus}
shouldSyncFocus={shouldSyncFocus}
hoverStyle={item.isSelected && styles.activeComponentBG}
hasAnimateInHighlightStyle
>
<View style={styles.flex1}>
{!isLargeScreenWidth && (
Expand Down
12 changes: 11 additions & 1 deletion src/components/SelectionList/Search/TransactionListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,16 @@ function TransactionListItem<TItem extends ListItem>({

const {isLargeScreenWidth} = useResponsiveLayout();

const listItemPressableStyle = [styles.selectionListPressableItemWrapper, styles.pv3, styles.ph3, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive];
const listItemPressableStyle = [
styles.selectionListPressableItemWrapper,
styles.pv3,
styles.ph3,
item.isSelected && styles.activeComponentBG,
isFocused && styles.sidebarLinkActive,
// Removing some of the styles because they are added to the parent OpacityView via animatedHighlightStyle
{backgroundColor: 'unset'},
styles.mh0,
];

const listItemWrapperStyle = [
styles.flex1,
Expand All @@ -50,6 +59,7 @@ function TransactionListItem<TItem extends ListItem>({
onLongPressRow={onLongPressRow}
shouldSyncFocus={shouldSyncFocus}
hoverStyle={item.isSelected && styles.activeComponentBG}
hasAnimateInHighlightStyle
>
<TransactionListItemRow
item={transactionItem}
Expand Down
6 changes: 6 additions & 0 deletions src/components/SelectionList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ type ListItem = {

/** The style to override the cursor appearance */
cursorStyle?: CursorStyles[keyof CursorStyles];

/** Determines whether the newly added item should animate in / highlight */
shouldAnimateInHighlight?: boolean;
};

type TransactionListItemType = ListItem &
Expand Down Expand Up @@ -288,6 +291,8 @@ type BaseListItemProps<TItem extends ListItem> = CommonListItemProps<TItem> & {
children?: ReactElement<ListItemProps<TItem>> | ((hovered: boolean) => ReactElement<ListItemProps<TItem>>);
shouldSyncFocus?: boolean;
hoverStyle?: StyleProp<ViewStyle>;
hasAnimateInHighlightStyle?: boolean;
/** Errors that this user may contain */
shouldDisplayRBR?: boolean;
};

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

type ItemLayout = {
Expand Down
17 changes: 14 additions & 3 deletions src/hooks/useAnimatedHighlightStyle/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type Props = {
borderRadius: number;

/** Height of the item that is to be faded */
height: number;
height?: number;

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

/** Whether the item should be highlighted */
shouldHighlight: boolean;

/** The base backgroundColor used for the highlight animation, defaults to theme.appBG
* @default theme.appBG
*/
backgroundColor?: string;
/** The base highlightColor used for the highlight animation, defaults to theme.border
* @default theme.border
*/
highlightColor?: string;
};

/**
Expand All @@ -47,6 +56,8 @@ export default function useAnimatedHighlightStyle({
highlightEndDelay = CONST.ANIMATED_HIGHLIGHT_END_DELAY,
highlightEndDuration = CONST.ANIMATED_HIGHLIGHT_END_DURATION,
height,
highlightColor,
backgroundColor,
}: Props) {
const [startHighlight, setStartHighlight] = useState(false);
const repeatableProgress = useSharedValue(0);
Expand All @@ -55,8 +66,8 @@ export default function useAnimatedHighlightStyle({
const theme = useTheme();

const highlightBackgroundStyle = useAnimatedStyle(() => ({
backgroundColor: interpolateColor(repeatableProgress.value, [0, 1], [theme.appBG, theme.border]),
height: interpolate(nonRepeatableProgress.value, [0, 1], [0, height]),
backgroundColor: interpolateColor(repeatableProgress.value, [0, 1], [backgroundColor ?? theme.appBG, highlightColor ?? theme.border]),
height: height ? interpolate(nonRepeatableProgress.value, [0, 1], [0, height]) : 'auto',
opacity: interpolate(nonRepeatableProgress.value, [0, 1], [0, 1]),
borderRadius,
}));
Expand Down
Loading

0 comments on commit 8f5f609

Please sign in to comment.