diff --git a/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js b/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js index 05f629e01a1d1e..aaaeda859feaf9 100644 --- a/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js +++ b/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js @@ -36,6 +36,7 @@ const AndroidHorizontalScrollViewNativeComponent: HostComponent = NativeC snapToStart: true, snapToOffsets: true, contentOffset: true, + accessibilityCollectionInfo: true, }, }), ); diff --git a/Libraries/Lists/FlatList.js b/Libraries/Lists/FlatList.js index b9008a028a78ca..9bac80e1c2acce 100644 --- a/Libraries/Lists/FlatList.js +++ b/Libraries/Lists/FlatList.js @@ -606,11 +606,29 @@ class FlatList extends React.PureComponent, void> { return ( {item.map((it, kk) => { - const element = renderer({ - item: it, - index: index * numColumns + kk, - separators: info.separators, - }); + const accessibilityCollectionItemInfo = { + rowIndex: index, + rowSpan: 1, + columnIndex: (index * numColumns + kk) % numColumns, + columnSpan: 1, + heading: false, + itemIndex: index * numColumns + kk, + }; + + const element = ( + + {renderer({ + item: it, + index: index * numColumns + kk, + separators: info.separators, + })} + + ); return element != null ? ( {element} ) : null; @@ -618,12 +636,41 @@ class FlatList extends React.PureComponent, void> { ); } else { - return renderer(info); + const {index} = info; + + const accessibilityCollectionItemInfo = { + rowIndex: index, + rowSpan: 1, + columnIndex: 0, + columnSpan: 1, + heading: false, + itemIndex: index, + }; + + return ( + + {renderer(info)} + + ); } }, }; }; + _getAccessibilityCollectionInfo = () => { + const accessibilityCollectionProps = { + itemCount: this.props.data ? this.props.data.length : 0, + rowCount: this._getItemCount(this.props.data), + columnCount: this.props.numColumns, + hierarchical: false, + }; + + return accessibilityCollectionProps; + }; + render(): React.Node { const {numColumns, columnWrapperStyle, ...restProps} = this.props; @@ -633,6 +680,10 @@ class FlatList extends React.PureComponent, void> { getItem={this._getItem} getItemCount={this._getItemCount} keyExtractor={this._keyExtractor} + accessibilityCollectionInfo={this._getAccessibilityCollectionInfo()} + accessibilityRole={Platform.select({ + android: this.props.numColumns > 1 ? 'grid' : 'list', + })} ref={this._captureRef} viewabilityConfigCallbackPairs={this._virtualizedListPairs} {...this._renderer()} @@ -643,6 +694,7 @@ class FlatList extends React.PureComponent, void> { const styles = StyleSheet.create({ row: {flexDirection: 'row'}, + cellStyle: {flex: 1}, }); module.exports = FlatList; diff --git a/Libraries/Lists/VirtualizedList.js b/Libraries/Lists/VirtualizedList.js index 976b22705e5df3..605ff026c2c03d 100644 --- a/Libraries/Lists/VirtualizedList.js +++ b/Libraries/Lists/VirtualizedList.js @@ -2066,8 +2066,8 @@ class CellRenderer extends React.Component< : inversionStyle; const result = !CellRendererComponent ? ( /* $FlowFixMe[incompatible-type-arg] (>=0.89.0 site=react_native_fb) * - This comment suppresses an error found when Flow v0.89 was deployed. * - To see the error, delete this comment and run Flow. */ + This comment suppresses an error found when Flow v0.89 was deployed. * + To see the error, delete this comment and run Flow. */ {element} {itemSeparator} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index 25579c4c292f6b..bcb6edb023d411 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -151,6 +151,7 @@ public void setAccessibilityHint(@NonNull T view, @Nullable String accessibility updateViewContentDescription(view); } + @Override @ReactProp(name = ViewProps.ACCESSIBILITY_ROLE) public void setAccessibilityRole(@NonNull T view, @Nullable String accessibilityRole) { @@ -160,6 +161,19 @@ public void setAccessibilityRole(@NonNull T view, @Nullable String accessibility view.setTag(R.id.accessibility_role, AccessibilityRole.fromValue(accessibilityRole)); } + + @Override + @ReactProp(name = ViewProps.ACCESSIBILITY_COLLECTION_INFO) + public void setAccessibilityCollectionInfo(@NonNull T view, @Nullable ReadableMap accessibilityCollectionInfo) { + view.setTag(R.id.accessibility_collection_info, accessibilityCollectionInfo); + } + + @Override + @ReactProp(name = ViewProps.ACCESSIBILITY_COLLECTION_ITEM_INFO) + public void setAccessibilityCollectionItemInfo(@NonNull T view, @Nullable ReadableMap accessibilityCollectionItemInfo) { + view.setTag(R.id.accessibility_collection_item_info, accessibilityCollectionItemInfo); + } + @Override @ReactProp(name = ViewProps.ACCESSIBILITY_STATE) public void setViewState(@NonNull T view, @Nullable ReadableMap accessibilityState) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerAdapter.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerAdapter.java index c0e21dece66f2a..e202a91fe5d7bc 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerAdapter.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerAdapter.java @@ -30,6 +30,12 @@ public void setAccessibilityLiveRegion(@NonNull T view, @Nullable String liveReg @Override public void setAccessibilityRole(@NonNull T view, @Nullable String accessibilityRole) {} + @Override + public void setAccessibilityCollectionInfo(@NonNull T view, @Nullable ReadableMap accessibilityCollectionInfo) {} + + @Override + public void setAccessibilityCollectionItemInfo(@NonNull T view, @Nullable ReadableMap accessibilityCollectionItemInfo) {} + @Override public void setViewState(@NonNull T view, @Nullable ReadableMap accessibilityState) {} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java index 78c83a38611ab3..bca92f774b85ac 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java @@ -8,12 +8,14 @@ package com.facebook.react.uimanager; import android.content.Context; +import android.graphics.Rect; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.text.SpannableString; import android.text.style.URLSpan; import android.view.View; +import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import androidx.annotation.Nullable; import androidx.core.view.AccessibilityDelegateCompat; @@ -107,6 +109,8 @@ public enum AccessibilityRole { TAB, TABLIST, TIMER, + LIST, + GRID, TOOLBAR; public static String getValue(AccessibilityRole role) { @@ -135,6 +139,10 @@ public static String getValue(AccessibilityRole role) { return "android.widget.SpinButton"; case SWITCH: return "android.widget.Switch"; + case LIST: + return "android.widget.AbsListView"; + case GRID: + return "android.widget.GridView"; case NONE: case LINK: case SUMMARY: @@ -204,6 +212,20 @@ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCo } final ReadableArray accessibilityActions = (ReadableArray) host.getTag(R.id.accessibility_actions); + + final ReadableMap accessibilityCollectionItemInfo = + (ReadableMap) host.getTag(R.id.accessibility_collection_item_info); + if (accessibilityCollectionItemInfo != null) { + int rowIndex = accessibilityCollectionItemInfo.getInt("rowIndex"); + int columnIndex = accessibilityCollectionItemInfo.getInt("columnIndex"); + int rowSpan = accessibilityCollectionItemInfo.getInt("rowSpan"); + int columnSpan = accessibilityCollectionItemInfo.getInt("columnSpan"); + boolean heading = accessibilityCollectionItemInfo.getBoolean("heading"); + + AccessibilityNodeInfoCompat.CollectionItemInfoCompat collectionItemInfoCompat = AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(rowIndex, rowSpan, columnIndex, columnSpan, heading); + info.setCollectionItemInfo(collectionItemInfoCompat); + } + if (accessibilityActions != null) { for (int i = 0; i < accessibilityActions.size(); i++) { final ReadableMap action = accessibilityActions.getMap(i); @@ -259,12 +281,14 @@ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCo } } + @Override public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { super.onInitializeAccessibilityEvent(host, event); // Set item count and current item index on accessibility events for adjustable // in order to make Talkback announce the value of the adjustable final ReadableMap accessibilityValue = (ReadableMap) host.getTag(R.id.accessibility_value); + if (accessibilityValue != null && accessibilityValue.hasKey("min") && accessibilityValue.hasKey("now") @@ -438,7 +462,8 @@ public static void setDelegate(final View view) { && (view.getTag(R.id.accessibility_role) != null || view.getTag(R.id.accessibility_state) != null || view.getTag(R.id.accessibility_actions) != null - || view.getTag(R.id.react_test_id) != null)) { + || view.getTag(R.id.react_test_id) != null + || view.getTag(R.id.accessibility_collection_item_info) != null)) { ViewCompat.setAccessibilityDelegate(view, new ReactAccessibilityDelegate()); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/interfaces/BaseViewManagerDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/interfaces/BaseViewManagerDelegate.java index 1598b578877b2f..452bde73552e61 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/interfaces/BaseViewManagerDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/interfaces/BaseViewManagerDelegate.java @@ -47,6 +47,12 @@ public void setProperty(T view, String propName, @Nullable Object value) { case ViewProps.ACCESSIBILITY_STATE: mViewManager.setViewState(view, (ReadableMap) value); break; + case ViewProps.ACCESSIBILITY_COLLECTION_INFO: + mViewManager.setAccessibilityCollectionInfo(view, (ReadableMap) value); + break; + case ViewProps.ACCESSIBILITY_COLLECTION_ITEM_INFO: + mViewManager.setAccessibilityCollectionItemInfo(view, (ReadableMap) value); + break; case ViewProps.BACKGROUND_COLOR: mViewManager.setBackgroundColor( view, value == null ? 0 : ColorPropConverter.getColor(value, view.getContext())); diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/interfaces/BaseViewManagerInterface.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/interfaces/BaseViewManagerInterface.java index 5423eeedd91a71..e20654208398d7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/interfaces/BaseViewManagerInterface.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/interfaces/BaseViewManagerInterface.java @@ -26,6 +26,10 @@ public interface BaseViewManagerInterface { void setAccessibilityLiveRegion(T view, @Nullable String liveRegion); void setAccessibilityRole(T view, @Nullable String accessibilityRole); + + void setAccessibilityCollectionInfo(T view, @Nullable ReadableMap accessibilityCollectionInfo); + + void setAccessibilityCollectionItemInfo(T view, @Nullable ReadableMap accessibilityCollectionItemInfo); void setViewState(T view, @Nullable ReadableMap accessibilityState); diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/interfaces/ViewProps.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/interfaces/ViewProps.java index 2327b364980dae..9cec3cafdd6e25 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/interfaces/ViewProps.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/interfaces/ViewProps.java @@ -145,6 +145,8 @@ public class ViewProps { public static final String Z_INDEX = "zIndex"; public static final String RENDER_TO_HARDWARE_TEXTURE = "renderToHardwareTextureAndroid"; public static final String ACCESSIBILITY_LABEL = "accessibilityLabel"; + public static final String ACCESSIBILITY_COLLECTION_INFO = "accessibilityCollectionInfo"; + public static final String ACCESSIBILITY_COLLECTION_ITEM_INFO = "accessibilityCollectionItemInfo"; public static final String ACCESSIBILITY_HINT = "accessibilityHint"; public static final String ACCESSIBILITY_LIVE_REGION = "accessibilityLiveRegion"; public static final String ACCESSIBILITY_ROLE = "accessibilityRole"; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java index 0304f0e06783e3..d0d1939cd4c578 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java @@ -21,6 +21,7 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.widget.HorizontalScrollView; import android.widget.OverScroller; @@ -30,6 +31,8 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; +import com.facebook.react.R; +import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.common.ReactConstants; @@ -38,6 +41,7 @@ import com.facebook.react.uimanager.FabricViewStateManager; import com.facebook.react.uimanager.MeasureSpecAssertions; import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.ReactAccessibilityDelegate; import com.facebook.react.uimanager.ReactClippingViewGroup; import com.facebook.react.uimanager.ReactClippingViewGroupHelper; import com.facebook.react.uimanager.ViewProps; @@ -122,6 +126,55 @@ public ReactHorizontalScrollView(Context context, @Nullable FpsListener fpsListe public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { super.onInitializeAccessibilityEvent(host, event); event.setScrollable(mScrollEnabled); + final ReadableMap accessibilityCollectionInfo = (ReadableMap) host.getTag(R.id.accessibility_collection_info); + + if (accessibilityCollectionInfo != null) { + event.setItemCount(accessibilityCollectionInfo.getInt("itemCount")); + View contentView = getContentView(); + Integer firstVisibleIndex = null; + Integer lastVisibleIndex = null; + + if (!(contentView instanceof ViewGroup)) { + return; + } + + for(int index = 0; index < ((ViewGroup) contentView).getChildCount(); index++) { + View nextChild = ((ViewGroup) contentView).getChildAt(index); + boolean isVisible = isPartiallyScrolledInView(nextChild); + + ReadableMap accessibilityCollectionItemInfo = (ReadableMap) nextChild.getTag(R.id.accessibility_collection_item_info); + + if (!(nextChild instanceof ViewGroup)) { + return; + } + + int childCount = ((ViewGroup) nextChild).getChildCount(); + + // If this child's accessibilityCollectionItemInfo is null, we'll check one more nested child. + // Happens when getItemLayout is not passed in FlatList which adds an additional View in the hierarchy. + if (childCount > 0 && accessibilityCollectionItemInfo == null) { + View nestedNextChild = ((ViewGroup) nextChild).getChildAt(0); + if (nestedNextChild != null) { + ReadableMap nestedChildAccessibilityInfo = (ReadableMap) nestedNextChild.getTag(R.id.accessibility_collection_item_info); + if (nestedChildAccessibilityInfo != null) { + accessibilityCollectionItemInfo = nestedChildAccessibilityInfo; + } + } + } + + if (isVisible == true && accessibilityCollectionItemInfo != null) { + if(firstVisibleIndex == null) { + firstVisibleIndex = accessibilityCollectionItemInfo.getInt("itemIndex"); + } + lastVisibleIndex = accessibilityCollectionItemInfo.getInt("itemIndex");; + } + + if (firstVisibleIndex != null && lastVisibleIndex != null) { + event.setFromIndex(firstVisibleIndex); + event.setToIndex(lastVisibleIndex); + } + } + } } @Override @@ -129,6 +182,23 @@ public void onInitializeAccessibilityNodeInfo( View host, AccessibilityNodeInfoCompat info) { super.onInitializeAccessibilityNodeInfo(host, info); info.setScrollable(mScrollEnabled); + final ReactAccessibilityDelegate.AccessibilityRole accessibilityRole = + (ReactAccessibilityDelegate.AccessibilityRole) host.getTag(R.id.accessibility_role); + + if (accessibilityRole != null) { + ReactAccessibilityDelegate.setRole(info, accessibilityRole, host.getContext()); + } + + final ReadableMap accessibilityCollectionInfo = (ReadableMap) host.getTag(R.id.accessibility_collection_info); + + if (accessibilityCollectionInfo != null) { + int rowCount = accessibilityCollectionInfo.getInt("rowCount"); + int columnCount = accessibilityCollectionInfo.getInt("columnCount"); + boolean hierarchical = accessibilityCollectionInfo.getBoolean("hierarchical"); + + AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfoCompat = AccessibilityNodeInfoCompat.CollectionInfoCompat.obtain(rowCount, columnCount, hierarchical); + info.setCollectionInfo(collectionInfoCompat); + } } }); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java index 05112f45e449ad..16481172c3aa73 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java @@ -9,8 +9,10 @@ import android.graphics.Color; import android.util.DisplayMetrics; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; +import com.facebook.react.R; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.module.annotations.ReactModule; @@ -164,6 +166,11 @@ public void setPagingEnabled(ReactHorizontalScrollView view, boolean pagingEnabl view.setPagingEnabled(pagingEnabled); } + @ReactProp(name = ViewProps.ACCESSIBILITY_COLLECTION_INFO) + public void setAccessibilityCollectionInfo(ReactHorizontalScrollView view, @Nullable ReadableMap accessibilityCollectionInfo) { + view.setTag(R.id.accessibility_collection_info, accessibilityCollectionInfo); + } + /** Controls overScroll behaviour */ @ReactProp(name = "overScrollMode") public void setOverScrollMode(ReactHorizontalScrollView view, String value) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java index 6a2273b989960b..d2f9bfac20b1b3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java @@ -20,13 +20,18 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; import android.widget.OverScroller; import android.widget.ScrollView; import androidx.annotation.Nullable; +import androidx.core.view.AccessibilityDelegateCompat; import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Assertions; +import com.facebook.react.R; import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.common.ReactConstants; @@ -34,6 +39,7 @@ import com.facebook.react.uimanager.FabricViewStateManager; import com.facebook.react.uimanager.MeasureSpecAssertions; import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.ReactAccessibilityDelegate; import com.facebook.react.uimanager.ReactClippingViewGroup; import com.facebook.react.uimanager.ReactClippingViewGroupHelper; import com.facebook.react.uimanager.ViewProps; @@ -69,6 +75,7 @@ public class ReactScrollView extends ScrollView private final @Nullable OverScroller mScroller; private final VelocityHelper mVelocityHelper = new VelocityHelper(); private final Rect mRect = new Rect(); // for reuse to avoid allocation + private final Rect mTempRect = new Rect(); private boolean mActivelyScrolling; private @Nullable Rect mClippingRect; @@ -109,6 +116,12 @@ public ReactScrollView(ReactContext context) { this(context, null); } + + private View getContentView() { + View contentView = getChildAt(0); + return contentView; + } + public ReactScrollView(ReactContext context, @Nullable FpsListener fpsListener) { super(context); mFpsListener = fpsListener; @@ -117,6 +130,93 @@ public ReactScrollView(ReactContext context, @Nullable FpsListener fpsListener) mScroller = getOverScrollerFromParent(); setOnHierarchyChangeListener(this); setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY); + + ViewCompat.setAccessibilityDelegate( + this, + new AccessibilityDelegateCompat() { + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + event.setScrollable(mScrollEnabled); + final ReadableMap accessibilityCollectionInfo = (ReadableMap) host.getTag(R.id.accessibility_collection_info); + + if (accessibilityCollectionInfo != null) { + event.setItemCount(accessibilityCollectionInfo.getInt("itemCount")); + View contentView = getContentView(); + Integer firstVisibleIndex = null; + Integer lastVisibleIndex = null; + + if (!(contentView instanceof ViewGroup)) { + return; + } + + for(int index = 0; index < ((ViewGroup) contentView).getChildCount(); index++) { + View nextChild = ((ViewGroup) contentView).getChildAt(index); + boolean isVisible = isPartiallyScrolledInView(nextChild); + + ReadableMap accessibilityCollectionItemInfo = (ReadableMap) nextChild.getTag(R.id.accessibility_collection_item_info); + + if (!(nextChild instanceof ViewGroup)) { + return; + } + + int childCount = ((ViewGroup) nextChild).getChildCount(); + + // If this child's accessibilityCollectionItemInfo is null, we'll check one more nested child. + // Happens when getItemLayout is not passed in FlatList which adds an additional View in the hierarchy. + if (childCount > 0 && accessibilityCollectionItemInfo == null) { + View nestedNextChild = ((ViewGroup) nextChild).getChildAt(0); + if (nestedNextChild != null) { + ReadableMap nestedChildAccessibilityInfo = (ReadableMap) nestedNextChild.getTag(R.id.accessibility_collection_item_info); + if (nestedChildAccessibilityInfo != null) { + accessibilityCollectionItemInfo = nestedChildAccessibilityInfo; + } + } + } + + if (isVisible == true && accessibilityCollectionItemInfo != null) { + if(firstVisibleIndex == null) { + firstVisibleIndex = accessibilityCollectionItemInfo.getInt("itemIndex"); + } + lastVisibleIndex = accessibilityCollectionItemInfo.getInt("itemIndex");; + } + + if (firstVisibleIndex != null && lastVisibleIndex != null) { + event.setFromIndex(firstVisibleIndex); + event.setToIndex(lastVisibleIndex); + } + } + } + } + + @Override + public void onInitializeAccessibilityNodeInfo( + View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + + final ReactAccessibilityDelegate.AccessibilityRole accessibilityRole = + (ReactAccessibilityDelegate.AccessibilityRole) host.getTag(R.id.accessibility_role); + + if (accessibilityRole != null) { + ReactAccessibilityDelegate.setRole(info, accessibilityRole, host.getContext()); + } + + final ReadableMap accessibilityCollectionInfo = (ReadableMap) host.getTag(R.id.accessibility_collection_info); + + if (accessibilityCollectionInfo != null) { + int rowCount = accessibilityCollectionInfo.getInt("rowCount"); + int columnCount = accessibilityCollectionInfo.getInt("columnCount"); + boolean hierarchical = accessibilityCollectionInfo.getBoolean("hierarchical"); + + AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfoCompat = AccessibilityNodeInfoCompat.CollectionInfoCompat.obtain(rowCount, columnCount, hierarchical); + info.setCollectionInfo(collectionInfoCompat); + } + + info.setScrollable(mScrollEnabled); + } + }); + } @Nullable @@ -263,6 +363,19 @@ public void requestChildFocus(View child, View focused) { super.requestChildFocus(child, focused); } + private int getScrollDelta(View descendent) { + descendent.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(descendent, mTempRect); + return computeScrollDeltaToGetChildRectOnScreen(mTempRect); + } + + /** Returns whether the given descendent is partially scrolled in view */ + private boolean isPartiallyScrolledInView(View descendent) { + int scrollDelta = getScrollDelta(descendent); + descendent.getDrawingRect(mTempRect); + return scrollDelta != 0 && Math.abs(scrollDelta) < mTempRect.width(); + } + private void scrollToChild(View child) { Rect tempRect = new Rect(); child.getDrawingRect(tempRect); diff --git a/ReactAndroid/src/main/res/views/uimanager/values/ids.xml b/ReactAndroid/src/main/res/views/uimanager/values/ids.xml index 6886defd469257..37ab9d1796ec1d 100644 --- a/ReactAndroid/src/main/res/views/uimanager/values/ids.xml +++ b/ReactAndroid/src/main/res/views/uimanager/values/ids.xml @@ -15,7 +15,13 @@ - + + + + + + + diff --git a/packages/rn-tester/js/examples/FlatList/FlatList-nested.js b/packages/rn-tester/js/examples/FlatList/FlatList-nested.js new file mode 100644 index 00000000000000..a27a859de28151 --- /dev/null +++ b/packages/rn-tester/js/examples/FlatList/FlatList-nested.js @@ -0,0 +1,109 @@ +import React from "react"; +import { + SafeAreaView, + View, + FlatList, + StyleSheet, + Text, + StatusBar, +} from "react-native"; + +const DATA = [ + { + id: "bd7acbea-c1b1-46c2-aed5-3ad53abb28ba", + title: "First Item", + }, + { + id: "3ac68afc-c605-48d3-a4f8-fbd91aa97f63", + title: "Second Item", + }, + { + id: "58694a0f-3da1-471f-bd96-145571e29d72", + title: "Third Item", + }, + { + id: "bd7acbea-c1b1-46c2-aed5-3ad53abb8bbb", + title: "Fourth Item", + }, + { + id: "3ac68afc-c605-48d3-a4f8-fbd91aa97676", + title: "Fifth Item", + }, + { + id: "58694a0f-3da1-471f-bd96-145571e27234", + title: "Sixth Item", + }, + { + id: "58694a0f-3da1-471f-bd96-145571e29234", + title: "Seven Item", + }, + { + id: "58694a0f-3da1-471f-bd96-145571429234", + title: "Eight Item", + }, + { + id: "58694a0f-3da1-471f-bd96-115571429234", + title: "Nine Item", + }, + { + id: "58694a0f-3da1-471f-bd96-1155h1429234", + title: "Ten Item", + }, +]; + +const Item = ({ title }) => ( + + {title} + +); + +const renderItem = ({ item }) => ; + +const renderFlatList = ({ item }) => ( + + Flatlist {item} + + +); + +const FlatListNested = () => { + return ( + + item.toString()} + /> + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + marginTop: StatusBar.currentHeight || 0, + }, + item: { + backgroundColor: "#f9c2ff", + padding: 20, + marginVertical: 8, + marginHorizontal: 16, + }, + title: { + fontSize: 16, + }, +}); + +exports.title = "FlatList Nested"; +exports.testTitle = "Test accessibility announcement in nested flatlist"; +exports.category = "ListView"; +exports.documentationURL = "https://reactnative.dev/docs/flatlist"; +exports.description = "Nested flatlist example"; +exports.examples = [ + { + title: "FlatList Nested example", + render: function (): React.Element { + return ; + }, + }, +]; diff --git a/packages/rn-tester/js/utils/RNTesterList.android.js b/packages/rn-tester/js/utils/RNTesterList.android.js index dcaa6330a30be9..bb921469fd1fe9 100644 --- a/packages/rn-tester/js/utils/RNTesterList.android.js +++ b/packages/rn-tester/js/utils/RNTesterList.android.js @@ -43,6 +43,11 @@ const ComponentExamples: Array = [ module: require('../examples/FlatList/FlatList-onEndReached'), category: 'ListView', }, + { + key: 'FlatList-nested', + module: require('../examples/FlatList/FlatList-nested'), + category: 'ListView', + }, { key: 'ImageExample', category: 'Basic',