Skip to content

Commit 2d58821

Browse files
fabOnReactfacebook-github-bot
authored andcommitted
TalkBack support for ScrollView accessibility announcements (list and grid) - Javascript Only Changes (facebook#33180)
Summary: This is the Javascript-only changes from D34518929 (facebook@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: facebook#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]: facebook#30977 [18]: fabOnReact/react-native-notes#6 [19]: facebook#31666 [20]: https://reactnative.dev/docs/next/flatlist#required-renderitem "FlatList renderItem documentation" [21]: 7514735 "commit that introduces fourth param accessibilityCollectionItem in callback renderItem" [22]: facebook#33180 (comment) "discussion on the additional container View around each FlatList cell" [23]: d50fd1a "commit adding prop getCellsInItemCount to VirtualizedList" Reviewed By: kacieb Differential Revision: D37189197 Pulled By: blavalla fbshipit-source-id: 3765213c5d8bfde56e0e5f155cdd899c368512e7
1 parent 71da212 commit 2d58821

File tree

14 files changed

+824
-31
lines changed

14 files changed

+824
-31
lines changed

Libraries/Components/View/ViewAccessibility.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export type AccessibilityRole =
4343
| 'tablist'
4444
| 'timer'
4545
| 'list'
46+
| 'grid'
4647
| 'toolbar';
4748

4849
// the info associated with an accessibility action

Libraries/Components/View/ViewPropTypes.js

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

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

Libraries/Lists/FlatList.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,10 +624,17 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
624624
return (
625625
<View style={StyleSheet.compose(styles.row, columnWrapperStyle)}>
626626
{item.map((it, kk) => {
627+
const itemIndex = index * cols + kk;
628+
const accessibilityCollectionItem = {
629+
...info.accessibilityCollectionItem,
630+
columnIndex: itemIndex % cols,
631+
itemIndex: itemIndex,
632+
};
627633
const element = renderer({
628634
item: it,
629-
index: index * cols + kk,
635+
index: itemIndex,
630636
separators: info.separators,
637+
accessibilityCollectionItem,
631638
});
632639
return element != null ? (
633640
<React.Fragment key={kk}>{element}</React.Fragment>
@@ -658,6 +665,7 @@ class FlatList<ItemT> extends React.PureComponent<Props<ItemT>, void> {
658665
return (
659666
<VirtualizedList
660667
{...restProps}
668+
numColumns={numColumns}
661669
getItem={this._getItem}
662670
getItemCount={this._getItemCount}
663671
keyExtractor={this._keyExtractor}

Libraries/Lists/VirtualizedList.js

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const ScrollView = require('../Components/ScrollView/ScrollView');
3535
const View = require('../Components/View/View');
3636
const Batchinator = require('../Interaction/Batchinator');
3737
const ReactNative = require('../Renderer/shims/ReactNative');
38+
const Platform = require('../Utilities/Platform');
3839
const flattenStyle = require('../StyleSheet/flattenStyle');
3940
const StyleSheet = require('../StyleSheet/StyleSheet');
4041
const infoLog = require('../Utilities/infoLog');
@@ -53,10 +54,20 @@ export type Separators = {
5354
...
5455
};
5556

57+
export type AccessibilityCollectionItem = {
58+
itemIndex: number,
59+
rowIndex: number,
60+
rowSpan: number,
61+
columnIndex: number,
62+
columnSpan: number,
63+
heading: boolean,
64+
};
65+
5666
export type RenderItemProps<ItemT> = {
5767
item: ItemT,
5868
index: number,
5969
separators: Separators,
70+
accessibilityCollectionItem: AccessibilityCollectionItem,
6071
...
6172
};
6273

@@ -85,9 +96,19 @@ type RequiredProps = {|
8596
*/
8697
getItem: (data: any, index: number) => ?Item,
8798
/**
88-
* Determines how many items are in the data blob.
99+
* Determines how many items (rows) are in the data blob.
89100
*/
90101
getItemCount: (data: any) => number,
102+
/**
103+
* Determines how many cells are in the data blob
104+
* see https://bit.ly/35RKX7H
105+
*/
106+
getCellsInItemCount?: (data: any) => number,
107+
/**
108+
* The number of columns used in FlatList.
109+
* The default of 1 is used in other components to calculate the accessibilityCollection prop.
110+
*/
111+
numColumns?: ?number,
91112
|};
92113
type OptionalProps = {|
93114
renderItem?: ?RenderItemType<Item>,
@@ -308,6 +329,10 @@ type Props = {|
308329
...OptionalProps,
309330
|};
310331

332+
function numColumnsOrDefault(numColumns: ?number) {
333+
return numColumns ?? 1;
334+
}
335+
311336
let _usedIndexForKey = false;
312337
let _keylessItemComponentName: string = '';
313338

@@ -1253,8 +1278,33 @@ class VirtualizedList extends React.PureComponent<Props, State> {
12531278
);
12541279
}
12551280

1281+
_getCellsInItemCount = props => {
1282+
const {getCellsInItemCount, data} = props;
1283+
if (getCellsInItemCount) {
1284+
return getCellsInItemCount(data);
1285+
}
1286+
if (Array.isArray(data)) {
1287+
return data.length;
1288+
}
1289+
return 0;
1290+
};
1291+
12561292
_defaultRenderScrollComponent = props => {
1293+
const {getItemCount, data} = props;
12571294
const onRefresh = props.onRefresh;
1295+
const numColumns = numColumnsOrDefault(props.numColumns);
1296+
const accessibilityRole = Platform.select({
1297+
android: numColumns > 1 ? 'grid' : 'list',
1298+
});
1299+
const rowCount = getItemCount(data);
1300+
const accessibilityCollection = {
1301+
// over-ride _getCellsInItemCount to handle Objects or other data formats
1302+
// see https://bit.ly/35RKX7H
1303+
itemCount: this._getCellsInItemCount(props),
1304+
rowCount,
1305+
columnCount: numColumns,
1306+
hierarchical: false,
1307+
};
12581308
if (this._isNestedWithSameOrientation()) {
12591309
// $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors
12601310
return <View {...props} />;
@@ -1269,6 +1319,8 @@ class VirtualizedList extends React.PureComponent<Props, State> {
12691319
// $FlowFixMe[prop-missing] Invalid prop usage
12701320
<ScrollView
12711321
{...props}
1322+
accessibilityRole={accessibilityRole}
1323+
accessibilityCollection={accessibilityCollection}
12721324
refreshControl={
12731325
props.refreshControl == null ? (
12741326
<RefreshControl
@@ -1284,8 +1336,14 @@ class VirtualizedList extends React.PureComponent<Props, State> {
12841336
/>
12851337
);
12861338
} else {
1287-
// $FlowFixMe[prop-missing] Invalid prop usage
1288-
return <ScrollView {...props} />;
1339+
return (
1340+
// $FlowFixMe[prop-missing] Invalid prop usage
1341+
<ScrollView
1342+
{...props}
1343+
accessibilityRole={accessibilityRole}
1344+
accessibilityCollection={accessibilityCollection}
1345+
/>
1346+
);
12891347
}
12901348
};
12911349

@@ -2056,10 +2114,19 @@ class CellRenderer extends React.Component<
20562114
}
20572115

20582116
if (renderItem) {
2117+
const accessibilityCollectionItem = {
2118+
itemIndex: index,
2119+
rowIndex: index,
2120+
rowSpan: 1,
2121+
columnIndex: 0,
2122+
columnSpan: 1,
2123+
heading: false,
2124+
};
20592125
return renderItem({
20602126
item,
20612127
index,
20622128
separators: this._separators,
2129+
accessibilityCollectionItem,
20632130
});
20642131
}
20652132

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 './VirtualizedList';
1313
import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils';
1414
import invariant from 'invariant';
1515
import * as React from 'react';
@@ -341,7 +341,16 @@ class VirtualizedSectionList<
341341
_renderItem =
342342
(listItemCount: number) =>
343343
// eslint-disable-next-line react/no-unstable-nested-components
344-
({item, index}: {item: Item, index: number, ...}) => {
344+
({
345+
item,
346+
index,
347+
accessibilityCollectionItem,
348+
}: {
349+
item: Item,
350+
index: number,
351+
accessibilityCollectionItem: AccessibilityCollectionItem,
352+
...
353+
}) => {
345354
const info = this._subExtractor(index);
346355
if (!info) {
347356
return null;
@@ -370,6 +379,7 @@ class VirtualizedSectionList<
370379
LeadingSeparatorComponent={
371380
infoIndex === 0 ? this.props.SectionSeparatorComponent : undefined
372381
}
382+
accessibilityCollectionItem={accessibilityCollectionItem}
373383
cellKey={info.key}
374384
index={infoIndex}
375385
item={item}
@@ -482,6 +492,7 @@ type ItemWithSeparatorProps = $ReadOnly<{|
482492
updatePropsFor: (prevCellKey: string, value: Object) => void,
483493
renderItem: Function,
484494
inverted: boolean,
495+
accessibilityCollectionItem: AccessibilityCollectionItem,
485496
|}>;
486497

487498
function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node {
@@ -499,6 +510,7 @@ function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node {
499510
index,
500511
section,
501512
inverted,
513+
accessibilityCollectionItem,
502514
} = props;
503515

504516
const [leadingSeparatorHiglighted, setLeadingSeparatorHighlighted] =
@@ -572,6 +584,7 @@ function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node {
572584
index,
573585
section,
574586
separators,
587+
accessibilityCollectionItem,
575588
});
576589
const leadingSeparator = LeadingSeparatorComponent != null && (
577590
<LeadingSeparatorComponent

Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ exports[`FlatList renders all the bells and whistles 1`] = `
66
ListEmptyComponent={[Function]}
77
ListFooterComponent={[Function]}
88
ListHeaderComponent={[Function]}
9+
accessibilityCollection={
10+
Object {
11+
"columnCount": 2,
12+
"hierarchical": false,
13+
"itemCount": 5,
14+
"rowCount": 3,
15+
}
16+
}
917
data={
1018
Array [
1119
Object {
@@ -29,6 +37,7 @@ exports[`FlatList renders all the bells and whistles 1`] = `
2937
getItemCount={[Function]}
3038
getItemLayout={[Function]}
3139
keyExtractor={[Function]}
40+
numColumns={2}
3241
onContentSizeChange={[Function]}
3342
onLayout={[Function]}
3443
onMomentumScrollBegin={[Function]}
@@ -121,6 +130,14 @@ exports[`FlatList renders all the bells and whistles 1`] = `
121130

122131
exports[`FlatList renders empty list 1`] = `
123132
<RCTScrollView
133+
accessibilityCollection={
134+
Object {
135+
"columnCount": 1,
136+
"hierarchical": false,
137+
"itemCount": 0,
138+
"rowCount": 0,
139+
}
140+
}
124141
data={Array []}
125142
getItem={[Function]}
126143
getItemCount={[Function]}
@@ -144,6 +161,14 @@ exports[`FlatList renders empty list 1`] = `
144161

145162
exports[`FlatList renders null list 1`] = `
146163
<RCTScrollView
164+
accessibilityCollection={
165+
Object {
166+
"columnCount": 1,
167+
"hierarchical": false,
168+
"itemCount": 0,
169+
"rowCount": 0,
170+
}
171+
}
147172
getItem={[Function]}
148173
getItemCount={[Function]}
149174
keyExtractor={[Function]}
@@ -166,6 +191,14 @@ exports[`FlatList renders null list 1`] = `
166191

167192
exports[`FlatList renders simple list (multiple columns) 1`] = `
168193
<RCTScrollView
194+
accessibilityCollection={
195+
Object {
196+
"columnCount": 2,
197+
"hierarchical": false,
198+
"itemCount": 3,
199+
"rowCount": 2,
200+
}
201+
}
169202
data={
170203
Array [
171204
Object {
@@ -182,6 +215,7 @@ exports[`FlatList renders simple list (multiple columns) 1`] = `
182215
getItem={[Function]}
183216
getItemCount={[Function]}
184217
keyExtractor={[Function]}
218+
numColumns={2}
185219
onContentSizeChange={[Function]}
186220
onLayout={[Function]}
187221
onMomentumScrollBegin={[Function]}
@@ -237,6 +271,14 @@ exports[`FlatList renders simple list (multiple columns) 1`] = `
237271

238272
exports[`FlatList renders simple list 1`] = `
239273
<RCTScrollView
274+
accessibilityCollection={
275+
Object {
276+
"columnCount": 1,
277+
"hierarchical": false,
278+
"itemCount": 3,
279+
"rowCount": 3,
280+
}
281+
}
240282
data={
241283
Array [
242284
Object {
@@ -298,6 +340,14 @@ exports[`FlatList renders simple list 1`] = `
298340
exports[`FlatList renders simple list using ListItemComponent (multiple columns) 1`] = `
299341
<RCTScrollView
300342
ListItemComponent={[Function]}
343+
accessibilityCollection={
344+
Object {
345+
"columnCount": 2,
346+
"hierarchical": false,
347+
"itemCount": 3,
348+
"rowCount": 2,
349+
}
350+
}
301351
data={
302352
Array [
303353
Object {
@@ -314,6 +364,7 @@ exports[`FlatList renders simple list using ListItemComponent (multiple columns)
314364
getItem={[Function]}
315365
getItemCount={[Function]}
316366
keyExtractor={[Function]}
367+
numColumns={2}
317368
onContentSizeChange={[Function]}
318369
onLayout={[Function]}
319370
onMomentumScrollBegin={[Function]}
@@ -369,6 +420,14 @@ exports[`FlatList renders simple list using ListItemComponent (multiple columns)
369420
exports[`FlatList renders simple list using ListItemComponent 1`] = `
370421
<RCTScrollView
371422
ListItemComponent={[Function]}
423+
accessibilityCollection={
424+
Object {
425+
"columnCount": 1,
426+
"hierarchical": false,
427+
"itemCount": 3,
428+
"rowCount": 3,
429+
}
430+
}
372431
data={
373432
Array [
374433
Object {

0 commit comments

Comments
 (0)