Skip to content

Commit

Permalink
feat: grid accessibility announcement in flatlist
Browse files Browse the repository at this point in the history
  • Loading branch information
intergalacticspacehighway committed Jun 20, 2021
1 parent a40df5d commit ec09520
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const AndroidHorizontalScrollViewNativeComponent: HostComponent<Props> = NativeC
snapToStart: true,
snapToOffsets: true,
contentOffset: true,
accessibilityCollectionInfo: true,
},
}),
);
Expand Down
64 changes: 58 additions & 6 deletions Libraries/Lists/FlatList.js
Original file line number Diff line number Diff line change
Expand Up @@ -606,24 +606,71 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
return (
<View style={StyleSheet.compose(styles.row, columnWrapperStyle)}>
{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 = (
<View
importantForAccessibility="yes"
style={styles.cellStyle}
accessibilityCollectionItemInfo={
accessibilityCollectionItemInfo
}>
{renderer({
item: it,
index: index * numColumns + kk,
separators: info.separators,
})}
</View>
);
return element != null ? (
<React.Fragment key={kk}>{element}</React.Fragment>
) : null;
})}
</View>
);
} else {
return renderer(info);
const {index} = info;

const accessibilityCollectionItemInfo = {
rowIndex: index,
rowSpan: 1,
columnIndex: 0,
columnSpan: 1,
heading: false,
itemIndex: index,
};

return (
<View
importantForAccessibility="yes"
style={styles.cellStyle}
accessibilityCollectionItemInfo={accessibilityCollectionItemInfo}>
{renderer(info)}
</View>
);
}
},
};
};

_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;

Expand All @@ -633,6 +680,10 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, 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()}
Expand All @@ -643,6 +694,7 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {

const styles = StyleSheet.create({
row: {flexDirection: 'row'},
cellStyle: {flex: 1},
});

module.exports = FlatList;
34 changes: 5 additions & 29 deletions Libraries/Lists/VirtualizedList.js
Original file line number Diff line number Diff line change
Expand Up @@ -1236,15 +1236,6 @@ class VirtualizedList extends React.PureComponent<Props, State> {

_defaultRenderScrollComponent = props => {
const onRefresh = props.onRefresh;
const accessibilityCollectionProps = {
accessibilityRole: 'list',
accessibilityCollectionInfo: {
rowCount: this.props.getItemCount(this.props.data),
columnCount: 1,
hierarchical: false,
},
};

if (this._isNestedWithSameOrientation()) {
// $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors
return <View {...props} />;
Expand All @@ -1255,11 +1246,9 @@ class VirtualizedList extends React.PureComponent<Props, State> {
JSON.stringify(props.refreshing ?? 'undefined') +
'`',
);

return (
// $FlowFixMe[prop-missing] Invalid prop usage
<ScrollView
{...accessibilityCollectionProps}
{...props}
refreshControl={
props.refreshControl == null ? (
Expand All @@ -1276,7 +1265,7 @@ class VirtualizedList extends React.PureComponent<Props, State> {
);
} else {
// $FlowFixMe[prop-missing] Invalid prop usage
return <ScrollView {...accessibilityCollectionProps} {...props} />;
return <ScrollView {...props} />;
}
};

Expand Down Expand Up @@ -2075,32 +2064,19 @@ class CellRenderer extends React.Component<
: horizontal
? [styles.row, inversionStyle]
: inversionStyle;

const accessibilityCollectionItemInfo = {
rowIndex: index,
rowSpan: 1,
columnIndex: 1,
columnSpan: 1,
heading: false,
};

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. */
<View
style={cellStyle}
onLayout={onLayout}
accessibilityCollectionItemInfo={accessibilityCollectionItemInfo}>
This comment suppresses an error found when Flow v0.89 was deployed. *
To see the error, delete this comment and run Flow. */
<View style={cellStyle} onLayout={onLayout}>
{element}
{itemSeparator}
</View>
) : (
<CellRendererComponent
{...this.props}
style={cellStyle}
onLayout={onLayout}
accessibilityCollectionItemInfo={accessibilityCollectionItemInfo}>
onLayout={onLayout}>
{element}
{itemSeparator}
</CellRendererComponent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ public enum AccessibilityRole {
TABLIST,
TIMER,
LIST,
GRID,
TOOLBAR;

public static String getValue(AccessibilityRole role) {
Expand Down Expand Up @@ -140,6 +141,8 @@ public static String getValue(AccessibilityRole role) {
return "android.widget.Switch";
case LIST:
return "android.widget.AbsListView";
case GRID:
return "android.widget.GridView";
case NONE:
case LINK:
case SUMMARY:
Expand Down Expand Up @@ -209,20 +212,20 @@ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCo
}
final ReadableArray accessibilityActions =
(ReadableArray) host.getTag(R.id.accessibility_actions);
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);
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);
Expand Down Expand Up @@ -278,53 +281,13 @@ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCo
}
}

private boolean isViewVisible(View scrollView, View view) {
Rect scrollBounds = new Rect();
scrollView.getDrawingRect(scrollBounds);
float viewHeight = view.getHeight();
// Verify View is half visible
float top = view.getY() + viewHeight / 2;
float bottom = top + view.getHeight() - viewHeight / 2;

if (scrollBounds.top < top && scrollBounds.bottom > bottom) {
return true;
} else {
return false;
}
}

@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);
final ReadableMap accessibilityCollectionInfo = (ReadableMap) host.getTag(R.id.accessibility_collection_info);
if (accessibilityCollectionInfo != null) {
event.setItemCount(accessibilityCollectionInfo.getInt("rowCount"));

View contentView = ((ViewGroup) host).getChildAt(0);

ReadableMap firstVisible = null;
ReadableMap lastVisible = null;

for(int index = 0; index < ((ViewGroup) contentView).getChildCount(); index++) {
View nextChild = ((ViewGroup) contentView).getChildAt(index);
boolean isVisible = isViewVisible(host, nextChild);
if (isVisible == true) {
if(firstVisible == null) {
firstVisible = (ReadableMap) nextChild.getTag(R.id.accessibility_collection_item_info);
}
lastVisible = (ReadableMap) nextChild.getTag(R.id.accessibility_collection_item_info);
}


if (firstVisible != null && lastVisible != null) {
event.setFromIndex(firstVisible.getInt("rowIndex"));
event.setToIndex(lastVisible.getInt("rowIndex"));
}
}
}

if (accessibilityValue != null
&& accessibilityValue.hasKey("min")
Expand Down Expand Up @@ -499,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());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -122,13 +126,79 @@ 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
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);
}
}
});

Expand Down
Loading

0 comments on commit ec09520

Please sign in to comment.