diff --git a/android/build.gradle b/android/build.gradle
new file mode 100644
index 000000000..7d780b1c6
--- /dev/null
+++ b/android/build.gradle
@@ -0,0 +1,20 @@
+def safeExtGet(prop, fallback) {
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
+}
+
+apply plugin: "com.android.library"
+apply plugin: "org.jetbrains.kotlin.android"
+
+android {
+ namespace "com.shopify.flashlist"
+ compileSdkVersion safeExtGet("compileSdkVersion", 35)
+
+ defaultConfig {
+ minSdkVersion safeExtGet("minSdkVersion", 24)
+ targetSdkVersion safeExtGet("targetSdkVersion", 35)
+ }
+}
+
+dependencies {
+ implementation "com.facebook.react:react-android"
+}
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..94cbbcfc3
--- /dev/null
+++ b/android/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/android/src/main/java/com/shopify/flashlist/FlashListAccessibilityView.kt b/android/src/main/java/com/shopify/flashlist/FlashListAccessibilityView.kt
new file mode 100644
index 000000000..134c207c1
--- /dev/null
+++ b/android/src/main/java/com/shopify/flashlist/FlashListAccessibilityView.kt
@@ -0,0 +1,21 @@
+package com.shopify.flashlist
+
+import android.content.Context
+import android.view.View
+import com.facebook.react.views.view.ReactViewGroup
+
+internal class FlashListAccessibilityView(context: Context) : ReactViewGroup(context) {
+ var reverseAccessibilityOrder: Boolean = false
+
+ override fun addChildrenForAccessibility(outChildren: ArrayList) {
+ if (!reverseAccessibilityOrder) {
+ super.addChildrenForAccessibility(outChildren)
+ return
+ }
+
+ val children = ArrayList()
+ super.addChildrenForAccessibility(children)
+ children.reverse()
+ outChildren.addAll(children)
+ }
+}
diff --git a/android/src/main/java/com/shopify/flashlist/FlashListAccessibilityViewManager.kt b/android/src/main/java/com/shopify/flashlist/FlashListAccessibilityViewManager.kt
new file mode 100644
index 000000000..7cbdb6b9c
--- /dev/null
+++ b/android/src/main/java/com/shopify/flashlist/FlashListAccessibilityViewManager.kt
@@ -0,0 +1,24 @@
+package com.shopify.flashlist
+
+import com.facebook.react.module.annotations.ReactModule
+import com.facebook.react.uimanager.ThemedReactContext
+import com.facebook.react.uimanager.annotations.ReactProp
+import com.facebook.react.views.view.ReactViewGroup
+import com.facebook.react.views.view.ReactViewManager
+
+@ReactModule(name = FlashListAccessibilityViewManager.NAME)
+internal class FlashListAccessibilityViewManager : ReactViewManager() {
+ companion object {
+ const val NAME = "FlashListAccessibilityView"
+ }
+
+ override fun getName(): String = NAME
+
+ override fun createViewInstance(context: ThemedReactContext): ReactViewGroup =
+ FlashListAccessibilityView(context)
+
+ @ReactProp(name = "reverseAccessibilityOrder", defaultBoolean = false)
+ fun setReverseAccessibilityOrder(view: ReactViewGroup, reverse: Boolean) {
+ (view as FlashListAccessibilityView).reverseAccessibilityOrder = reverse
+ }
+}
diff --git a/android/src/main/java/com/shopify/flashlist/FlashListPackage.kt b/android/src/main/java/com/shopify/flashlist/FlashListPackage.kt
new file mode 100644
index 000000000..1c5959890
--- /dev/null
+++ b/android/src/main/java/com/shopify/flashlist/FlashListPackage.kt
@@ -0,0 +1,18 @@
+@file:Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
+
+package com.shopify.flashlist
+
+import com.facebook.react.ReactPackage
+import com.facebook.react.bridge.NativeModule
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.uimanager.ViewManager
+
+class FlashListPackage : ReactPackage {
+ override fun createNativeModules(
+ reactContext: ReactApplicationContext
+ ): List = emptyList()
+
+ override fun createViewManagers(
+ reactContext: ReactApplicationContext
+ ): List> = listOf(FlashListAccessibilityViewManager())
+}
diff --git a/package.json b/package.json
index 65da93dc4..9d08af0ca 100644
--- a/package.json
+++ b/package.json
@@ -79,6 +79,8 @@
"typescript": "5.8.3"
},
"files": [
+ "android/build.gradle",
+ "android/src",
"dist",
"src",
"jestSetup.js",
diff --git a/src/__tests__/RecyclerView.test.tsx b/src/__tests__/RecyclerView.test.tsx
index c01af4834..a3bc45b6f 100644
--- a/src/__tests__/RecyclerView.test.tsx
+++ b/src/__tests__/RecyclerView.test.tsx
@@ -36,6 +36,7 @@ const renderRecyclerView = (args: {
numColumns?: number;
masonry?: boolean;
horizontal?: boolean;
+ inverted?: boolean;
ref?: React.Ref>;
data?: number[];
}) => {
@@ -43,6 +44,7 @@ const renderRecyclerView = (args: {
numColumns = 1,
masonry = false,
horizontal = false,
+ inverted = false,
ref,
data,
} = args;
@@ -59,6 +61,7 @@ const renderRecyclerView = (args: {
drawDistance={0}
numColumns={numColumns}
horizontal={horizontal}
+ inverted={inverted}
renderItem={({ item }) => {item}}
/>
);
@@ -76,6 +79,23 @@ describe("RecyclerView", () => {
expect(result).toContainReactComponent(Text, { children: 0 });
expect(result).not.toContainReactComponent(Text, { children: 11 });
});
+
+ it("reverses native accessibility order for inverted items", () => {
+ const result = renderRecyclerView({
+ data: [0, 1, 2],
+ inverted: true,
+ });
+
+ const accessibilityContainer = result
+ .findAll(View)
+ .find(
+ (component) => component.props.reverseAccessibilityOrder !== undefined
+ );
+
+ expect(accessibilityContainer).toHaveReactProps({
+ reverseAccessibilityOrder: true,
+ });
+ });
});
describe("Sticky header config", () => {
diff --git a/src/recyclerview/ViewHolderCollection.tsx b/src/recyclerview/ViewHolderCollection.tsx
index a652beb16..4409118f8 100644
--- a/src/recyclerview/ViewHolderCollection.tsx
+++ b/src/recyclerview/ViewHolderCollection.tsx
@@ -10,7 +10,7 @@ import { FlashListProps } from "../FlashListProps";
import { ViewHolder, ViewHolderProps } from "./ViewHolder";
import { RVDimension, RVLayout } from "./layout-managers/LayoutManager";
-import { CompatView } from "./components/CompatView";
+import { AccessibilityContainer } from "./components/AccessibilityContainer";
import { useRecyclerViewContext } from "./RecyclerViewContextProvider";
/**
@@ -172,7 +172,10 @@ export const ViewHolderCollection = (
// );
return (
-
+
{containerLayout &&
hasData &&
Array.from(renderStack.entries(), ([reactKey, { index }]) => {
@@ -209,6 +212,6 @@ export const ViewHolderCollection = (
/>
);
})}
-
+
);
};
diff --git a/src/recyclerview/components/AccessibilityContainer.android.ts b/src/recyclerview/components/AccessibilityContainer.android.ts
new file mode 100644
index 000000000..6060ce18c
--- /dev/null
+++ b/src/recyclerview/components/AccessibilityContainer.android.ts
@@ -0,0 +1,19 @@
+import type { ComponentType } from "react";
+import {
+ requireNativeComponent,
+ UIManager,
+ View,
+ ViewProps,
+} from "react-native";
+
+export interface AccessibilityContainerProps extends ViewProps {
+ reverseAccessibilityOrder?: boolean;
+}
+
+const componentName = "FlashListAccessibilityView";
+
+export const AccessibilityContainer = UIManager.hasViewManagerConfig(
+ componentName
+)
+ ? requireNativeComponent(componentName)
+ : (View as ComponentType);
diff --git a/src/recyclerview/components/AccessibilityContainer.ts b/src/recyclerview/components/AccessibilityContainer.ts
new file mode 100644
index 000000000..35ef68573
--- /dev/null
+++ b/src/recyclerview/components/AccessibilityContainer.ts
@@ -0,0 +1,9 @@
+import type { ComponentType } from "react";
+import { View, ViewProps } from "react-native";
+
+export interface AccessibilityContainerProps extends ViewProps {
+ reverseAccessibilityOrder?: boolean;
+}
+
+export const AccessibilityContainer =
+ View as ComponentType;