Skip to content

Commit ec09520

Browse files
feat: grid accessibility announcement in flatlist
1 parent a40df5d commit ec09520

File tree

7 files changed

+270
-87
lines changed

7 files changed

+270
-87
lines changed

Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const AndroidHorizontalScrollViewNativeComponent: HostComponent<Props> = NativeC
3636
snapToStart: true,
3737
snapToOffsets: true,
3838
contentOffset: true,
39+
accessibilityCollectionInfo: true,
3940
},
4041
}),
4142
);

Libraries/Lists/FlatList.js

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -606,24 +606,71 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
606606
return (
607607
<View style={StyleSheet.compose(styles.row, columnWrapperStyle)}>
608608
{item.map((it, kk) => {
609-
const element = renderer({
610-
item: it,
611-
index: index * numColumns + kk,
612-
separators: info.separators,
613-
});
609+
const accessibilityCollectionItemInfo = {
610+
rowIndex: index,
611+
rowSpan: 1,
612+
columnIndex: (index * numColumns + kk) % numColumns,
613+
columnSpan: 1,
614+
heading: false,
615+
itemIndex: index * numColumns + kk,
616+
};
617+
618+
const element = (
619+
<View
620+
importantForAccessibility="yes"
621+
style={styles.cellStyle}
622+
accessibilityCollectionItemInfo={
623+
accessibilityCollectionItemInfo
624+
}>
625+
{renderer({
626+
item: it,
627+
index: index * numColumns + kk,
628+
separators: info.separators,
629+
})}
630+
</View>
631+
);
614632
return element != null ? (
615633
<React.Fragment key={kk}>{element}</React.Fragment>
616634
) : null;
617635
})}
618636
</View>
619637
);
620638
} else {
621-
return renderer(info);
639+
const {index} = info;
640+
641+
const accessibilityCollectionItemInfo = {
642+
rowIndex: index,
643+
rowSpan: 1,
644+
columnIndex: 0,
645+
columnSpan: 1,
646+
heading: false,
647+
itemIndex: index,
648+
};
649+
650+
return (
651+
<View
652+
importantForAccessibility="yes"
653+
style={styles.cellStyle}
654+
accessibilityCollectionItemInfo={accessibilityCollectionItemInfo}>
655+
{renderer(info)}
656+
</View>
657+
);
622658
}
623659
},
624660
};
625661
};
626662

663+
_getAccessibilityCollectionInfo = () => {
664+
const accessibilityCollectionProps = {
665+
itemCount: this.props.data ? this.props.data.length : 0,
666+
rowCount: this._getItemCount(this.props.data),
667+
columnCount: this.props.numColumns,
668+
hierarchical: false,
669+
};
670+
671+
return accessibilityCollectionProps;
672+
};
673+
627674
render(): React.Node {
628675
const {numColumns, columnWrapperStyle, ...restProps} = this.props;
629676

@@ -633,6 +680,10 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
633680
getItem={this._getItem}
634681
getItemCount={this._getItemCount}
635682
keyExtractor={this._keyExtractor}
683+
accessibilityCollectionInfo={this._getAccessibilityCollectionInfo()}
684+
accessibilityRole={Platform.select({
685+
android: this.props.numColumns > 1 ? 'grid' : 'list',
686+
})}
636687
ref={this._captureRef}
637688
viewabilityConfigCallbackPairs={this._virtualizedListPairs}
638689
{...this._renderer()}
@@ -643,6 +694,7 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
643694

644695
const styles = StyleSheet.create({
645696
row: {flexDirection: 'row'},
697+
cellStyle: {flex: 1},
646698
});
647699

648700
module.exports = FlatList;

Libraries/Lists/VirtualizedList.js

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,15 +1236,6 @@ class VirtualizedList extends React.PureComponent<Props, State> {
12361236

12371237
_defaultRenderScrollComponent = props => {
12381238
const onRefresh = props.onRefresh;
1239-
const accessibilityCollectionProps = {
1240-
accessibilityRole: 'list',
1241-
accessibilityCollectionInfo: {
1242-
rowCount: this.props.getItemCount(this.props.data),
1243-
columnCount: 1,
1244-
hierarchical: false,
1245-
},
1246-
};
1247-
12481239
if (this._isNestedWithSameOrientation()) {
12491240
// $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors
12501241
return <View {...props} />;
@@ -1255,11 +1246,9 @@ class VirtualizedList extends React.PureComponent<Props, State> {
12551246
JSON.stringify(props.refreshing ?? 'undefined') +
12561247
'`',
12571248
);
1258-
12591249
return (
12601250
// $FlowFixMe[prop-missing] Invalid prop usage
12611251
<ScrollView
1262-
{...accessibilityCollectionProps}
12631252
{...props}
12641253
refreshControl={
12651254
props.refreshControl == null ? (
@@ -1276,7 +1265,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
12761265
);
12771266
} else {
12781267
// $FlowFixMe[prop-missing] Invalid prop usage
1279-
return <ScrollView {...accessibilityCollectionProps} {...props} />;
1268+
return <ScrollView {...props} />;
12801269
}
12811270
};
12821271

@@ -2075,32 +2064,19 @@ class CellRenderer extends React.Component<
20752064
: horizontal
20762065
? [styles.row, inversionStyle]
20772066
: inversionStyle;
2078-
2079-
const accessibilityCollectionItemInfo = {
2080-
rowIndex: index,
2081-
rowSpan: 1,
2082-
columnIndex: 1,
2083-
columnSpan: 1,
2084-
heading: false,
2085-
};
2086-
20872067
const result = !CellRendererComponent ? (
20882068
/* $FlowFixMe[incompatible-type-arg] (>=0.89.0 site=react_native_fb) *
2089-
This comment suppresses an error found when Flow v0.89 was deployed. *
2090-
To see the error, delete this comment and run Flow. */
2091-
<View
2092-
style={cellStyle}
2093-
onLayout={onLayout}
2094-
accessibilityCollectionItemInfo={accessibilityCollectionItemInfo}>
2069+
This comment suppresses an error found when Flow v0.89 was deployed. *
2070+
To see the error, delete this comment and run Flow. */
2071+
<View style={cellStyle} onLayout={onLayout}>
20952072
{element}
20962073
{itemSeparator}
20972074
</View>
20982075
) : (
20992076
<CellRendererComponent
21002077
{...this.props}
21012078
style={cellStyle}
2102-
onLayout={onLayout}
2103-
accessibilityCollectionItemInfo={accessibilityCollectionItemInfo}>
2079+
onLayout={onLayout}>
21042080
{element}
21052081
{itemSeparator}
21062082
</CellRendererComponent>

ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java

Lines changed: 16 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ public enum AccessibilityRole {
110110
TABLIST,
111111
TIMER,
112112
LIST,
113+
GRID,
113114
TOOLBAR;
114115

115116
public static String getValue(AccessibilityRole role) {
@@ -140,6 +141,8 @@ public static String getValue(AccessibilityRole role) {
140141
return "android.widget.Switch";
141142
case LIST:
142143
return "android.widget.AbsListView";
144+
case GRID:
145+
return "android.widget.GridView";
143146
case NONE:
144147
case LINK:
145148
case SUMMARY:
@@ -209,20 +212,20 @@ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCo
209212
}
210213
final ReadableArray accessibilityActions =
211214
(ReadableArray) host.getTag(R.id.accessibility_actions);
212-
final ReadableMap accessibilityCollectionInfo =
213-
(ReadableMap) host.getTag(R.id.accessibility_collection_info);
214215

215-
216-
if (accessibilityCollectionInfo != null) {
217-
int rowCount = accessibilityCollectionInfo.getInt("rowCount");
218-
int columnCount = accessibilityCollectionInfo.getInt("columnCount");
219-
boolean hierarchical = accessibilityCollectionInfo.getBoolean("hierarchical");
220-
221-
AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfoCompat = AccessibilityNodeInfoCompat.CollectionInfoCompat.obtain(rowCount, columnCount, hierarchical);
222-
info.setCollectionInfo(collectionInfoCompat);
216+
final ReadableMap accessibilityCollectionItemInfo =
217+
(ReadableMap) host.getTag(R.id.accessibility_collection_item_info);
218+
if (accessibilityCollectionItemInfo != null) {
219+
int rowIndex = accessibilityCollectionItemInfo.getInt("rowIndex");
220+
int columnIndex = accessibilityCollectionItemInfo.getInt("columnIndex");
221+
int rowSpan = accessibilityCollectionItemInfo.getInt("rowSpan");
222+
int columnSpan = accessibilityCollectionItemInfo.getInt("columnSpan");
223+
boolean heading = accessibilityCollectionItemInfo.getBoolean("heading");
224+
225+
AccessibilityNodeInfoCompat.CollectionItemInfoCompat collectionItemInfoCompat = AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(rowIndex, rowSpan, columnIndex, columnSpan, heading);
226+
info.setCollectionItemInfo(collectionItemInfoCompat);
223227
}
224228

225-
226229
if (accessibilityActions != null) {
227230
for (int i = 0; i < accessibilityActions.size(); i++) {
228231
final ReadableMap action = accessibilityActions.getMap(i);
@@ -278,53 +281,13 @@ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCo
278281
}
279282
}
280283

281-
private boolean isViewVisible(View scrollView, View view) {
282-
Rect scrollBounds = new Rect();
283-
scrollView.getDrawingRect(scrollBounds);
284-
float viewHeight = view.getHeight();
285-
// Verify View is half visible
286-
float top = view.getY() + viewHeight / 2;
287-
float bottom = top + view.getHeight() - viewHeight / 2;
288284

289-
if (scrollBounds.top < top && scrollBounds.bottom > bottom) {
290-
return true;
291-
} else {
292-
return false;
293-
}
294-
}
295-
296285
@Override
297286
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
298287
super.onInitializeAccessibilityEvent(host, event);
299288
// Set item count and current item index on accessibility events for adjustable
300289
// in order to make Talkback announce the value of the adjustable
301290
final ReadableMap accessibilityValue = (ReadableMap) host.getTag(R.id.accessibility_value);
302-
final ReadableMap accessibilityCollectionInfo = (ReadableMap) host.getTag(R.id.accessibility_collection_info);
303-
if (accessibilityCollectionInfo != null) {
304-
event.setItemCount(accessibilityCollectionInfo.getInt("rowCount"));
305-
306-
View contentView = ((ViewGroup) host).getChildAt(0);
307-
308-
ReadableMap firstVisible = null;
309-
ReadableMap lastVisible = null;
310-
311-
for(int index = 0; index < ((ViewGroup) contentView).getChildCount(); index++) {
312-
View nextChild = ((ViewGroup) contentView).getChildAt(index);
313-
boolean isVisible = isViewVisible(host, nextChild);
314-
if (isVisible == true) {
315-
if(firstVisible == null) {
316-
firstVisible = (ReadableMap) nextChild.getTag(R.id.accessibility_collection_item_info);
317-
}
318-
lastVisible = (ReadableMap) nextChild.getTag(R.id.accessibility_collection_item_info);
319-
}
320-
321-
322-
if (firstVisible != null && lastVisible != null) {
323-
event.setFromIndex(firstVisible.getInt("rowIndex"));
324-
event.setToIndex(lastVisible.getInt("rowIndex"));
325-
}
326-
}
327-
}
328291

329292
if (accessibilityValue != null
330293
&& accessibilityValue.hasKey("min")
@@ -499,7 +462,8 @@ public static void setDelegate(final View view) {
499462
&& (view.getTag(R.id.accessibility_role) != null
500463
|| view.getTag(R.id.accessibility_state) != null
501464
|| view.getTag(R.id.accessibility_actions) != null
502-
|| view.getTag(R.id.react_test_id) != null)) {
465+
|| view.getTag(R.id.react_test_id) != null
466+
|| view.getTag(R.id.accessibility_collection_item_info) != null)) {
503467
ViewCompat.setAccessibilityDelegate(view, new ReactAccessibilityDelegate());
504468
}
505469
}

ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import android.view.KeyEvent;
2222
import android.view.MotionEvent;
2323
import android.view.View;
24+
import android.view.ViewGroup;
2425
import android.view.accessibility.AccessibilityEvent;
2526
import android.widget.HorizontalScrollView;
2627
import android.widget.OverScroller;
@@ -30,6 +31,8 @@
3031
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
3132
import com.facebook.common.logging.FLog;
3233
import com.facebook.infer.annotation.Assertions;
34+
import com.facebook.react.R;
35+
import com.facebook.react.bridge.ReadableMap;
3336
import com.facebook.react.bridge.WritableMap;
3437
import com.facebook.react.bridge.WritableNativeMap;
3538
import com.facebook.react.common.ReactConstants;
@@ -38,6 +41,7 @@
3841
import com.facebook.react.uimanager.FabricViewStateManager;
3942
import com.facebook.react.uimanager.MeasureSpecAssertions;
4043
import com.facebook.react.uimanager.PixelUtil;
44+
import com.facebook.react.uimanager.ReactAccessibilityDelegate;
4145
import com.facebook.react.uimanager.ReactClippingViewGroup;
4246
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
4347
import com.facebook.react.uimanager.ViewProps;
@@ -122,13 +126,79 @@ public ReactHorizontalScrollView(Context context, @Nullable FpsListener fpsListe
122126
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
123127
super.onInitializeAccessibilityEvent(host, event);
124128
event.setScrollable(mScrollEnabled);
129+
final ReadableMap accessibilityCollectionInfo = (ReadableMap) host.getTag(R.id.accessibility_collection_info);
130+
131+
if (accessibilityCollectionInfo != null) {
132+
event.setItemCount(accessibilityCollectionInfo.getInt("itemCount"));
133+
View contentView = getContentView();
134+
Integer firstVisibleIndex = null;
135+
Integer lastVisibleIndex = null;
136+
137+
if (!(contentView instanceof ViewGroup)) {
138+
return;
139+
}
140+
141+
for(int index = 0; index < ((ViewGroup) contentView).getChildCount(); index++) {
142+
View nextChild = ((ViewGroup) contentView).getChildAt(index);
143+
boolean isVisible = isPartiallyScrolledInView(nextChild);
144+
145+
ReadableMap accessibilityCollectionItemInfo = (ReadableMap) nextChild.getTag(R.id.accessibility_collection_item_info);
146+
147+
if (!(nextChild instanceof ViewGroup)) {
148+
return;
149+
}
150+
151+
int childCount = ((ViewGroup) nextChild).getChildCount();
152+
153+
// If this child's accessibilityCollectionItemInfo is null, we'll check one more nested child.
154+
// Happens when getItemLayout is not passed in FlatList which adds an additional View in the hierarchy.
155+
if (childCount > 0 && accessibilityCollectionItemInfo == null) {
156+
View nestedNextChild = ((ViewGroup) nextChild).getChildAt(0);
157+
if (nestedNextChild != null) {
158+
ReadableMap nestedChildAccessibilityInfo = (ReadableMap) nestedNextChild.getTag(R.id.accessibility_collection_item_info);
159+
if (nestedChildAccessibilityInfo != null) {
160+
accessibilityCollectionItemInfo = nestedChildAccessibilityInfo;
161+
}
162+
}
163+
}
164+
165+
if (isVisible == true && accessibilityCollectionItemInfo != null) {
166+
if(firstVisibleIndex == null) {
167+
firstVisibleIndex = accessibilityCollectionItemInfo.getInt("itemIndex");
168+
}
169+
lastVisibleIndex = accessibilityCollectionItemInfo.getInt("itemIndex");;
170+
}
171+
172+
if (firstVisibleIndex != null && lastVisibleIndex != null) {
173+
event.setFromIndex(firstVisibleIndex);
174+
event.setToIndex(lastVisibleIndex);
175+
}
176+
}
177+
}
125178
}
126179

127180
@Override
128181
public void onInitializeAccessibilityNodeInfo(
129182
View host, AccessibilityNodeInfoCompat info) {
130183
super.onInitializeAccessibilityNodeInfo(host, info);
131184
info.setScrollable(mScrollEnabled);
185+
final ReactAccessibilityDelegate.AccessibilityRole accessibilityRole =
186+
(ReactAccessibilityDelegate.AccessibilityRole) host.getTag(R.id.accessibility_role);
187+
188+
if (accessibilityRole != null) {
189+
ReactAccessibilityDelegate.setRole(info, accessibilityRole, host.getContext());
190+
}
191+
192+
final ReadableMap accessibilityCollectionInfo = (ReadableMap) host.getTag(R.id.accessibility_collection_info);
193+
194+
if (accessibilityCollectionInfo != null) {
195+
int rowCount = accessibilityCollectionInfo.getInt("rowCount");
196+
int columnCount = accessibilityCollectionInfo.getInt("columnCount");
197+
boolean hierarchical = accessibilityCollectionInfo.getBoolean("hierarchical");
198+
199+
AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfoCompat = AccessibilityNodeInfoCompat.CollectionInfoCompat.obtain(rowCount, columnCount, hierarchical);
200+
info.setCollectionInfo(collectionInfoCompat);
201+
}
132202
}
133203
});
134204

0 commit comments

Comments
 (0)