-
Notifications
You must be signed in to change notification settings - Fork 24.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
TalkBack support for ScrollView accessibility announcements (list and…
… grid) - JAVA ONLY CHANGES (#33180) Summary: This is the Java-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: kacieb Differential Revision: D37186697 Pulled By: blavalla fbshipit-source-id: 7bb95274326ded417c6f1365cc8633391f589d1a
- Loading branch information
1 parent
8cf57a5
commit 105a239
Showing
10 changed files
with
240 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
154 changes: 154 additions & 0 deletions
154
...d/src/main/java/com/facebook/react/views/scroll/ReactScrollViewAccessibilityDelegate.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
/* | ||
* Copyright (c) Meta Platforms, Inc. and affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
package com.facebook.react.views.scroll; | ||
|
||
import android.view.View; | ||
import android.view.ViewGroup; | ||
import android.view.accessibility.AccessibilityEvent; | ||
import androidx.core.view.AccessibilityDelegateCompat; | ||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; | ||
import com.facebook.react.R; | ||
import com.facebook.react.bridge.AssertionException; | ||
import com.facebook.react.bridge.ReactSoftExceptionLogger; | ||
import com.facebook.react.bridge.ReadableMap; | ||
import com.facebook.react.uimanager.ReactAccessibilityDelegate; | ||
|
||
public class ReactScrollViewAccessibilityDelegate extends AccessibilityDelegateCompat { | ||
private final String TAG = ReactScrollViewAccessibilityDelegate.class.getSimpleName(); | ||
|
||
@Override | ||
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { | ||
super.onInitializeAccessibilityEvent(host, event); | ||
if (host instanceof ReactScrollView || host instanceof ReactHorizontalScrollView) { | ||
onInitializeAccessibilityEventInternal(host, event); | ||
} else { | ||
ReactSoftExceptionLogger.logSoftException( | ||
TAG, | ||
new AssertionException( | ||
"ReactScrollViewAccessibilityDelegate should only be used with ReactScrollView or ReactHorizontalScrollView, not with class: " | ||
+ host.getClass().getSimpleName())); | ||
} | ||
} | ||
|
||
@Override | ||
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { | ||
super.onInitializeAccessibilityNodeInfo(host, info); | ||
if (host instanceof ReactScrollView || host instanceof ReactHorizontalScrollView) { | ||
onInitializeAccessibilityNodeInfoInternal(host, info); | ||
} else { | ||
ReactSoftExceptionLogger.logSoftException( | ||
TAG, | ||
new AssertionException( | ||
"ReactScrollViewAccessibilityDelegate should only be used with ReactScrollView or ReactHorizontalScrollView, not with class: " | ||
+ host.getClass().getSimpleName())); | ||
} | ||
}; | ||
|
||
private void onInitializeAccessibilityEventInternal(View view, AccessibilityEvent event) { | ||
final ReadableMap accessibilityCollection = | ||
(ReadableMap) view.getTag(R.id.accessibility_collection); | ||
|
||
if (accessibilityCollection != null) { | ||
event.setItemCount(accessibilityCollection.getInt("itemCount")); | ||
View contentView; | ||
if (view instanceof ViewGroup) { | ||
ViewGroup viewGroup = (ViewGroup) view; | ||
contentView = viewGroup.getChildAt(0); | ||
} else { | ||
return; | ||
} | ||
Integer firstVisibleIndex = null; | ||
Integer lastVisibleIndex = null; | ||
|
||
if (!(contentView instanceof ViewGroup)) { | ||
return; | ||
} | ||
|
||
for (int index = 0; index < ((ViewGroup) contentView).getChildCount(); index++) { | ||
View nextChild = ((ViewGroup) contentView).getChildAt(index); | ||
boolean isVisible; | ||
if (view instanceof ReactScrollView) { | ||
ReactScrollView scrollView = (ReactScrollView) view; | ||
isVisible = scrollView.isPartiallyScrolledInView(nextChild); | ||
} else if (view instanceof ReactHorizontalScrollView) { | ||
ReactHorizontalScrollView scrollView = (ReactHorizontalScrollView) view; | ||
isVisible = scrollView.isPartiallyScrolledInView(nextChild); | ||
} else { | ||
return; | ||
} | ||
ReadableMap accessibilityCollectionItem = | ||
(ReadableMap) nextChild.getTag(R.id.accessibility_collection_item); | ||
|
||
if (!(nextChild instanceof ViewGroup)) { | ||
return; | ||
} | ||
|
||
int childCount = ((ViewGroup) nextChild).getChildCount(); | ||
|
||
// If this child's accessibilityCollectionItem is null, we'll check one more | ||
// nested child. | ||
// Happens when getItemLayout is not passed in FlatList which adds an additional | ||
// View in the hierarchy. | ||
if (childCount > 0 && accessibilityCollectionItem == null) { | ||
View nestedNextChild = ((ViewGroup) nextChild).getChildAt(0); | ||
if (nestedNextChild != null) { | ||
ReadableMap nestedChildAccessibility = | ||
(ReadableMap) nestedNextChild.getTag(R.id.accessibility_collection_item); | ||
if (nestedChildAccessibility != null) { | ||
accessibilityCollectionItem = nestedChildAccessibility; | ||
} | ||
} | ||
} | ||
|
||
if (isVisible == true && accessibilityCollectionItem != null) { | ||
if (firstVisibleIndex == null) { | ||
firstVisibleIndex = accessibilityCollectionItem.getInt("itemIndex"); | ||
} | ||
lastVisibleIndex = accessibilityCollectionItem.getInt("itemIndex"); | ||
} | ||
|
||
if (firstVisibleIndex != null && lastVisibleIndex != null) { | ||
event.setFromIndex(firstVisibleIndex); | ||
event.setToIndex(lastVisibleIndex); | ||
} | ||
} | ||
} | ||
} | ||
|
||
private void onInitializeAccessibilityNodeInfoInternal( | ||
View view, AccessibilityNodeInfoCompat info) { | ||
final ReactAccessibilityDelegate.AccessibilityRole accessibilityRole = | ||
(ReactAccessibilityDelegate.AccessibilityRole) view.getTag(R.id.accessibility_role); | ||
|
||
if (accessibilityRole != null) { | ||
ReactAccessibilityDelegate.setRole(info, accessibilityRole, view.getContext()); | ||
} | ||
|
||
final ReadableMap accessibilityCollection = | ||
(ReadableMap) view.getTag(R.id.accessibility_collection); | ||
|
||
if (accessibilityCollection != null) { | ||
int rowCount = accessibilityCollection.getInt("rowCount"); | ||
int columnCount = accessibilityCollection.getInt("columnCount"); | ||
boolean hierarchical = accessibilityCollection.getBoolean("hierarchical"); | ||
|
||
AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfoCompat = | ||
AccessibilityNodeInfoCompat.CollectionInfoCompat.obtain( | ||
rowCount, columnCount, hierarchical); | ||
info.setCollectionInfo(collectionInfoCompat); | ||
} | ||
|
||
if (view instanceof ReactScrollView) { | ||
ReactScrollView scrollView = (ReactScrollView) view; | ||
info.setScrollable(scrollView.getScrollEnabled()); | ||
} else if (view instanceof ReactHorizontalScrollView) { | ||
ReactHorizontalScrollView scrollView = (ReactHorizontalScrollView) view; | ||
info.setScrollable(scrollView.getScrollEnabled()); | ||
} | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters