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',