diff --git a/src/__tests__/InvertedSeparator.test.tsx b/src/__tests__/InvertedSeparator.test.tsx new file mode 100644 index 000000000..4cdfdc08d --- /dev/null +++ b/src/__tests__/InvertedSeparator.test.tsx @@ -0,0 +1,95 @@ +import React from "react"; +import { Text, View } from "react-native"; +import "@quilted/react-testing/matchers"; +import { render } from "@quilted/react-testing"; + +import { FlashList } from ".."; + +jest.mock("../recyclerview/utils/measureLayout", () => { + const originalModule = jest.requireActual( + "../recyclerview/utils/measureLayout" + ); + return { + ...originalModule, + measureParentSize: jest.fn().mockImplementation(() => ({ + width: 399, + height: 899, + })), + measureFirstChildLayout: jest.fn().mockImplementation(() => ({ + x: 0, + y: 0, + width: 399, + height: 899, + })), + measureItemLayout: jest.fn().mockImplementation(() => ({ + x: 0, + y: 0, + width: 100, + height: 100, + })), + }; +}); + +const Separator = () => ; + +/** + * Returns, for every rendered cell, whether the separator sits before or after + * the item inside the cell. Cells are keyed by their item index. + */ +const separatorPlacementByIndex = (result: ReturnType) => { + const placement = new Map(); + for (const cell of result.findAllWhere( + (node: any) => node.type === View && typeof node.props.index === "number" + )) { + const index = (cell as any).props.index as number; + if (placement.has(index)) continue; + const childTypes = React.Children.toArray((cell as any).props.children).map( + (child: any) => child.type + ); + const itemAt = childTypes.indexOf(Text); + const separatorAt = childTypes.indexOf(Separator); + placement.set( + index, + separatorAt === -1 ? "none" : separatorAt < itemAt ? "before" : "after" + ); + } + return placement; +}; + +const renderList = (inverted: boolean) => + render( + {`item-${item}`}} + ItemSeparatorComponent={Separator} + /> + ); + +describe("ItemSeparatorComponent placement", () => { + it("renders the separator after the item in a regular list", () => { + expect([...separatorPlacementByIndex(renderList(false)).entries()]).toEqual( + [ + [0, "after"], + [1, "after"], + [2, "after"], + [3, "none"], + ] + ); + }); + + it("renders the separator before the item in an inverted list", () => { + // Both the scroller and each cell are flipped, so a trailing separator + // would visually land between item i and item i - 1, leaving the two + // items at the far end of the list flush against each other. + expect([...separatorPlacementByIndex(renderList(true)).entries()]).toEqual([ + [0, "before"], + [1, "before"], + [2, "before"], + [3, "none"], + ]); + }); +}); diff --git a/src/recyclerview/ViewHolder.tsx b/src/recyclerview/ViewHolder.tsx index 54b840301..1f468bea2 100644 --- a/src/recyclerview/ViewHolder.tsx +++ b/src/recyclerview/ViewHolder.tsx @@ -139,8 +139,13 @@ const ViewHolderInternal = (props: ViewHolderProps) => { style={style} index={index} > + {/* In an inverted list the scroller and every cell are both flipped, so a + cell's content reads normally while its neighbours are mirrored: the + item at index + 1 ends up on the cell's leading side. Render the + separator before the item so it still lands between the two. */} + {inverted ? separator : null} {children} - {separator} + {inverted ? null : separator} ); };