diff --git a/src/__tests__/RecyclerView.test.tsx b/src/__tests__/RecyclerView.test.tsx index c01af4834..b3e3dcd4e 100644 --- a/src/__tests__/RecyclerView.test.tsx +++ b/src/__tests__/RecyclerView.test.tsx @@ -4,7 +4,7 @@ import "@quilted/react-testing/matchers"; import { render } from "@quilted/react-testing"; import { FlashListRef } from "../FlashListRef"; -import { FlashList } from ".."; +import { FlashList, FlashListProps } from ".."; // Mock measureLayout to return fixed dimensions jest.mock("../recyclerview/utils/measureLayout", () => { @@ -398,6 +398,70 @@ describe("RecyclerView", () => { }); }); + describe("onChangeStickyIndex previous index", () => { + const scrollTo = (root: ReturnType, y: number) => { + const scrollable = root.findWhere((node: any) => node.props.onScroll); + if (!scrollable) throw new Error("Could not find scrollable component"); + + const onScroll: any = scrollable.prop("onScroll" as never); + root.act(() => { + onScroll({ + nativeEvent: { + contentOffset: { x: 0, y }, + contentSize: { width: 399, height: 2000 }, + layoutMeasurement: { width: 399, height: 899 }, + }, + }); + }); + }; + + // Each item is 100px tall, so with stickyHeaderIndices=[0, 5, 10, 15] + // header 5 sits at y=500 and header 10 at y=1000. + const renderWithStickyHeaders = ( + stickyHeaderConfig?: FlashListProps["stickyHeaderConfig"] + ) => { + const onChangeStickyIndex = jest.fn(); + const result = render( + {item}} + stickyHeaderIndices={[0, 5, 10, 15]} + stickyHeaderConfig={stickyHeaderConfig} + onChangeStickyIndex={onChangeStickyIndex} + overrideProps={{ initialDrawBatchSize: 1 }} + drawDistance={0} + /> + ); + + return { result, onChangeStickyIndex }; + }; + + it("reports the previously stuck index with the default config", () => { + const { result, onChangeStickyIndex } = renderWithStickyHeaders(); + + scrollTo(result, 550); + expect(onChangeStickyIndex).toHaveBeenLastCalledWith(5, 0); + + scrollTo(result, 1050); + expect(onChangeStickyIndex).toHaveBeenLastCalledWith(10, 5); + }); + + it("reports the previously stuck index when hideRelatedCell is enabled", () => { + const { result, onChangeStickyIndex } = renderWithStickyHeaders({ + hideRelatedCell: true, + }); + + scrollTo(result, 550); + expect(onChangeStickyIndex).toHaveBeenLastCalledWith(5, 0); + + scrollTo(result, 1050); + expect(onChangeStickyIndex).toHaveBeenLastCalledWith(10, 5); + }); + }); + describe("autoscrolling to bottom is suppressed while offset projection is disabled", () => { const scrollTo = (root: ReturnType, y: number) => { const scrollable = root.findWhere((node: any) => node.props.onScroll); diff --git a/src/recyclerview/RecyclerView.tsx b/src/recyclerview/RecyclerView.tsx index 3c25e7e89..a636d6fc0 100644 --- a/src/recyclerview/RecyclerView.tsx +++ b/src/recyclerview/RecyclerView.tsx @@ -121,6 +121,10 @@ const RecyclerViewComponent = ( // Refs for sticky headers and scroll anchoring const stickyHeaderRef = useRef(null); const scrollAnchorRef = useRef(null); + // Last sticky index reported to `onChangeStickyIndex`. Tracked separately + // from `currentStickyIndex` because that state is only updated when + // `stickyHeaderConfig.hideRelatedCell` is enabled. + const lastReportedStickyIndexRef = useRef(-1); // State for managing layout and render updates const [_, setLayoutTreeId] = useLayoutState(0); @@ -447,7 +451,11 @@ const RecyclerViewComponent = ( if (stickyHeaderHideRelatedCell) { setCurrentStickyIndex(newStickyHeaderIndex); } - onChangeStickyIndex?.(newStickyHeaderIndex, currentStickyIndex); + onChangeStickyIndex?.( + newStickyHeaderIndex, + lastReportedStickyIndexRef.current + ); + lastReportedStickyIndexRef.current = newStickyHeaderIndex; }} /> ); @@ -462,7 +470,6 @@ const RecyclerViewComponent = ( horizontal, recyclerViewManager, extraData, - currentStickyIndex, onChangeStickyIndex, stickyHeaderHideRelatedCell, stickyHeaderZIndex,