diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index f37c1eadf350..debc3681765e 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -106,6 +106,7 @@ function MoneyRequestReportPreviewContent({ isDelegateAccessRestricted, renderTransactionItem, onLayout, + currentWidth, reportPreviewStyles, shouldDisplayContextMenu = true, isInvoice, @@ -362,29 +363,47 @@ function MoneyRequestReportPreviewContent({ thumbsUpScale.set(isApprovedAnimationRunning ? withDelay(CONST.ANIMATION_THUMBSUP_DELAY, withSpring(1, {duration: CONST.ANIMATION_THUMBSUP_DURATION})) : 1); }, [isApproved, isApprovedAnimationRunning, thumbsUpScale]); + const carouselTransactions = transactions.slice(0, 11); const [currentIndex, setCurrentIndex] = useState(0); - const [lastVisibleIndex, setLastVisibleIndex] = useState(0); + const [currentVisibleItems, setCurrentVisibleItems] = useState([0]); + const [footerWidth, setFooterWidth] = useState(0); + // optimisticIndex - value for index we are scrolling to with an arrow button or undefined after scroll is completed + // value ensures that disabled state is applied instantly and not overriden by onViewableItemsChanged when scrolling + // undefined makes arrow buttons react on currentIndex changes when scrolling manually + const [optimisticIndex, setOptimisticIndex] = useState(undefined); const carouselRef = useRef | null>(null); + const visibleItemsOnEndCount = useMemo(() => { + const lastItemWidth = transactions.length > 10 ? footerWidth : reportPreviewStyles.transactionPreviewStyle.width; + const lastItemWithGap = lastItemWidth + styles.gap2.gap; + const itemWithGap = reportPreviewStyles.transactionPreviewStyle.width + styles.gap2.gap; + return Math.floor((currentWidth - 2 * styles.pl2.paddingLeft - lastItemWithGap) / itemWithGap) + 1; + }, [transactions.length, footerWidth, reportPreviewStyles.transactionPreviewStyle.width, currentWidth, styles.pl2.paddingLeft, styles.gap2.gap]); const viewabilityConfig = useMemo(() => { - return {itemVisiblePercentThreshold: 90}; + return {itemVisiblePercentThreshold: 100}; }, []); // eslint-disable-next-line react-compiler/react-compiler const onViewableItemsChanged = useRef(({viewableItems}: {viewableItems: ViewToken[]; changed: ViewToken[]}) => { const newIndex = viewableItems.at(0)?.index; - const lastIndex = viewableItems.at(viewableItems.length - 1)?.index; if (typeof newIndex === 'number') { setCurrentIndex(newIndex); } - if (typeof lastIndex === 'number') { - setLastVisibleIndex(lastIndex); - } + const viewableItemsIndexes = viewableItems.map((item) => item.index).filter((item): item is number => item !== null); + setCurrentVisibleItems(viewableItemsIndexes); }).current; const handleChange = (index: number) => { - if (index >= transactions.length || index < 0) { + if (index > carouselTransactions.length - visibleItemsOnEndCount) { + setOptimisticIndex(carouselTransactions.length - visibleItemsOnEndCount); + carouselRef.current?.scrollToIndex({index: carouselTransactions.length - visibleItemsOnEndCount, animated: true, viewOffset: 2 * styles.gap2.gap}); + return; + } + if (index < 0) { + setOptimisticIndex(0); + carouselRef.current?.scrollToIndex({index: 0, animated: true, viewOffset: 2 * styles.gap2.gap}); return; } + setOptimisticIndex(index); carouselRef.current?.scrollToIndex({index, animated: true, viewOffset: 2 * styles.gap2.gap}); }; @@ -398,7 +417,10 @@ function MoneyRequestReportPreviewContent({ const renderFlatlistItem = (itemInfo: ListRenderItemInfo) => { if (itemInfo.index > 9) { return ( - + setFooterWidth(e.nativeEvent.layout.width)} + > +{transactions.length - 10} {translate('common.more').toLowerCase()} @@ -422,6 +444,20 @@ function MoneyRequestReportPreviewContent({ /> ); + useEffect(() => { + if ( + optimisticIndex === undefined || + optimisticIndex !== currentIndex || + // currentIndex is still the same as target (f.ex. 0), but not yet scrolled to the far left + (currentVisibleItems.at(0) !== optimisticIndex && optimisticIndex !== undefined) || + // currentIndex reached, but not scrolled to the end + (optimisticIndex === carouselTransactions.length - visibleItemsOnEndCount && currentVisibleItems.length !== visibleItemsOnEndCount) + ) { + return; + } + setOptimisticIndex(undefined); + }, [carouselTransactions.length, currentIndex, currentVisibleItems, currentVisibleItems.length, optimisticIndex, visibleItemsOnEndCount]); + const getPreviewName = () => { if (isInvoice && isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW)) { const originalMessage = getOriginalMessage(action); @@ -507,7 +543,7 @@ function MoneyRequestReportPreviewContent({ accessibilityLabel="button" style={[styles.reportPreviewArrowButton, {backgroundColor: theme.buttonDefaultBG}]} onPress={() => handleChange(currentIndex - 1)} - disabled={currentIndex === 0} + disabled={optimisticIndex !== undefined ? optimisticIndex === 0 : currentIndex === 0 && currentVisibleItems.at(0) === 0} disabledStyle={[styles.cursorDefault, styles.buttonOpacityDisabled]} > handleChange(currentIndex + 1)} - disabled={lastVisibleIndex === Math.min(transactions.length - 1, 10)} + disabled={ + optimisticIndex + ? optimisticIndex + visibleItemsOnEndCount >= carouselTransactions.length + : currentVisibleItems.at(-1) === carouselTransactions.length - 1 + } disabledStyle={[styles.cursorDefault, styles.buttonOpacityDisabled]} > {shouldUseNarrowLayout && transactions.length > 1 && ( - {transactions.slice(0, 11).map((item, index) => ( + {carouselTransactions.map((item, index) => ( { setCurrentWidth(e.nativeEvent.layout.width ?? 255); }} + currentWidth={currentWidth} reportPreviewStyles={reportPreviewStyles} shouldDisplayContextMenu={shouldDisplayContextMenu} isInvoice={isInvoice} diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/types.ts b/src/components/ReportActionItem/MoneyRequestReportPreview/types.ts index 4a429e5e8861..030ff001907f 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/types.ts +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/types.ts @@ -77,6 +77,9 @@ type MoneyRequestReportPreviewContentProps = MoneyRequestReportPreviewContentOny /** Extra styles passed used by MoneyRequestReportPreviewContent */ reportPreviewStyles: MoneyRequestReportPreviewStyleType; + /** MoneyRequestReportPreview's current width */ + currentWidth: number; + /** Callback passed to onLayout */ onLayout: (e: LayoutChangeEvent) => void;