From 2d5882132fb2c533fe9bbba83576b8fac4aca727 Mon Sep 17 00:00:00 2001 From: fabriziobertoglio1987 Date: Fri, 17 Jun 2022 17:59:51 -0700 Subject: [PATCH] TalkBack support for ScrollView accessibility announcements (list and grid) - Javascript Only Changes (#33180) Summary: This is the Javascript-only changes from D34518929 (https://github.com/facebook/react-native/commit/dd6325bafe1a539d348f3710e717a6344576b859), 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: https://github.com/facebook/react-native/pull/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]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/6#issuecomment-1050452894 [2]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/6#issuecomment-1050462465 [3]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/6#issuecomment-1032340879 [4]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/6#issuecomment-1050618308 [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]:https://github.com/fabriziobertoglio1987/react-native-notes/issues/6#issuecomment-1042518901 "test case on Android GridView" [12]:https://github.com/fabriziobertoglio1987/react-native-notes/issues/6#issuecomment-1050452894 "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]: https://github.com/facebook/react-native/issues/30977 [18]: https://github.com/fabriziobertoglio1987/react-native-notes/issues/6 [19]: https://github.com/facebook/react-native/pull/31666 [20]: https://reactnative.dev/docs/next/flatlist#required-renderitem "FlatList renderItem documentation" [21]: https://github.com/fabriziobertoglio1987/react-native/commit/75147359c5d070406ebbe488c57c3cd94c08c19d "commit that introduces fourth param accessibilityCollectionItem in callback renderItem" [22]: https://github.com/facebook/react-native/pull/33180#discussion_r826748664 "discussion on the additional container View around each FlatList cell" [23]: https://github.com/fabriziobertoglio1987/react-native/commit/d50fd1a68112f40f4be3ac3aa4d67f96df33e387 "commit adding prop getCellsInItemCount to VirtualizedList" Reviewed By: kacieb Differential Revision: D37189197 Pulled By: blavalla fbshipit-source-id: 3765213c5d8bfde56e0e5f155cdd899c368512e7 --- .../Components/View/ViewAccessibility.js | 1 + Libraries/Components/View/ViewPropTypes.js | 17 + Libraries/Lists/FlatList.js | 10 +- Libraries/Lists/VirtualizedList.js | 73 +++- Libraries/Lists/VirtualizedSectionList.js | 17 +- .../__snapshots__/FlatList-test.js.snap | 59 +++ .../__snapshots__/SectionList-test.js.snap | 52 ++- .../VirtualizedList-test.js.snap | 360 ++++++++++++++++++ .../VirtualizedSectionList-test.js.snap | 88 +++++ .../js/examples/FlatList/FlatList-basic.js | 24 +- .../examples/FlatList/FlatList-multiColumn.js | 7 +- .../js/examples/FlatList/FlatList-nested.js | 124 ++++++ .../SectionList/SectionList-scrollable.js | 18 +- .../js/utils/RNTesterList.android.js | 5 + 14 files changed, 824 insertions(+), 31 deletions(-) create mode 100644 packages/rn-tester/js/examples/FlatList/FlatList-nested.js diff --git a/Libraries/Components/View/ViewAccessibility.js b/Libraries/Components/View/ViewAccessibility.js index 51343752c55e4e..182112160c680e 100644 --- a/Libraries/Components/View/ViewAccessibility.js +++ b/Libraries/Components/View/ViewAccessibility.js @@ -43,6 +43,7 @@ export type AccessibilityRole = | 'tablist' | 'timer' | 'list' + | 'grid' | 'toolbar'; // the info associated with an accessibility action diff --git a/Libraries/Components/View/ViewPropTypes.js b/Libraries/Components/View/ViewPropTypes.js index 0d6753be9f755c..aff394035d2093 100644 --- a/Libraries/Components/View/ViewPropTypes.js +++ b/Libraries/Components/View/ViewPropTypes.js @@ -464,6 +464,23 @@ export type ViewProps = $ReadOnly<{| */ accessibilityActions?: ?$ReadOnlyArray, + /** + * + * Node Information of a FlatList, VirtualizedList or SectionList collection item. + * A collection item starts at a given row and column in the collection, and spans one or more rows and columns. + * + * @platform android + * + */ + accessibilityCollectionItem?: ?{ + rowIndex: number, + rowSpan: number, + columnIndex: number, + columnSpan: number, + heading: boolean, + itemIndex: number, + }, + /** * Specifies the nativeID of the associated label text. When the assistive technology focuses on the component with this props, the text is read aloud. * diff --git a/Libraries/Lists/FlatList.js b/Libraries/Lists/FlatList.js index 8e810372bd74d7..e08fb950dd5c20 100644 --- a/Libraries/Lists/FlatList.js +++ b/Libraries/Lists/FlatList.js @@ -624,10 +624,17 @@ class FlatList extends React.PureComponent, void> { return ( {item.map((it, kk) => { + const itemIndex = index * cols + kk; + const accessibilityCollectionItem = { + ...info.accessibilityCollectionItem, + columnIndex: itemIndex % cols, + itemIndex: itemIndex, + }; const element = renderer({ item: it, - index: index * cols + kk, + index: itemIndex, separators: info.separators, + accessibilityCollectionItem, }); return element != null ? ( {element} @@ -658,6 +665,7 @@ class FlatList extends React.PureComponent, void> { return ( = { item: ItemT, index: number, separators: Separators, + accessibilityCollectionItem: AccessibilityCollectionItem, ... }; @@ -85,9 +96,19 @@ type RequiredProps = {| */ getItem: (data: any, index: number) => ?Item, /** - * Determines how many items are in the data blob. + * Determines how many items (rows) are in the data blob. */ getItemCount: (data: any) => number, + /** + * Determines how many cells are in the data blob + * see https://bit.ly/35RKX7H + */ + getCellsInItemCount?: (data: any) => number, + /** + * The number of columns used in FlatList. + * The default of 1 is used in other components to calculate the accessibilityCollection prop. + */ + numColumns?: ?number, |}; type OptionalProps = {| renderItem?: ?RenderItemType, @@ -308,6 +329,10 @@ type Props = {| ...OptionalProps, |}; +function numColumnsOrDefault(numColumns: ?number) { + return numColumns ?? 1; +} + let _usedIndexForKey = false; let _keylessItemComponentName: string = ''; @@ -1253,8 +1278,33 @@ class VirtualizedList extends React.PureComponent { ); } + _getCellsInItemCount = props => { + const {getCellsInItemCount, data} = props; + if (getCellsInItemCount) { + return getCellsInItemCount(data); + } + if (Array.isArray(data)) { + return data.length; + } + return 0; + }; + _defaultRenderScrollComponent = props => { + const {getItemCount, data} = props; const onRefresh = props.onRefresh; + const numColumns = numColumnsOrDefault(props.numColumns); + const accessibilityRole = Platform.select({ + android: numColumns > 1 ? 'grid' : 'list', + }); + const rowCount = getItemCount(data); + const accessibilityCollection = { + // over-ride _getCellsInItemCount to handle Objects or other data formats + // see https://bit.ly/35RKX7H + itemCount: this._getCellsInItemCount(props), + rowCount, + columnCount: numColumns, + hierarchical: false, + }; if (this._isNestedWithSameOrientation()) { // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors return ; @@ -1269,6 +1319,8 @@ class VirtualizedList extends React.PureComponent { // $FlowFixMe[prop-missing] Invalid prop usage { /> ); } else { - // $FlowFixMe[prop-missing] Invalid prop usage - return ; + return ( + // $FlowFixMe[prop-missing] Invalid prop usage + + ); } }; @@ -2056,10 +2114,19 @@ class CellRenderer extends React.Component< } if (renderItem) { + const accessibilityCollectionItem = { + itemIndex: index, + rowIndex: index, + rowSpan: 1, + columnIndex: 0, + columnSpan: 1, + heading: false, + }; return renderItem({ item, index, separators: this._separators, + accessibilityCollectionItem, }); } diff --git a/Libraries/Lists/VirtualizedSectionList.js b/Libraries/Lists/VirtualizedSectionList.js index 396ad7b58ca2de..82f9048c8a46e8 100644 --- a/Libraries/Lists/VirtualizedSectionList.js +++ b/Libraries/Lists/VirtualizedSectionList.js @@ -9,7 +9,7 @@ */ import type {ViewToken} from './ViewabilityHelper'; - +import type {AccessibilityCollectionItem} from './VirtualizedList'; import {keyExtractor as defaultKeyExtractor} from './VirtualizeUtils'; import invariant from 'invariant'; import * as React from 'react'; @@ -341,7 +341,16 @@ class VirtualizedSectionList< _renderItem = (listItemCount: number) => // eslint-disable-next-line react/no-unstable-nested-components - ({item, index}: {item: Item, index: number, ...}) => { + ({ + item, + index, + accessibilityCollectionItem, + }: { + item: Item, + index: number, + accessibilityCollectionItem: AccessibilityCollectionItem, + ... + }) => { const info = this._subExtractor(index); if (!info) { return null; @@ -370,6 +379,7 @@ class VirtualizedSectionList< LeadingSeparatorComponent={ infoIndex === 0 ? this.props.SectionSeparatorComponent : undefined } + accessibilityCollectionItem={accessibilityCollectionItem} cellKey={info.key} index={infoIndex} item={item} @@ -482,6 +492,7 @@ type ItemWithSeparatorProps = $ReadOnly<{| updatePropsFor: (prevCellKey: string, value: Object) => void, renderItem: Function, inverted: boolean, + accessibilityCollectionItem: AccessibilityCollectionItem, |}>; function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node { @@ -499,6 +510,7 @@ function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node { index, section, inverted, + accessibilityCollectionItem, } = props; const [leadingSeparatorHiglighted, setLeadingSeparatorHighlighted] = @@ -572,6 +584,7 @@ function ItemWithSeparator(props: ItemWithSeparatorProps): React.Node { index, section, separators, + accessibilityCollectionItem, }); const leadingSeparator = LeadingSeparatorComponent != null && ( Object { @@ -1566,6 +1718,14 @@ exports[`VirtualizedList test getItem functionality where data is not an Array 1 exports[`VirtualizedList warns if both renderItem or ListItemComponent are specified. Uses ListItemComponent 1`] = ` { /* $FlowFixMe[invalid-computed-prop] (>=0.111.0 site=react_native_fb) * This comment suppresses an error found when Flow v0.111 was deployed. * To see the error, delete this comment and run Flow. */ - [flatListPropKey]: ({item, separators}) => { + [flatListPropKey]: ({item, separators, accessibilityCollectionItem}) => { return ( - + + + ); }, }; diff --git a/packages/rn-tester/js/examples/FlatList/FlatList-multiColumn.js b/packages/rn-tester/js/examples/FlatList/FlatList-multiColumn.js index a4ec63a8c4d397..eb673afc1686e0 100644 --- a/packages/rn-tester/js/examples/FlatList/FlatList-multiColumn.js +++ b/packages/rn-tester/js/examples/FlatList/FlatList-multiColumn.js @@ -140,9 +140,12 @@ class MultiColumnExample extends React.PureComponent< getItemLayout(data, index).length + 2 * (CARD_MARGIN + BORDER_WIDTH); return {length, offset: length * index, index}; } - _renderItemComponent = ({item}: RenderItemProps) => { + _renderItemComponent = ({item, accessibilityCollectionItem}) => { return ( - + ( + + {item.title} + +); + +const renderItem = props => ; + +const renderFlatList = ({item}) => { + return ( + + Flatlist {item} + + + ); +}; + +const FlatListNested = (): React.Node => { + return ( + + item.toString()} + /> + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + marginTop: StatusBar.currentHeight || 0, + }, + item: { + backgroundColor: '#f9c2ff', + padding: 20, + marginVertical: 8, + marginHorizontal: 16, + }, + title: { + fontSize: 16, + }, +}); + +exports.title = 'FlatList Nested'; +exports.testTitle = 'Test accessibility announcement in nested flatlist'; +exports.category = 'ListView'; +exports.documentationURL = 'https://reactnative.dev/docs/flatlist'; +exports.description = 'Nested flatlist example'; +exports.examples = [ + { + title: 'FlatList Nested example', + render: function (): React.Element { + return ; + }, + }, +]; diff --git a/packages/rn-tester/js/examples/SectionList/SectionList-scrollable.js b/packages/rn-tester/js/examples/SectionList/SectionList-scrollable.js index 88fda4e8171869..962af98305bae4 100644 --- a/packages/rn-tester/js/examples/SectionList/SectionList-scrollable.js +++ b/packages/rn-tester/js/examples/SectionList/SectionList-scrollable.js @@ -109,7 +109,7 @@ const EmptySectionList = () => ( const renderItemComponent = setItemState => - ({item, separators}) => { + ({item, separators, accessibilityCollectionItem}) => { if (isNaN(item.key)) { return; } @@ -119,12 +119,16 @@ const renderItemComponent = }; return ( - + + + ); }; diff --git a/packages/rn-tester/js/utils/RNTesterList.android.js b/packages/rn-tester/js/utils/RNTesterList.android.js index f150ea666b1109..61e5318eab352c 100644 --- a/packages/rn-tester/js/utils/RNTesterList.android.js +++ b/packages/rn-tester/js/utils/RNTesterList.android.js @@ -31,6 +31,11 @@ const Components: Array = [ category: 'ListView', supportsTVOS: true, }, + { + key: 'FlatList-nested', + module: require('../examples/FlatList/FlatList-nested'), + category: 'ListView', + }, { key: 'ImageExample', category: 'Basic',