diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 9238488361b0..a13b816fd8b8 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -466,6 +466,7 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr } contentContainerStyle={[contentContainerStyle, styles.pb3]} scrollEventThrottle={1} + shouldKeepFocusedItemAtTopOfViewableArea={type === CONST.SEARCH.DATA_TYPES.CHAT} /> ); } diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 3e1b3a3c2d70..bf8aa5454caa 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -107,6 +107,7 @@ function BaseSelectionList( scrollEventThrottle, contentContainerStyle, shouldHighlightSelectedItem = false, + shouldKeepFocusedItemAtTopOfViewableArea = false, }: BaseSelectionListProps, ref: ForwardedRef, ) { @@ -126,6 +127,20 @@ function BaseSelectionList( const [currentPage, setCurrentPage] = useState(1); const isTextInputFocusedRef = useRef(false); const {singleExecution} = useSingleExecution(); + const [itemHeights, setItemHeights] = useState>({}); + + const onItemLayout = (event: LayoutChangeEvent, itemKey: string | null | undefined) => { + if (!itemKey) { + return; + } + + const {height} = event.nativeEvent.layout; + + setItemHeights((prevHeights) => ({ + ...prevHeights, + [itemKey]: height, + })); + }; const incrementPage = () => setCurrentPage((prev) => prev + 1); @@ -151,7 +166,7 @@ function BaseSelectionList( const selectedOptions: TItem[] = []; sections.forEach((section, sectionIndex) => { - const sectionHeaderHeight = variables.optionsListSectionHeaderHeight; + const sectionHeaderHeight = !!section.title || !!section.CustomSectionHeader ? variables.optionsListSectionHeaderHeight : 0; itemLayouts.push({length: sectionHeaderHeight, offset}); offset += sectionHeaderHeight; @@ -175,7 +190,7 @@ function BaseSelectionList( disabledIndex += 1; // Account for the height of the item in getItemLayout - const fullItemHeight = getItemHeight(item); + const fullItemHeight = item?.keyForList && itemHeights[item.keyForList] ? itemHeights[item.keyForList] : getItemHeight(item); itemLayouts.push({length: fullItemHeight, offset}); offset += fullItemHeight; @@ -207,7 +222,7 @@ function BaseSelectionList( itemLayouts, allSelected: selectedOptions.length > 0 && selectedOptions.length === allOptions.length - disabledOptionsIndexes.length, }; - }, [canSelectMultiple, sections, customListHeader, customListHeaderHeight, getItemHeight]); + }, [canSelectMultiple, sections, customListHeader, customListHeaderHeight, itemHeights, getItemHeight]); const [slicedSections, ShowMoreButtonInstance] = useMemo(() => { let remainingOptionsLimit = CONST.MAX_SELECTION_LIST_PAGE_LENGTH * currentPage; @@ -257,8 +272,20 @@ function BaseSelectionList( const itemIndex = item.index ?? -1; const sectionIndex = item.sectionIndex ?? -1; + let viewOffsetToKeepFocusedItemAtTopOfViewableArea = 0; + + // Since there are always two items above the focused item in viewable area, and items can grow beyond the screen size + // in searchType chat, the focused item may move out of view. To prevent this, we will ensure that the focused item remains at + // the top of the viewable area at all times by adjusting the viewOffset. + if (shouldKeepFocusedItemAtTopOfViewableArea) { + const firstPreviousItem = index > 0 ? flattenedSections.allOptions.at(index - 1) : undefined; + const firstPreviousItemHeight = firstPreviousItem && firstPreviousItem.keyForList ? itemHeights[firstPreviousItem.keyForList] : 0; + const secondPreviousItem = index > 1 ? flattenedSections.allOptions.at(index - 2) : undefined; + const secondPreviousItemHeight = secondPreviousItem && secondPreviousItem?.keyForList ? itemHeights[secondPreviousItem.keyForList] : 0; + viewOffsetToKeepFocusedItemAtTopOfViewableArea = firstPreviousItemHeight + secondPreviousItemHeight; + } - listRef.current.scrollToLocation({sectionIndex, itemIndex, animated, viewOffset: variables.contentHeaderHeight}); + listRef.current.scrollToLocation({sectionIndex, itemIndex, animated, viewOffset: variables.contentHeaderHeight - viewOffsetToKeepFocusedItemAtTopOfViewableArea}); }, // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps @@ -450,7 +477,7 @@ function BaseSelectionList( }; return ( - <> + onItemLayout(event, item?.keyForList)}> ( wrapperStyle={listItemWrapperStyle} /> {item.footerContent && item.footerContent} - + ); }; diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 8fb50456182c..a534ba4a1623 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -597,6 +597,9 @@ type BaseSelectionListProps = Partial & { /** Whether we highlight all the selected items */ shouldHighlightSelectedItem?: boolean; + + /** Determines if the focused item should remain at the top of the viewable area when navigating with arrow keys */ + shouldKeepFocusedItemAtTopOfViewableArea?: boolean; } & TRightHandSideComponent; type SelectionListHandle = {