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}
);
};