Closed fabOnReact closed 2 years ago
Talkback moves focus on a <Text>
element that has several nested <Text accessibilityRole=“link”>
elements
Related https://github.com/facebook/react-native/pull/31757 https://github.com/facebook/react-native/commit/b352e2da8137452f66717cf1cecb2e72abd727d7
Expected Result:
https://github.com/fabriziobertoglio1987/react-native-notes/blob/de66d8064361246ddb7ce0b97d4d2d8cfcfcc172/packages/rn-tester/js/examples/Accessibility/AccessibilityAndroidExample.android.js#L177-L215
<Text>
element that has a nested <Text accessibilityRole=“link”>
elementFunctionality built with commit https://github.com/facebook/react-native/commit/b352e2da8137452f66717cf1cecb2e72abd727d7
Expected Result: The user can interact with the nested link through the TalkBack accessibility menu.
https://github.com/fabriziobertoglio1987/react-native-notes/blob/de66d8064361246ddb7ce0b97d4d2d8cfcfcc172/packages/rn-tester/js/examples/Accessibility/AccessibilityAndroidExample.android.js#L177-L215
https://github.com/facebook/react-native/issues/30375#issuecomment-830781591
![image](https://user-images.githubusercontent.com/24992535/154790238-50ee94d1-29cc-41dc-8efb-2b070923902d.png)
```java
@NonNull
ClickableSpan[] getClickableSpans(CharSequence text) {
try {
if (text instanceof Spanned) {
Spanned spanned = (Spanned) text;
return spanned.getSpans(0, text.length(), ClickableSpan.class);
}
} catch (Exception e) {
//log exception
}
return new ClickableSpan[0];
}
```
git diff main..independent-links **/ReactBaseTextShadowNode.java
getClickableSpan
https://stackoverflow.com/a/62222068/7295772 https://android.googlesource.com/platform/frameworks/support/+/f7528a970ad7f2f099b4dc1397084cc388c9193c/core/src/main/java/androidx/core/view/ViewCompat.java#1316 ```java /** * Allow accessibility services to find and activate clickable spans in the application. * * @param view The view *
* Compatibility: *
g d main..independent-links
. The functionality works in branch independent-links
.https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/android/text/style/AccessibilityClickableSpan.java#L36 ```java /** * {@link ClickableSpan} cannot be parceled, but accessibility services need to be able to cause * their callback handlers to be called. This class serves as a parcelable placeholder for the * real spans. * * This span is also passed back to an app's process when an accessibility service tries to click * it. It contains enough information to track down the original clickable span so it can be * called. * * @hide */ public class AccessibilityClickableSpan extends ClickableSpan ``` https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/android/os/Parcel.java#L61 ```java /** * Container for a message (data and object references) that can * be sent through an IBinder. A Parcel can contain both flattened data * that will be unflattened on the other side of the IPC (using the various * methods here for writing specific types, or the general * {@link Parcelable} interface), and references to live {@link IBinder} * objects that will result in the other side receiving a proxy IBinder * connected with the original IBinder in the Parcel. *
The {@link Parcelable} protocol provides an extremely efficient (but * low-level) protocol for objects to write and read themselves from Parcels. * You can use the direct methods {@link #writeParcelable(Parcelable, int)} * and {@link #readParcelable(ClassLoader)} or * {@link #writeParcelableArray} and * {@link #readParcelableArray(ClassLoader)} to write or read. ``` https://en.wikipedia.org/wiki/Inter-process_communication >In [computer science](https://en.wikipedia.org/wiki/Computer_science), inter-process communication or interprocess communication (IPC) refers specifically to the mechanisms an [operating system](https://en.wikipedia.org/wiki/Operating_system) provides to allow the [processes](https://en.wikipedia.org/wiki/Process_(computing)) to manage shared data. Typically, applications can use IPC, categorized as [clients and servers](https://en.wikipedia.org/wiki/Client%E2%80%93server_model), where the client requests data and the server responds to client requests.[[1]](https://en.wikipedia.org/wiki/Inter-process_communication#cite_note-microsoft.com-1) Many applications are both clients and servers, as commonly seen in [distributed computing](https://en.wikipedia.org/wiki/Distributed_computing).
https://github.com/fabriziobertoglio1987/react-native-notes/blob/de66d8064361246ddb7ce0b97d4d2d8cfcfcc172/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java#L483-L520 https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/com/android/internal/widget/ExploreByTouchHelper.java#L32-L48 https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/com/android/internal/widget/ExploreByTouchHelper.java#L653-L662 https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/android/widget/SimpleMonthView.java#L1050-L1071 https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/android/widget/RadialTimePickerView.java#L1149-L1181
https://github.com/fabriziobertoglio1987/react-native-notes/blob/de66d8064361246ddb7ce0b97d4d2d8cfcfcc172/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java#L534-L549
- [x] change values in [getVirtualViewAt](https://github.com/fabriziobertoglio1987/react-native-notes/blob/de66d8064361246ddb7ce0b97d4d2d8cfcfcc172/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java#L484) and verify changes in TalkBack - [x] Try to implement AccessibilityClickableSpan on Android App and test it with TalkBack - [x] Implement logic in a separate AccessibilityDelegate attached to TextView - [x] debug [this methods](https://github.com/fabriziobertoglio1987/react-native-notes/issues/9#issuecomment-1046384200) and see what is not working in main branch - [x] import AccessibilityRole from ReactAccessibilityDelegate - [x] try to delete logic not added with `g d ..blavalla/export-D28691177 **/ReactAccessibilityDelegate.java` from `ReactTextAccessibilityDelegate` - [x] comment accessibilityAction logic and test if still works in both Text and nested Text - [x] comment accessibilityState logic => [does not announce disabled](https://github.com/fabriziobertoglio1987/react-native-notes/issues/9#issuecomment-1046673529) but is disabled - [x] check diff with master, comment logic and verify is required for the functionality - [x] Remove logic for ReactTextAccessibilityDelegate - [x] check accessibilityAction both on Text, View, Pressable on main and branch - [x] change the length of the AccessibilityClickableSpan and test TalkBack focus - [x] verify differences between approach in independent-links and [getClickableSpan](https://github.com/fabriziobertoglio1987/react-native-notes/issues/9#issuecomment-1045918241) - [x] call `setFocusable` - [x] Text actions don't work - [x] Text does not announce disabled - [x] further test accessibility functionalities in Text, Nested Text, View, Button, TouchableOpacity - [x] scrolling down with TalkBack sometimes trigger [no next link](https://github.com/fabriziobertoglio1987/react-native-notes/issues/9#issuecomment-1047480800) - [x] ReactAccessibilityDelegate extends ExploreByTouch and ReactTextDelegate extends ReactAccessibilityDelegate - [x] prepare questions on what you should improve
- [ ] improve functionality to span text over one line - [ ] [Nested text still does not announce disabled](https://github.com/fabriziobertoglio1987/react-native-notes/issues/9#issuecomment-1046673529), so you may look into their respective ViewManager and Views - [ ] Move shared logic to a new class - [ ] Try to use default constructor instead of the one with 3 params - [ ] Method setAccessibilityDelegate should be moved to common parent - [ ] Follow same pattern used with ReactSliderAccessibilityDelegate - [ ] review diff between independent-links and main - [ ] save class instance in constructor `mSharedClassInstance` - [ ] use shared logic `mSharedClassInstance.sharedMethod()` (composition) - [ ] Use [multiple inheritance ](https://stackoverflow.com/a/21824485/7295772)in ReactTextAccessibility Delegate (extends ExploreByTouchHelper and ReactAccessibilityDelegate) - [ ] Share interface (in ruby a module) between ReactAccessibilityDelegate and ReactTextAccessibilityDelegate
- [ ] change constructor to take 1 parameter
```javascript
https://user-images.githubusercontent.com/24992535/154877966-12d79405-1980-4f9a-b538-d7040ee5fbf9.mp4
CLICK TO OPEN SOURCECODE
CLICK TO OPEN VIDEO TESTS
https://github.com/fabriziobertoglio1987/react-native-notes/blob/de66d8064361246ddb7ce0b97d4d2d8cfcfcc172/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java#L499-L505 https://github.com/fabriziobertoglio1987/react-native-notes/blob/de66d8064361246ddb7ce0b97d4d2d8cfcfcc172/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactShadowNode.java#L24-L54
Related https://github.com/fabriziobertoglio1987/react-native-notes/issues/9#issuecomment-1045918241
MainActivity.java
```java
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LinearLayout linearLayout = (LinearLayout) findViewById(R.id.linearlayout);
TextView textView = new TextView(this);
textView.setMovementMethod(LinkMovementMethod.getInstance());
SpannableStringBuilder spannable = new SpannableStringBuilder("Text is spantastic!");
spannable.setSpan(
new CustomClickableSpan(Color.RED),
2, // start
12, // end
Spannable.SPAN_EXCLUSIVE_INCLUSIVE
);
textView.setText(spannable);
linearLayout.addView(textView);
}
}
```
CustomClickabeSpan.java
```java
public class CustomClickableSpan extends ClickableSpan {
private int color;
public CustomClickableSpan(int spanColor) {
super();
color = spanColor;
}
@Override
public void onClick(@NonNull View widget) {
Log.w("TESTING::", "onClick called on view: " + widget);
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(color);
}
}
```
CLICK TO OPEN SOURCECODE
CLICK TO OPEN VIDEO TESTS
**Expected Result**:
- Announces disabled.
- is disabled
**Actual Result**:
- Does **not** announce disabled.
- is disabled
```javascript <>
Here the accessibility links are created
https://github.com/fabriziobertoglio1987/react-native-notes/blob/052efec4b4c94dd5cf7a61ef6fc6043a5129884e/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java#L77-L82
https://github.com/fabriziobertoglio1987/react-native-notes/blob/052efec4b4c94dd5cf7a61ef6fc6043a5129884e/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactTextAccessibilityDelegate.java#L469-L501
The accessibility node information are set here
https://github.com/fabriziobertoglio1987/react-native-notes/blob/052efec4b4c94dd5cf7a61ef6fc6043a5129884e/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactTextAccessibilityDelegate.java#L366-L372
CLICK TO OPEN SOURCECODE
CLICK TO OPEN NOTES
CLICK TO OPEN VIDEO TESTS
When reaching the last page and trying to further scroll down in the ScrollView.
**Expected Result**:
TalkBack does not announce.
**Actual Result**:
TalkBack announces no next link.
```javascript
<>
CLICK TO OPEN SOURCECODE
CLICK TO OPEN VIDEO TESTS - BRANCH
CLICK TO OPEN VIDEO TESTS - MAIN
- [x] Review Meeting Notes when B. talks about tests - [x] Review comment on [slack](https://reactnativeac-vaz6944.slack.com/archives/C032SJNL35G/p1645142532114889) - [x] Understand if rn-tester tests are updated. Check rn-tester github history. - [x] Evaluate if building rn-tester with Fabric - [x] Evaluate if building rn-tester with Paper - [x] Test on Paper - [x] Test on Fabric - [x] Test for potential regressions in Fabric - [x] search for android method that provide this functionalities (for ex. retrieve all the links, instead of writing the logic we can find method in Android API and use it to retrieve the spans)
```javascript
/**
* 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.
*
* @format
* @flow
*/
'use strict';
import type {PressEvent} from 'react-native/Libraries/Types/CoreEventTypes';
const React = require('react');
const {
AccessibilityInfo,
TextInput,
Button,
Image,
Text,
View,
TouchableOpacity,
TouchableWithoutFeedback,
Alert,
StyleSheet,
Slider,
Platform,
} = require('react-native');
import type {EventSubscription} from 'react-native/Libraries/vendor/emitter/EventEmitter';
const RNTesterBlock = require('../../components/RNTesterBlock');
const checkImageSource = require('./check.png');
const uncheckImageSource = require('./uncheck.png');
const mixedCheckboxImageSource = require('./mixed.png');
const {createRef} = require('react');
const styles = StyleSheet.create({
default: {
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#0f0f0f',
flex: 1,
fontSize: 13,
padding: 4,
},
touchable: {
backgroundColor: 'blue',
borderColor: 'red',
borderWidth: 1,
borderRadius: 10,
padding: 10,
borderStyle: 'solid',
},
image: {
width: 20,
height: 20,
resizeMode: 'contain',
marginRight: 10,
},
disabledImage: {
width: 120,
height: 120,
},
containerAlignCenter: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
},
});
class AccessibilityExample extends React.Component<{}> {
render(): React.Node {
return (
```javascript
/**
* 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.
*
* @format
* @flow strict-local
*/
'use strict';
const React = require('react');
import RNTesterBlock from '../../components/RNTesterBlock';
import RNTesterPage from '../../components/RNTesterPage';
import {StyleSheet, Text, View, TouchableWithoutFeedback} from 'react-native';
const importantForAccessibilityValues = [
'auto',
'yes',
'no',
'no-hide-descendants',
];
type AccessibilityAndroidExampleState = {
count: number,
backgroundImportantForAcc: number,
forgroundImportantForAcc: number,
};
class AccessibilityAndroidExample extends React.Component<
{},
AccessibilityAndroidExampleState,
> {
state: AccessibilityAndroidExampleState = {
count: 0,
backgroundImportantForAcc: 0,
forgroundImportantForAcc: 0,
};
_addOne = () => {
this.setState({
count: ++this.state.count,
});
};
_changeBackgroundImportantForAcc = () => {
this.setState({
backgroundImportantForAcc: (this.state.backgroundImportantForAcc + 1) % 4,
});
};
_changeForgroundImportantForAcc = () => {
this.setState({
forgroundImportantForAcc: (this.state.forgroundImportantForAcc + 1) % 4,
});
};
render(): React.Node {
return (
```javascript
/**
* 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.
*
* @format
* @flow
*/
'use strict';
import type {PressEvent} from 'react-native/Libraries/Types/CoreEventTypes';
const React = require('react');
const {
AccessibilityInfo,
TextInput,
Button,
Image,
Text,
View,
TouchableOpacity,
TouchableWithoutFeedback,
Alert,
StyleSheet,
Slider,
Platform,
} = require('react-native');
import type {EventSubscription} from 'react-native/Libraries/vendor/emitter/EventEmitter';
const RNTesterBlock = require('../../components/RNTesterBlock');
const checkImageSource = require('./check.png');
const uncheckImageSource = require('./uncheck.png');
const mixedCheckboxImageSource = require('./mixed.png');
const {createRef} = require('react');
const styles = StyleSheet.create({
default: {
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#0f0f0f',
flex: 1,
fontSize: 13,
padding: 4,
},
touchable: {
backgroundColor: 'blue',
borderColor: 'red',
borderWidth: 1,
borderRadius: 10,
padding: 10,
borderStyle: 'solid',
},
image: {
width: 20,
height: 20,
resizeMode: 'contain',
marginRight: 10,
},
disabledImage: {
width: 120,
height: 120,
},
containerAlignCenter: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
},
});
class AccessibilityExample extends React.Component<{}> {
render(): React.Node {
return (
```javascript
/**
* 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.
*
* @format
* @flow strict-local
*/
'use strict';
const React = require('react');
import RNTesterBlock from '../../components/RNTesterBlock';
import RNTesterPage from '../../components/RNTesterPage';
import {StyleSheet, Text, View, TouchableWithoutFeedback} from 'react-native';
const importantForAccessibilityValues = [
'auto',
'yes',
'no',
'no-hide-descendants',
];
type AccessibilityAndroidExampleState = {
count: number,
backgroundImportantForAcc: number,
forgroundImportantForAcc: number,
};
class AccessibilityAndroidExample extends React.Component<
{},
AccessibilityAndroidExampleState,
> {
state: AccessibilityAndroidExampleState = {
count: 0,
backgroundImportantForAcc: 0,
forgroundImportantForAcc: 0,
};
_addOne = () => {
this.setState({
count: ++this.state.count,
});
};
_changeBackgroundImportantForAcc = () => {
this.setState({
backgroundImportantForAcc: (this.state.backgroundImportantForAcc + 1) % 4,
});
};
_changeForgroundImportantForAcc = () => {
this.setState({
forgroundImportantForAcc: (this.state.forgroundImportantForAcc + 1) % 4,
});
};
render(): React.Node {
return (
>They should be focused in order from top to bottom *after* thecontents of the entire paragraph.
https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/android/widget/TextView.java#L11866-L12090 https://github.com/androidx/androidx/blob/150112ca4e1e670fb4eb6756993f3a25df0a5da9/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java#L2695-L2719
https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/android/widget/TextView.java#L11956-L11967 ```java // A view should not be exposed as clickable/long-clickable to a service because of a // LinkMovementMethod. if ((info.isClickable() || info.isLongClickable()) && mMovement instanceof LinkMovementMethod) { if (!hasOnClickListeners()) { info.setClickable(false); info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK); } if (!hasOnLongClickListeners()) { info.setLongClickable(false); info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK); } } ```
https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/android/view/accessibility/AccessibilityNodeInfo.java#L2852-L2876 ```java /** * Sets the text of this node. *
* Note: Cannot be called from an * {@link android.accessibilityservice.AccessibilityService}. * This class is made immutable before being delivered to an AccessibilityService. *
* * @param text The text. * * @throws IllegalStateException If called from an AccessibilityService. */ public void setText(CharSequence text) { enforceNotSealed(); mOriginalText = text; if (text instanceof Spanned) { CharSequence tmpText = text; tmpText = replaceClickableSpan(tmpText); tmpText = replaceReplacementSpan(tmpText); mText = tmpText; return; } mText = (text == null) ? null : text.subSequence(0, text.length()); } ```
https://github.com/androidx/androidx/blob/150112ca4e1e670fb4eb6756993f3a25df0a5da9/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java#L2660-L2682
```java
/**
* Gets the text of this node.
*
* @return The text.
*/
public CharSequence getText() {
if (hasSpans()) {
List
The method returns an instance of Spannable
which uses AccessibilityClickableSpanCompat https://github.com/androidx/androidx/blob/150112ca4e1e670fb4eb6756993f3a25df0a5da9/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityClickableSpanCompat.java#L54-L59
debug the value returned in `onInitializeAccessibilityNodeInfo` `info.getText()`
https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/android/text/Spanned.java#L170-L198
Notes 1st of March
- [x] [AccessibilityLink][4] is duplicate of [SetSpanOperation][3] (used [here][5]) - [x] try to revert this changes to [ReactTextView][6] - [x] read android sourcecode and avoid code duplication (for ex. [getClickableSpans][7]) - [x] [consider moving the logic to AccessibilityNodeInfo callbacks][12] (spans are already managed in [nodeInfo][8]) - [x] add back [accessibility_links][11] tag commit to revert https://github.com/fabriziobertoglio1987/react-native/commit/d35e79f3df3c7e4ee9414e33a28752c4e9dd6e6d - [x] adding [mAccessibilityLinks][9] to [nodeInfo][10] - [x] remove duplicate logic from ReactTextAccessibilityDelegate - [x] [retrieve SPANS_START_KEY and SPANS_END_KEY from nodeInfo][1] - [x] check buck [dependency][2] uimanager - [x] review diff
Seems that spans variables area already saved after calling setText
- try to create setter to read variables
- log variables inside one of the NodeInfo callbacks
https://github.com/androidx/androidx/blob/150112ca4e1e670fb4eb6756993f3a25df0a5da9/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java#L2667
```java
List
https://github.com/fabriziobertoglio1987/react-native-notes/blob/4368f3a8ae3b5c11ffb2b5eb947f5a5aabb62524/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java#L616-L640
AccessibilityLinks constructor
>Should I follow the same pattern used with the Android Widgets TextView, GridView, ScrollView which consist of:
>1) an AccessibilityDelegate class with the logic shared between all the widgets
>2) Each Widget (ScrollView, GridView) over-rides the AccessibilityDelegate logic with the methods onInitializeAccessibilityEvent or NodeInfoInternal
>- AccessibilityNodeInfo method set/getText() allows us to retrieve the Text spans
>- AccessibilityLinks returns from the spans the information of the links start, end, text and position
>- ExploreByTouchHelper includes methods like onPopulateNodeForVirtualView etc.. to implement the functionality of navigating links with TalkBack
Notes
This issue fixes https://github.com/facebook/react-native/issues/32004. The Pull Request was previously published by blavalla with https://github.com/facebook/react-native/pull/31757.
This is a follow-up on D23553222, which made links functional by using Talkback's Links menu. We don't often use this as the sole access point for links due to it being more difficult for users to navigate to and easy for users to miss if they don't listen to the full description, including the hint text that announces that links are available.
Retrieving accessibility links and triggering TalkBack Focus over the Text
getVirtualViewAt
method (more info)Implementing ExploreByTouchHelper to detect touches over links and display TalkBack rectangle around them.
getVirtualViewAt
and onPopulateBoundsForVirtualView
.
The two methods implements the following functionalities (more info):
[Android] [Added] - Make links independently focusable by Talkback
1. User Interacts with links through TalkBack default accessibility menu (link) 2. The nested link becomes the next focusable element after the parent element that contains it. (link) 3. Testing accessibility examples in pr branch (link) 4. Testing accessibility android examples in pr branch (link)
Test on main branch 5. Testing accessibility examples in main branch (link) 6. Testing accessibility android examples in main branch (link)
Testing with the link.id in AccessibilityLink (discussion)
// ID is the reverse of what is expected, since the ClickableSpans are returned in reverse
// order due to being added in reverse order. If we don't do this, focus will move to the
// last link first and move backwards.
//
// If this approach becomes unreliable, we should instead look at their start position and
// order them manually.
link.id = spans.length - 1 - i;
links.add(link);
Expected Result: Swiping move TalkBack focus in this order:
This is a test of inline links in React Native. Here's another link. Here is a link that spans multiple lines because the text is so long. This sentence has no links in it. links available
test
, inline links
, another link
, link that spans multiple lines...
)Links are displayed in the above order in the TalkBack menu
Actual Results:
RESULT 1 (SUCCESS) - Swiping moves TalkBack focus in the correct order
1. Parent Text `This is a test of inline links in React Native. Here's another link. Here is a link that spans multiple lines because the text is so long. This sentence has no links in it. links available` 2. Nested Texts in order from top to bottom
RESULT 2 (FAIL) - Links are NOT displayed in the correct order in the TalkBack menu
Testing without the link.id in AccessibilityLink (discussion)
// ID is the reverse of what is expected, since the ClickableSpans are returned in reverse
// order due to being added in reverse order. If we don't do this, focus will move to the
// last link first and move backwards.
//
// If this approach becomes unreliable, we should instead look at their start position and
// order them manually.
// link.id = spans.length - 1 - i;
links.add(link);
Expected Result: Swiping move TalkBack focus in this order:
This is a test of inline links in React Native. Here's another link. Here is a link that spans multiple lines because the text is so long. This sentence has no links in it. links available
test
, inline links
, another link
, link that spans multiple lines...
)Links are displayed in the above order in the TalkBack menu
Actual Results:
1. Parent Text `This is a test of inline links in React Native. Here's another link. Here is a link that spans multiple lines because the text is so long. This sentence has no links in it. links available` 2. Swipe right or down moves the focus to the last link `link that spans multiple lines because the text is too long`.
new branch https://github.com/fabriziobertoglio1987/react-native/tree/independent-links-rebased old branch https://github.com/fabriziobertoglio1987/react-native/tree/independent-links https://github.com/facebook/react-native/commit/b352e2da8137452f66717cf1cecb2e72abd727d7
https://github.com/facebook/react-native/pull/31757
The PR allows TalkBack to move the focus directly on the nested Text link ( that contains it.
If there are multiple links within a single text element, they will each be focusable in order after the main element.
accessibilityRole="link"
). The nested link becomes the next focusable element after theRelated https://github.com/facebook/react-native/issues/30375 https://developer.android.com/reference/android/text/style/ClickableSpan https://developer.android.com/reference/androidx/core/view/ViewCompat#enableAccessibleClickableSpanSupport(android.view.View) https://stackoverflow.com/a/62222068/7295772 https://github.com/facebook/react-native/issues/32004