Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Role and item announcement in Flatlist #31666

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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={
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only issue I can think of with this approach is that it needs an additional wrapper View so that we can get the accessibility collection info on the native side.
The solution would be if we can create accessibility-only Views. I think @amarlette had created an issue for the same, but I am able to find it.

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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
columnCount: this.props.numColumns,
columnCount: numColumnsOrDefault(this.props.numColumns),

numColums default was removed with commit fabOnReact/react-native-notes@7d5895d. Rebasing will trigger a runtime.
More info in my comment fabOnReact/react-native-notes#6 (comment)

Copy link
Contributor

@fabOnReact fabOnReact Feb 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @blavalla
I'm preparing a pull request.
I would publish the following changes:

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;
4 changes: 2 additions & 2 deletions Libraries/Lists/VirtualizedList.js
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
<View style={cellStyle} onLayout={onLayout}>
{element}
{itemSeparator}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -107,6 +109,8 @@ public enum AccessibilityRole {
TAB,
TABLIST,
TIMER,
LIST,
GRID,
TOOLBAR;

public static String getValue(AccessibilityRole role) {
Expand Down Expand Up @@ -135,6 +139,10 @@ public static String getValue(AccessibilityRole role) {
return "android.widget.SpinButton";
case SWITCH:
return "android.widget.Switch";
case LIST:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow, I didn't realize we didn't have a role for list! Good catch!

return "android.widget.AbsListView";
case GRID:
return "android.widget.GridView";
case NONE:
case LINK:
case SUMMARY:
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ public interface BaseViewManagerInterface<T extends View> {
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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
Loading