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;