Skip to content

Commit dd6325b

Browse files
fabOnReactfacebook-github-bot
authored andcommitted
TalkBack support for ScrollView accessibility announcements (list and grid) (#33180)
Summary: 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: kacieb Differential Revision: D34518929 Pulled By: blavalla fbshipit-source-id: 410a05263a56162bf505a4cad957b24005ed65ed
1 parent 47d742a commit dd6325b

24 files changed

+1055
-51
lines changed

Libraries/Components/View/ViewAccessibility.js

+1
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

+17
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

+9-1
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

+70-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const Batchinator = require('../Interaction/Batchinator');
1212
const FillRateHelper = require('./FillRateHelper');
1313
const ReactNative = require('../Renderer/shims/ReactNative');
1414
const RefreshControl = require('../Components/RefreshControl/RefreshControl');
15+
const Platform = require('../Utilities/Platform');
1516
const ScrollView = require('../Components/ScrollView/ScrollView');
1617
const StyleSheet = require('../StyleSheet/StyleSheet');
1718
const View = require('../Components/View/View');
@@ -52,10 +53,20 @@ export type Separators = {
5253
...
5354
};
5455

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

@@ -84,9 +95,19 @@ type RequiredProps = {|
8495
*/
8596
getItem: (data: any, index: number) => ?Item,
8697
/**
87-
* Determines how many items are in the data blob.
98+
* Determines how many items (rows) are in the data blob.
8899
*/
89100
getItemCount: (data: any) => number,
101+
/**
102+
* Determines how many cells are in the data blob
103+
* see https://bit.ly/35RKX7H
104+
*/
105+
getCellsInItemCount?: (data: any) => number,
106+
/**
107+
* The number of columns used in FlatList.
108+
* The default of 1 is used in other components to calculate the accessibilityCollection prop.
109+
*/
110+
numColumns?: ?number,
90111
|};
91112
type OptionalProps = {|
92113
renderItem?: ?RenderItemType<Item>,
@@ -306,6 +327,10 @@ type Props = {|
306327
...OptionalProps,
307328
|};
308329

330+
function numColumnsOrDefault(numColumns: ?number) {
331+
return numColumns ?? 1;
332+
}
333+
309334
let _usedIndexForKey = false;
310335
let _keylessItemComponentName: string = '';
311336

@@ -1242,8 +1267,33 @@ class VirtualizedList extends React.PureComponent<Props, State> {
12421267
);
12431268
}
12441269

1270+
_getCellsInItemCount = props => {
1271+
const {getCellsInItemCount, data} = props;
1272+
if (getCellsInItemCount) {
1273+
return getCellsInItemCount(data);
1274+
}
1275+
if (Array.isArray(data)) {
1276+
return data.length;
1277+
}
1278+
return 0;
1279+
};
1280+
12451281
_defaultRenderScrollComponent = props => {
1282+
const {getItemCount, data} = props;
12461283
const onRefresh = props.onRefresh;
1284+
const numColumns = numColumnsOrDefault(props.numColumns);
1285+
const accessibilityRole = Platform.select({
1286+
android: numColumns > 1 ? 'grid' : 'list',
1287+
});
1288+
const rowCount = getItemCount(data);
1289+
const accessibilityCollection = {
1290+
// over-ride _getCellsInItemCount to handle Objects or other data formats
1291+
// see https://bit.ly/35RKX7H
1292+
itemCount: this._getCellsInItemCount(props),
1293+
rowCount,
1294+
columnCount: numColumns,
1295+
hierarchical: false,
1296+
};
12471297
if (this._isNestedWithSameOrientation()) {
12481298
// $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors
12491299
return <View {...props} />;
@@ -1258,6 +1308,8 @@ class VirtualizedList extends React.PureComponent<Props, State> {
12581308
// $FlowFixMe[prop-missing] Invalid prop usage
12591309
<ScrollView
12601310
{...props}
1311+
accessibilityRole={accessibilityRole}
1312+
accessibilityCollection={accessibilityCollection}
12611313
refreshControl={
12621314
props.refreshControl == null ? (
12631315
<RefreshControl
@@ -1272,8 +1324,14 @@ class VirtualizedList extends React.PureComponent<Props, State> {
12721324
/>
12731325
);
12741326
} else {
1275-
// $FlowFixMe[prop-missing] Invalid prop usage
1276-
return <ScrollView {...props} />;
1327+
return (
1328+
// $FlowFixMe[prop-missing] Invalid prop usage
1329+
<ScrollView
1330+
{...props}
1331+
accessibilityRole={accessibilityRole}
1332+
accessibilityCollection={accessibilityCollection}
1333+
/>
1334+
);
12771335
}
12781336
};
12791337

@@ -2018,10 +2076,19 @@ class CellRenderer extends React.Component<
20182076
}
20192077

20202078
if (renderItem) {
2079+
const accessibilityCollectionItem = {
2080+
itemIndex: index,
2081+
rowIndex: index,
2082+
rowSpan: 1,
2083+
columnIndex: 0,
2084+
columnSpan: 1,
2085+
heading: false,
2086+
};
20212087
return renderItem({
20222088
item,
20232089
index,
20242090
separators: this._separators,
2091+
accessibilityCollectionItem,
20252092
});
20262093
}
20272094

Libraries/Lists/VirtualizedSectionList.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import invariant from 'invariant';
1212
import type {ViewToken} from './ViewabilityHelper';
13+
import type {AccessibilityCollectionItem} from './VirtualizedList';
1314
import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils';
1415
import {View, VirtualizedList} from 'react-native';
1516
import * as React from 'react';
@@ -338,7 +339,16 @@ class VirtualizedSectionList<
338339

339340
_renderItem =
340341
(listItemCount: number) =>
341-
({item, index}: {item: Item, index: number, ...}) => {
342+
({
343+
item,
344+
index,
345+
accessibilityCollectionItem,
346+
}: {
347+
item: Item,
348+
index: number,
349+
accessibilityCollectionItem: AccessibilityCollectionItem,
350+
...
351+
}) => {
342352
const info = this._subExtractor(index);
343353
if (!info) {
344354
return null;
@@ -367,6 +377,7 @@ class VirtualizedSectionList<
367377
LeadingSeparatorComponent={
368378
infoIndex === 0 ? this.props.SectionSeparatorComponent : undefined
369379
}
380+
accessibilityCollectionItem={accessibilityCollectionItem}
370381
cellKey={info.key}
371382
index={infoIndex}
372383
item={item}
@@ -479,6 +490,7 @@ type ItemWithSeparatorProps = $ReadOnly<{|
479490
updatePropsFor: (prevCellKey: string, value: Object) => void,
480491
renderItem: Function,
481492
inverted: boolean,
493+
accessibilityCollectionItem: AccessibilityCollectionItem,
482494
|}>;
483495

484496
function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node {
@@ -496,6 +508,7 @@ function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node {
496508
index,
497509
section,
498510
inverted,
511+
accessibilityCollectionItem,
499512
} = props;
500513

501514
const [leadingSeparatorHiglighted, setLeadingSeparatorHighlighted] =
@@ -569,6 +582,7 @@ function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node {
569582
index,
570583
section,
571584
separators,
585+
accessibilityCollectionItem,
572586
});
573587
const leadingSeparator = LeadingSeparatorComponent != null && (
574588
<LeadingSeparatorComponent

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

+59
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)