Skip to content

Commit 463af23

Browse files
fabOnReactfacebook-github-bot
authored andcommitted
TalkBack support for ScrollView accessibility announcements (list and grid) - Javascript Only Changes (#33180)
Summary: This is the Javascript-only changes from D34518929 (dd6325b), split out for push safety. Original summary and test plan below: This issue fixes [30977][17] . The Pull Request was previously published by [intergalacticspacehighway][13] with [31666][19]. The solution consists of: 1. Adding Javascript logic in the [FlatList][14], SectionList, VirtualizedList components to provide accessibility information (row and column position) for each cell in the method [renderItem][20] as a fourth parameter [accessibilityCollectionItem][21]. The information is saved on the native side in the AccessibilityNodeInfo and announced by TalkBack when changing row, column, or page ([video example][12]). The prop accessibilityCollectionItem is available in the View component which wraps each FlatList cell. 2. Adding Java logic in [ReactScrollView.java][16] and HorizontalScrollView to announce pages with TalkBack when scrolling up/down. The missing AOSP logic in [ScrollView.java][10] (see also the [GridView][11] example) is responsible for announcing Page Scrolling with TalkBack. Relevant Links: x [Additional notes on this PR][18] x [discussion on the additional container View around each FlatList cell][22] x [commit adding prop getCellsInItemCount to VirtualizedList][23] ## Changelog [Android] [Added] - Accessibility announcement for list and grid in FlatList Pull Request resolved: #33180 Test Plan: [1]. TalkBack announces pages and cells with Horizontal Flatlist in the Paper Renderer ([link][1]) [2]. TalkBack announces pages and cells with Vertical Flatlist in the Paper Renderer ([link][2]) [3]. `FlatList numColumns={undefined}` Should not trigger Runtime Error NoSuchKey exception columnCount when enabling TalkBack. ([link][3]) [4]. TalkBack announces pages and cells with Nested Horizontal Flatlist in the rn-tester app ([link][4]) [1]: fabOnReact/react-native-notes#6 (comment) [2]: fabOnReact/react-native-notes#6 (comment) [3]: fabOnReact/react-native-notes#6 (comment) [4]: fabOnReact/react-native-notes#6 (comment) [10]:https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/android/widget/AdapterView.java#L1027-L1029 "GridView.java method responsible for calling setFromIndex and setToIndex" [11]:fabOnReact/react-native-notes#6 (comment) "test case on Android GridView" [12]:fabOnReact/react-native-notes#6 (comment) "TalkBack announces pages and cells with Horizontal Flatlist in the Paper Renderer" [13]:https://github.com/intergalacticspacehighway "github intergalacticspacehighway" [14]:https://github.com/fabriziobertoglio1987/react-native/blob/80acf523a4410adac8005d5c9472fb87f78e12ee/Libraries/Lists/FlatList.js#L617-L636 "FlatList accessibilityCollectionItem" [16]:https://github.com/fabriziobertoglio1987/react-native/blob/5706bd7d3ee35dca48f85322a2bdcaec0bce2c85/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java#L183-L184 "logic added to ReactScrollView.java" [17]: #30977 [18]: fabOnReact/react-native-notes#6 [19]: #31666 [20]: https://reactnative.dev/docs/next/flatlist#required-renderitem "FlatList renderItem documentation" [21]: fabOnReact@7514735 "commit that introduces fourth param accessibilityCollectionItem in callback renderItem" [22]: #33180 (comment) "discussion on the additional container View around each FlatList cell" [23]: fabOnReact@d50fd1a "commit adding prop getCellsInItemCount to VirtualizedList" Reviewed By: lunaleaps Differential Revision: D37668064 Pulled By: blavalla fbshipit-source-id: 7ba4068405fdcb9823d0daed2d8c36f0a56dbf0f
1 parent cc19cdc commit 463af23

17 files changed

+1489
-34
lines changed

Libraries/Components/View/ViewPropTypes.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,23 @@ export type ViewProps = $ReadOnly<{|
461461
*/
462462
accessibilityActions?: ?$ReadOnlyArray<AccessibilityActionInfo>,
463463

464+
/**
465+
*
466+
* Node Information of a FlatList, VirtualizedList or SectionList collection item.
467+
* A collection item starts at a given row and column in the collection, and spans one or more rows and columns.
468+
*
469+
* @platform android
470+
*
471+
*/
472+
accessibilityCollectionItem?: ?{
473+
rowIndex: number,
474+
rowSpan: number,
475+
columnIndex: number,
476+
columnSpan: number,
477+
heading: boolean,
478+
itemIndex: number,
479+
},
480+
464481
/**
465482
* Specifies the nativeID of the associated label text. When the assistive technology focuses on the component with this props, the text is read aloud.
466483
*

Libraries/Lists/FlatList.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -625,11 +625,18 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
625625
return (
626626
<View style={StyleSheet.compose(styles.row, columnWrapperStyle)}>
627627
{item.map((it, kk) => {
628+
const itemIndex = index * cols + kk;
629+
const accessibilityCollectionItem = {
630+
...info.accessibilityCollectionItem,
631+
columnIndex: itemIndex % cols,
632+
itemIndex: itemIndex,
633+
};
628634
const element = renderer({
629635
// $FlowFixMe[incompatible-call]
630636
item: it,
631-
index: index * cols + kk,
637+
index: itemIndex,
632638
separators: info.separators,
639+
accessibilityCollectionItem,
633640
});
634641
return element != null ? (
635642
<React.Fragment key={kk}>{element}</React.Fragment>
@@ -660,6 +667,7 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
660667
return (
661668
<VirtualizedList
662669
{...restProps}
670+
numColumns={numColumns}
663671
getItem={this._getItem}
664672
getItemCount={this._getItemCount}
665673
keyExtractor={this._keyExtractor}

Libraries/Lists/VirtualizedList.js

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const ScrollView = require('../Components/ScrollView/ScrollView');
4242
const View = require('../Components/View/View');
4343
const Batchinator = require('../Interaction/Batchinator');
4444
const ReactNative = require('../Renderer/shims/ReactNative');
45+
const Platform = require('../Utilities/Platform');
4546
const flattenStyle = require('../StyleSheet/flattenStyle');
4647
const StyleSheet = require('../StyleSheet/StyleSheet');
4748
const infoLog = require('../Utilities/infoLog');
@@ -74,6 +75,11 @@ type State = {
7475
* Use the following helper functions for default values
7576
*/
7677

78+
// numColumnsOrDefault(this.props.numColumns)
79+
function numColumnsOrDefault(numColumns: ?number) {
80+
return numColumns ?? 1;
81+
}
82+
7783
// horizontalOrDefault(this.props.horizontal)
7884
function horizontalOrDefault(horizontal: ?boolean) {
7985
return horizontal ?? false;
@@ -1010,10 +1016,35 @@ class VirtualizedList extends React.PureComponent<Props, State> {
10101016
);
10111017
}
10121018

1019+
_getCellsInItemCount = (props: Props) => {
1020+
const {getCellsInItemCount, data} = props;
1021+
if (getCellsInItemCount) {
1022+
return getCellsInItemCount(data);
1023+
}
1024+
if (Array.isArray(data)) {
1025+
return data.length;
1026+
}
1027+
return 0;
1028+
};
1029+
10131030
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
10141031
* LTI update could not be added via codemod */
10151032
_defaultRenderScrollComponent = props => {
1033+
const {getItemCount, data} = props;
10161034
const onRefresh = props.onRefresh;
1035+
const numColumns = numColumnsOrDefault(props.numColumns);
1036+
const accessibilityRole = Platform.select({
1037+
android: numColumns > 1 ? 'grid' : 'list',
1038+
});
1039+
const rowCount = getItemCount(data);
1040+
const accessibilityCollection = {
1041+
// over-ride _getCellsInItemCount to handle Objects or other data formats
1042+
// see https://bit.ly/35RKX7H
1043+
itemCount: this._getCellsInItemCount(props),
1044+
rowCount,
1045+
columnCount: numColumns,
1046+
hierarchical: false,
1047+
};
10171048
if (this._isNestedWithSameOrientation()) {
10181049
// $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors
10191050
return <View {...props} />;
@@ -1028,6 +1059,8 @@ class VirtualizedList extends React.PureComponent<Props, State> {
10281059
// $FlowFixMe[prop-missing] Invalid prop usage
10291060
<ScrollView
10301061
{...props}
1062+
accessibilityRole={accessibilityRole}
1063+
accessibilityCollection={accessibilityCollection}
10311064
refreshControl={
10321065
props.refreshControl == null ? (
10331066
<RefreshControl
@@ -1043,8 +1076,14 @@ class VirtualizedList extends React.PureComponent<Props, State> {
10431076
/>
10441077
);
10451078
} else {
1046-
// $FlowFixMe[prop-missing] Invalid prop usage
1047-
return <ScrollView {...props} />;
1079+
return (
1080+
// $FlowFixMe[prop-missing] Invalid prop usage
1081+
<ScrollView
1082+
{...props}
1083+
accessibilityRole={accessibilityRole}
1084+
accessibilityCollection={accessibilityCollection}
1085+
/>
1086+
);
10481087
}
10491088
};
10501089

@@ -1821,10 +1860,19 @@ class CellRenderer extends React.Component<
18211860
}
18221861

18231862
if (renderItem) {
1863+
const accessibilityCollectionItem = {
1864+
itemIndex: index,
1865+
rowIndex: index,
1866+
rowSpan: 1,
1867+
columnIndex: 0,
1868+
columnSpan: 1,
1869+
heading: false,
1870+
};
18241871
return renderItem({
18251872
item,
18261873
index,
18271874
separators: this._separators,
1875+
accessibilityCollectionItem,
18281876
});
18291877
}
18301878

Libraries/Lists/VirtualizedListProps.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,20 @@ export type Separators = {
2727
...
2828
};
2929

30+
export type AccessibilityCollectionItem = {
31+
itemIndex: number,
32+
rowIndex: number,
33+
rowSpan: number,
34+
columnIndex: number,
35+
columnSpan: number,
36+
heading: boolean,
37+
};
38+
3039
export type RenderItemProps<ItemT> = {
3140
item: ItemT,
3241
index: number,
3342
separators: Separators,
43+
accessibilityCollectionItem: AccessibilityCollectionItem,
3444
...
3545
};
3646

@@ -49,9 +59,19 @@ type RequiredProps = {|
4959
*/
5060
getItem: (data: any, index: number) => ?Item,
5161
/**
52-
* Determines how many items are in the data blob.
62+
* Determines how many items (rows) are in the data blob.
5363
*/
5464
getItemCount: (data: any) => number,
65+
/**
66+
* Determines how many cells are in the data blob
67+
* see https://bit.ly/35RKX7H
68+
*/
69+
getCellsInItemCount?: (data: any) => number,
70+
/**
71+
* The number of columns used in FlatList.
72+
* The default of 1 is used in other components to calculate the accessibilityCollection prop.
73+
*/
74+
numColumns?: ?number,
5575
|};
5676
type OptionalProps = {|
5777
renderItem?: ?RenderItemType<Item>,

Libraries/Lists/VirtualizedList_EXPERIMENTAL.js

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const ScrollView = require('../Components/ScrollView/ScrollView');
4949
const View = require('../Components/View/View');
5050
const Batchinator = require('../Interaction/Batchinator');
5151
const ReactNative = require('../Renderer/shims/ReactNative');
52+
const Platform = require('../Utilities/Platform');
5253
const flattenStyle = require('../StyleSheet/flattenStyle');
5354
const StyleSheet = require('../StyleSheet/StyleSheet');
5455
const infoLog = require('../Utilities/infoLog');
@@ -81,6 +82,11 @@ type State = {
8182
* Use the following helper functions for default values
8283
*/
8384

85+
// numColumnsOrDefault(this.props.numColumns)
86+
function numColumnsOrDefault(numColumns: ?number) {
87+
return numColumns ?? 1;
88+
}
89+
8490
// horizontalOrDefault(this.props.horizontal)
8591
function horizontalOrDefault(horizontal: ?boolean) {
8692
return horizontal ?? false;
@@ -1205,10 +1211,35 @@ class VirtualizedList extends React.PureComponent<Props, State> {
12051211
);
12061212
}
12071213

1214+
_getCellsInItemCount = (props: Props) => {
1215+
const {getCellsInItemCount, data} = props;
1216+
if (getCellsInItemCount) {
1217+
return getCellsInItemCount(data);
1218+
}
1219+
if (Array.isArray(data)) {
1220+
return data.length;
1221+
}
1222+
return 0;
1223+
};
1224+
12081225
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
12091226
* LTI update could not be added via codemod */
12101227
_defaultRenderScrollComponent = props => {
1228+
const {getItemCount, data} = props;
12111229
const onRefresh = props.onRefresh;
1230+
const numColumns = numColumnsOrDefault(props.numColumns);
1231+
const accessibilityRole = Platform.select({
1232+
android: numColumns > 1 ? 'grid' : 'list',
1233+
});
1234+
const rowCount = getItemCount(data);
1235+
const accessibilityCollection = {
1236+
// over-ride _getCellsInItemCount to handle Objects or other data formats
1237+
// see https://bit.ly/35RKX7H
1238+
itemCount: this._getCellsInItemCount(props),
1239+
rowCount,
1240+
columnCount: numColumns,
1241+
hierarchical: false,
1242+
};
12121243
if (this._isNestedWithSameOrientation()) {
12131244
// $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors
12141245
return <View {...props} />;
@@ -1223,6 +1254,8 @@ class VirtualizedList extends React.PureComponent<Props, State> {
12231254
// $FlowFixMe[prop-missing] Invalid prop usage
12241255
<ScrollView
12251256
{...props}
1257+
accessibilityRole={accessibilityRole}
1258+
accessibilityCollection={accessibilityCollection}
12261259
refreshControl={
12271260
props.refreshControl == null ? (
12281261
<RefreshControl
@@ -1238,8 +1271,14 @@ class VirtualizedList extends React.PureComponent<Props, State> {
12381271
/>
12391272
);
12401273
} else {
1241-
// $FlowFixMe[prop-missing] Invalid prop usage
1242-
return <ScrollView {...props} />;
1274+
return (
1275+
// $FlowFixMe[prop-missing] Invalid prop usage
1276+
<ScrollView
1277+
{...props}
1278+
accessibilityRole={accessibilityRole}
1279+
accessibilityCollection={accessibilityCollection}
1280+
/>
1281+
);
12431282
}
12441283
};
12451284

@@ -2037,10 +2076,19 @@ class CellRenderer extends React.Component<
20372076
}
20382077

20392078
if (renderItem) {
2079+
const accessibilityCollectionItem = {
2080+
itemIndex: index,
2081+
rowIndex: index,
2082+
rowSpan: 1,
2083+
columnIndex: 0,
2084+
columnSpan: 1,
2085+
heading: false,
2086+
};
20402087
return renderItem({
20412088
item,
20422089
index,
20432090
separators: this._separators,
2091+
accessibilityCollectionItem,
20442092
});
20452093
}
20462094

Libraries/Lists/VirtualizedSectionList.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*/
1010

1111
import type {ViewToken} from './ViewabilityHelper';
12-
12+
import type {AccessibilityCollectionItem} from './VirtualizedListProps';
1313
import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils';
1414
import invariant from 'invariant';
1515
import * as React from 'react';
@@ -342,7 +342,16 @@ class VirtualizedSectionList<
342342
_renderItem =
343343
(listItemCount: number) =>
344344
// eslint-disable-next-line react/no-unstable-nested-components
345-
({item, index}: {item: Item, index: number, ...}) => {
345+
({
346+
item,
347+
index,
348+
accessibilityCollectionItem,
349+
}: {
350+
item: Item,
351+
index: number,
352+
accessibilityCollectionItem: AccessibilityCollectionItem,
353+
...
354+
}) => {
346355
const info = this._subExtractor(index);
347356
if (!info) {
348357
return null;
@@ -371,6 +380,7 @@ class VirtualizedSectionList<
371380
LeadingSeparatorComponent={
372381
infoIndex === 0 ? this.props.SectionSeparatorComponent : undefined
373382
}
383+
accessibilityCollectionItem={accessibilityCollectionItem}
374384
cellKey={info.key}
375385
index={infoIndex}
376386
item={item}
@@ -486,6 +496,7 @@ type ItemWithSeparatorProps = $ReadOnly<{|
486496
updatePropsFor: (prevCellKey: string, value: Object) => void,
487497
renderItem: Function,
488498
inverted: boolean,
499+
accessibilityCollectionItem: AccessibilityCollectionItem,
489500
|}>;
490501

491502
function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node {
@@ -503,6 +514,7 @@ function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node {
503514
index,
504515
section,
505516
inverted,
517+
accessibilityCollectionItem,
506518
} = props;
507519

508520
const [leadingSeparatorHiglighted, setLeadingSeparatorHighlighted] =
@@ -576,6 +588,7 @@ function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node {
576588
index,
577589
section,
578590
separators,
591+
accessibilityCollectionItem,
579592
});
580593
const leadingSeparator = LeadingSeparatorComponent != null && (
581594
<LeadingSeparatorComponent

0 commit comments

Comments
 (0)