Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Making links independently focusable by Talkback #9

Closed
fabOnReact opened this issue Feb 19, 2022 · 29 comments
Closed

Making links independently focusable by Talkback #9

fabOnReact opened this issue Feb 19, 2022 · 29 comments

Comments

@fabOnReact
Copy link
Owner

fabOnReact commented Feb 19, 2022

new branch https://github.com/fabriziobertoglio1987/react-native/tree/independent-links-rebased
old branch https://github.com/fabriziobertoglio1987/react-native/tree/independent-links
facebook/react-native@b352e2d

Right now nested Text components are not accessible on Android. This is because we only create a native ReactTextView for the parent component; the styling and touch handling for the child component are handled using spans. In order for TalkBack to announce the link, we need to linkify the text using a ClickableSpan.
This diff adds ReactClickableSpan, which TextLayoutManager uses to linkify a span of text when its corresponding React component has accessibilityRole="link". For example:

  <Text>
    A paragraph with some
    <Text accessible={true} accessibilityRole="link" onPress={onPress} onClick={onClick}>links</Text>
    surrounded by other text.
  </Text>

With this diff, the child Text component will be announced by TalkBack ('links available') and exposed as an option in the context menu. Clicking on the link in the context menu fires the Text component's onClick, which we're explicitly forwarding to onPress in Text.js (for now - ideally this would probably use a separate event, but that would involve wiring it up in the renderer as well).

facebook/react-native#31757

Summary:
This follows up on D23553222 (facebook/react-native@b352e2d), 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 entire description, including the hint text that announces that links are available.

The PR allows TalkBack to move the focus directly on the nested Text link (accessibilityRole="link").
The nested link becomes the next focusable element after the that contains it.
If there are multiple links within a single text element, they will each be focusable in order after the main element.

Related facebook/react-native#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 facebook/react-native#32004

@fabOnReact
Copy link
Owner Author

fabOnReact commented Feb 19, 2022

The nested link becomes the next focusable element after the parent element that contains it.

Talkback moves focus on a <Text> element that has several nested <Text accessibilityRole=“link”> elements

Related facebook/react-native#31757 facebook/react-native@b352e2d

Expected Result:

  • Multiple nested links are focusable after the parent.
  • Talkback moves the focus to the next element. The focus moves to the next nested link.
CLICK TO OPEN SOURCECODE

https://github.com/fabriziobertoglio1987/react-native-notes/blob/de66d8064361246ddb7ce0b97d4d2d8cfcfcc172/packages/rn-tester/js/examples/Accessibility/AccessibilityAndroidExample.android.js#L177-L215

CLICK TO OPEN VIDEO TESTS - pr branch

linksOnFabriAndBranch.mp4

CLICK TO OPEN VIDEO TESTS - main branch

linksOnMainDontWork.mp4

@fabOnReact
Copy link
Owner Author

fabOnReact commented Feb 19, 2022

User Interacts with links through TalkBack default accessibility menu

  • Talkback moves focus on a <Text> element that has a nested <Text accessibilityRole=“link”> element
  • The user opens Talkback default accessibility menu

Functionality built with commit facebook/react-native@b352e2d

Expected Result:
The user can interact with the nested link through the TalkBack accessibility menu.

CLICK TO OPEN SOURCECODE

https://github.com/fabriziobertoglio1987/react-native-notes/blob/de66d8064361246ddb7ce0b97d4d2d8cfcfcc172/packages/rn-tester/js/examples/Accessibility/AccessibilityAndroidExample.android.js#L177-L215

CLICK TO OPEN VIDEO TESTS - pr branch

linksOnFabriAndBranch.mp4

CLICK TO OPEN VIDEO TESTS - main branch - link accessible through menu

2022-02-28.16-14-51.mp4

@fabOnReact fabOnReact pinned this issue Feb 19, 2022
@fabOnReact

This comment was marked as resolved.

@fabOnReact

This comment was marked as off-topic.

@fabOnReact

This comment was marked as off-topic.

@fabOnReact

This comment was marked as outdated.

@fabOnReact
Copy link
Owner Author

fabOnReact commented Feb 21, 2022

AccessibilityClickableSpan

https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/android/text/style/AccessibilityClickableSpan.java#L36

/**
 * {@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

/**
 * 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. 
  * <h3>Parcelables</h3>
 *
 * <p>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, inter-process communication or interprocess communication (IPC) refers specifically to the mechanisms an operating system provides to allow the processes to manage shared data. Typically, applications can use IPC, categorized as clients and servers, where the client requests data and the server responds to client requests.[1] Many applications are both clients and servers, as commonly seen in distributed computing.

@fabOnReact
Copy link
Owner Author

fabOnReact commented Feb 21, 2022

Done

  • change values in getVirtualViewAt and verify changes in TalkBack
  • Try to implement AccessibilityClickableSpan on Android App and test it with TalkBack
  • Implement logic in a separate AccessibilityDelegate attached to TextView
  • debug this methods and see what is not working in main branch
  • import AccessibilityRole from ReactAccessibilityDelegate
  • try to delete logic not added with g d ..blavalla/export-D28691177 **/ReactAccessibilityDelegate.java from ReactTextAccessibilityDelegate
  • comment accessibilityAction logic and test if still works in both Text and nested Text
  • comment accessibilityState logic => does not announce disabled but is disabled
  • check diff with master, comment logic and verify is required for the functionality
  • Remove logic for ReactTextAccessibilityDelegate
  • check accessibilityAction both on Text, View, Pressable on main and branch
  • change the length of the AccessibilityClickableSpan and test TalkBack focus
  • verify differences between approach in independent-links and getClickableSpan
  • call setFocusable
  • Text actions don't work
  • Text does not announce disabled
  • further test accessibility functionalities in Text, Nested Text, View, Button, TouchableOpacity
  • scrolling down with TalkBack sometimes trigger no next link
  • ReactAccessibilityDelegate extends ExploreByTouch and ReactTextDelegate extends ReactAccessibilityDelegate
  • prepare questions on what you should improve

Low Priority

  • improve functionality to span text over one line
  • Nested text still does not announce disabled, 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 in ReactTextAccessibility Delegate (extends ExploreByTouchHelper and ReactAccessibilityDelegate)
  • Share interface (in ruby a module) between ReactAccessibilityDelegate and ReactTextAccessibilityDelegate

Deleted

  • change constructor to take 1 parameter

@fabOnReact
Copy link
Owner Author

fabOnReact commented Feb 21, 2022

The TalkBack focus (green rectangle) does not span on multiple lines

CLICK TO OPEN SOURCECODE

<Text
  accessibilityRole="link"
  onPress={() => {
    alert('pressed long link');
  }}>
  link that spans multiple lines because the text is so long.
</Text>

CLICK TO OPEN VIDEO TESTS

linkThatSpansMultipleLines.mp4

@fabOnReact
Copy link
Owner Author

fabOnReact commented Feb 21, 2022

CustomClickableSpan is clickable without talkback

Related #9 (comment)

CLICK TO OPEN SOURCECODE

MainActivity.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

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 VIDEO TESTS

clickableSpan.mp4

@fabOnReact
Copy link
Owner Author

fabOnReact commented Feb 21, 2022

Nested Links do not announce disabled or other accessibility properties

Expected Result:

  • Announces disabled.
  • is disabled

Actual Result:

  • Does not announce disabled.
  • is disabled
CLICK TO OPEN SOURCECODE

        <Text accessible={true} accessibilityState={{disabled: true}}>
          This is a{' '}
          <Text
            style={{color: 'green'}}
            accessibilityState={{disabled: true}}
            accessibilityRole="link"
            onPress={() => {
              alert('pressed test');
            }}>
            test
          </Text>{' '}
          of{' '}
          <Text
            style={{color: 'green'}}
            accessibilityRole="link"
            onPress={() => {
              alert('pressed Inline Links');
            }}>
            Inline Links
          </Text>
          in React Native. Here's{' '}
          <Text
            accessibilityRole="link"
            onPress={() => {
              alert('pressed another link');
            }}>
            another link
          </Text>
          . Here is a{' '}
          <Text
            accessibilityRole="link"
            accessibilityState={{disabled: true}}
            onPress={() => {
              alert('pressed long link');
            }}>
            link that spans multiple lines because the text is so long.
          </Text>
          I wonder how this works?
        </Text>
        <Text>Normal Text</Text>
        <Button
          accessibilityRole="button"
          title="Testing"
          accessibilityState={{disabled: true}}
        />
      </>

CLICK TO OPEN NOTES

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 VIDEO TESTS

2022-02-21.17-47-24.mp4

fabOnReact added a commit to fabOnReact/react-native that referenced this issue Feb 21, 2022
possible regression fabOnReact/react-native-notes#9 (comment)
need to check on the original branch if caused by removing setState
@fabOnReact
Copy link
Owner Author

fabOnReact commented Feb 22, 2022

TalkBack announces no next link when trying to move focus to next element.

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.

CLICK TO OPEN SOURCECODE

<>
  <RNTesterBlock title="Disabled TouchableOpacity">
    <TouchableOpacity
      onPress={() => Alert.alert('Disabled Button has been pressed!')}
      accessibilityLabel={'You are pressing Disabled TouchableOpacity'}
      accessibilityState={{disabled: true}}>
      <View>
        <Text>
          I am disabled. Clicking me will not trigger any action.
        </Text>
      </View>
    </TouchableOpacity>
  </RNTesterBlock>
  <RNTesterBlock title="View with multiple states">
    <View
      focusable={true}
      accessibilityLabel="Accessibility label."
      style={{height: 200, backgroundColor: 'red'}}
      accessible={true}>
      <Text>This view is selected and disabled.</Text>
    </View>
  </RNTesterBlock>
</>

CLICK TO OPEN VIDEO TESTS - BRANCH

2022-02-22.14-48-57.mp4

CLICK TO OPEN VIDEO TESTS - MAIN

2022-02-22.15-02-24.mp4

@fabOnReact
Copy link
Owner Author

fabOnReact commented Feb 23, 2022

Done

  • Review Meeting Notes when B. talks about tests
  • Review comment on slack
  • Understand if rn-tester tests are updated. Check rn-tester github history.
  • Evaluate if building rn-tester with Fabric
  • Evaluate if building rn-tester with Paper
  • Test on Paper
  • Test on Fabric
  • Test for potential regressions in Fabric
  • 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)

  • Publish Videos of rn-tester tests
  • ReactClickableSpan check diff with master and changes in functionality

@fabOnReact fabOnReact changed the title Make links independently focusable by Talkback Making links independently focusable by Talkback Feb 28, 2022
@fabOnReact
Copy link
Owner Author

fabOnReact commented Mar 1, 2022

Testing accessibility examples in main branch

CLICK TO OPEN SOURCECODE

/**
 * 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 (
      <View>
        <RNTesterBlock title="TextView without label">
          <Text>
            Text's accessibilityLabel is the raw text itself unless it is set
            explicitly.
          </Text>
        </RNTesterBlock>

        <RNTesterBlock title="TextView with label">
          <Text accessibilityLabel="I have label, so I read it instead of embedded text.">
            This text component's accessibilityLabel is set explicitly.
          </Text>
        </RNTesterBlock>

        <RNTesterBlock title="Nonaccessible view with TextViews">
          <View>
            <Text style={{color: 'green'}}>This is text one.</Text>
            <Text style={{color: 'blue'}}>This is text two.</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="Accessible view with TextViews wihout label">
          <View accessible={true}>
            <Text style={{color: 'green'}}>This is text one.</Text>
            <Text style={{color: 'blue'}}>This is text two.</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="Accessible view with TextViews with label">
          <View
            accessible={true}
            accessibilityLabel="I have label, so I read it instead of embedded text.">
            <Text style={{color: 'green'}}>This is text one.</Text>
            <Text style={{color: 'blue'}}>This is text two.</Text>
          </View>
        </RNTesterBlock>

        {/* Android screen readers will say the accessibility hint instead of the text
           since the view doesn't have a label. */}
        <RNTesterBlock title="Accessible view with TextViews with hint">
          <View accessibilityHint="Accessibility hint." accessible={true}>
            <Text style={{color: 'green'}}>This is text one.</Text>
            <Text style={{color: 'blue'}}>This is text two.</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="Accessible view TextViews with label and hint">
          <View
            accessibilityLabel="Accessibility label."
            accessibilityHint="Accessibility hint."
            accessible={true}>
            <Text style={{color: 'green'}}>This is text one.</Text>
            <Text style={{color: 'blue'}}>This is text two.</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="Text with accessibilityRole = header">
          <Text accessibilityRole="header">This is a title.</Text>
        </RNTesterBlock>

        <RNTesterBlock title="Touchable with accessibilityRole = link">
          <TouchableOpacity
            onPress={() => Alert.alert('Link has been clicked!')}
            accessibilityRole="link">
            <View>
              <Text>Click me</Text>
            </View>
          </TouchableOpacity>
        </RNTesterBlock>

        <RNTesterBlock title="Touchable with accessibilityRole = button">
          <TouchableOpacity
            onPress={() => Alert.alert('Button has been pressed!')}
            accessibilityRole="button">
            <Text>Click me</Text>
          </TouchableOpacity>
        </RNTesterBlock>

        <RNTesterBlock title="Disabled Touchable with role">
          <TouchableOpacity
            onPress={() => Alert.alert('Button has been pressed!')}
            accessibilityRole="button"
            accessibilityState={{disabled: true}}
            disabled={true}>
            <View>
              <Text>
                I am disabled. Clicking me will not trigger any action.
              </Text>
            </View>
          </TouchableOpacity>
        </RNTesterBlock>

        <RNTesterBlock title="Disabled TouchableOpacity">
          <TouchableOpacity
            onPress={() => Alert.alert('Disabled Button has been pressed!')}
            accessibilityLabel={'You are pressing Disabled TouchableOpacity'}
            accessibilityState={{disabled: true}}>
            <View>
              <Text>
                I am disabled. Clicking me will not trigger any action.
              </Text>
            </View>
          </TouchableOpacity>
        </RNTesterBlock>
        <RNTesterBlock title="View with multiple states">
          <View
            accessible={true}
            accessibilityState={{selected: true, disabled: true}}>
            <Text>This view is selected and disabled.</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="View with label, hint, role, and state">
          <View
            accessible={true}
            accessibilityLabel="Accessibility label."
            accessibilityRole="button"
            accessibilityState={{selected: true}}
            accessibilityHint="Accessibility hint.">
            <Text>Accessible view with label, hint, role, and state</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="TextInput with accessibilityLabelledBy attribute">
          <View>
            <Text nativeID="formLabel1">Mail Address</Text>
            <TextInput
              accessibilityLabel="input test1"
              accessibilityLabelledBy="formLabel1"
              style={styles.default}
            />
            <Text nativeID="formLabel2">First Name</Text>
            <TextInput
              accessibilityLabel="input test2"
              accessibilityLabelledBy={['formLabel2', 'formLabel3']}
              style={styles.default}
              value="Foo"
            />
          </View>
        </RNTesterBlock>
      </View>
    );
  }
}

class CheckboxExample extends React.Component<
  {},
  {
    checkboxState: boolean | 'mixed',
  },
> {
  state = {
    checkboxState: true,
  };

  _onCheckboxPress = () => {
    let checkboxState = false;
    if (this.state.checkboxState === false) {
      checkboxState = 'mixed';
    } else if (this.state.checkboxState === 'mixed') {
      checkboxState = true;
    } else {
      checkboxState = false;
    }

    this.setState({
      checkboxState: checkboxState,
    });
  };

  render() {
    return (
      <TouchableOpacity
        onPress={this._onCheckboxPress}
        accessibilityLabel="element 2"
        accessibilityRole="checkbox"
        accessibilityState={{checked: this.state.checkboxState}}
        accessibilityHint="click me to change state">
        <Text>Checkbox example</Text>
      </TouchableOpacity>
    );
  }
}

class SwitchExample extends React.Component<
  {},
  {
    switchState: boolean,
  },
> {
  state = {
    switchState: true,
  };

  _onSwitchToggle = () => {
    const switchState = !this.state.switchState;

    this.setState({
      switchState: switchState,
    });
  };

  render() {
    return (
      <TouchableOpacity
        onPress={this._onSwitchToggle}
        accessibilityLabel="element 12"
        accessibilityRole="switch"
        accessibilityState={{checked: this.state.switchState}}
        accessible={true}>
        <Text>Switch example</Text>
      </TouchableOpacity>
    );
  }
}

class SelectionExample extends React.Component<
  {},
  {
    isSelected: boolean,
    isEnabled: boolean,
  },
> {
  constructor(props: {}) {
    super(props);
    this.selectableElement = createRef();
  }
  selectableElement: {
    current: React.ElementRef<typeof TouchableOpacity> | null,
  };

  state = {
    isSelected: true,
    isEnabled: false,
  };

  render(): React.Node {
    const {isSelected, isEnabled} = this.state;
    let accessibilityHint = 'click me to select';
    if (isSelected) {
      accessibilityHint = 'click me to unselect';
    }
    if (!isEnabled) {
      accessibilityHint = 'use the button on the right to enable selection';
    }
    let buttonTitle = isEnabled ? 'Disable selection' : 'Enable selection';
    const touchableHint = ` (touching the TouchableOpacity will ${
      isSelected ? 'disable' : 'enable'
    } accessibilityState.selected)`;
    return (
      <View style={styles.containerAlignCenter}>
        <TouchableOpacity
          ref={this.selectableElement}
          accessible={true}
          onPress={() => {
            if (isEnabled) {
              this.setState({
                isSelected: !isSelected,
              });
            } else {
              console.warn('selection is disabled, please enable selection.');
            }
          }}
          accessibilityLabel="element 19"
          accessibilityState={{
            selected: isSelected,
            disabled: !isEnabled,
          }}
          style={styles.touchable}
          accessibilityHint={accessibilityHint}>
          <Text style={{color: 'white'}}>
            {`Selectable TouchableOpacity Example ${touchableHint}`}
          </Text>
        </TouchableOpacity>
        <TextInput
          accessibilityLabel="element 20"
          accessibilityState={{
            selected: isSelected,
          }}
          multiline={true}
          placeholder={`TextInput Example - ${
            isSelected ? 'enabled' : 'disabled'
          } selection`}
        />
        <Button
          onPress={() => {
            this.setState({
              isEnabled: !this.state.isEnabled,
            });
          }}
          title={buttonTitle}
        />
      </View>
    );
  }
}

class ExpandableElementExample extends React.Component<
  {},
  {
    expandState: boolean,
  },
> {
  state = {
    expandState: false,
  };

  _onElementPress = () => {
    const expandState = !this.state.expandState;

    this.setState({
      expandState: expandState,
    });
  };

  render() {
    return (
      <TouchableOpacity
        onPress={this._onElementPress}
        accessibilityLabel="element 18"
        accessibilityState={{expanded: this.state.expandState}}
        accessibilityHint="click me to change state">
        <Text>Expandable element example</Text>
      </TouchableOpacity>
    );
  }
}

class NestedCheckBox extends React.Component<
  {},
  {
    checkbox1: boolean | 'mixed',
    checkbox2: boolean | 'mixed',
    checkbox3: boolean | 'mixed',
  },
> {
  state = {
    checkbox1: false,
    checkbox2: false,
    checkbox3: false,
  };

  _onPress1 = () => {
    let checkbox1 = false;
    if (this.state.checkbox1 === false) {
      checkbox1 = true;
    } else if (this.state.checkbox1 === 'mixed') {
      checkbox1 = false;
    } else {
      checkbox1 = false;
    }
    setTimeout(() => {
      this.setState({
        checkbox1: checkbox1,
        checkbox2: checkbox1,
        checkbox3: checkbox1,
      });
    }, 2000);
  };

  _onPress2 = () => {
    const checkbox2 = !this.state.checkbox2;

    this.setState({
      checkbox2: checkbox2,
      checkbox1:
        checkbox2 && this.state.checkbox3
          ? true
          : checkbox2 || this.state.checkbox3
          ? 'mixed'
          : false,
    });
  };

  _onPress3 = () => {
    const checkbox3 = !this.state.checkbox3;

    this.setState({
      checkbox3: checkbox3,
      checkbox1:
        this.state.checkbox2 && checkbox3
          ? true
          : this.state.checkbox2 || checkbox3
          ? 'mixed'
          : false,
    });
  };

  render() {
    return (
      <View>
        <TouchableOpacity
          style={{flex: 1, flexDirection: 'row'}}
          onPress={this._onPress1}
          accessibilityLabel="Meat"
          accessibilityHint="State changes in 2 seconds after clicking."
          accessibilityRole="checkbox"
          accessibilityState={{checked: this.state.checkbox1}}>
          <Image
            style={styles.image}
            source={
              this.state.checkbox1 === 'mixed'
                ? mixedCheckboxImageSource
                : this.state.checkbox1
                ? checkImageSource
                : uncheckImageSource
            }
          />
          <Text>Meat</Text>
        </TouchableOpacity>
        <TouchableOpacity
          style={{flex: 1, flexDirection: 'row'}}
          onPress={this._onPress2}
          accessibilityLabel="Beef"
          accessibilityRole="checkbox"
          accessibilityState={{checked: this.state.checkbox2}}>
          <Image
            style={styles.image}
            source={
              this.state.checkbox2 ? checkImageSource : uncheckImageSource
            }
          />
          <Text>Beef</Text>
        </TouchableOpacity>
        <TouchableOpacity
          style={{flex: 1, flexDirection: 'row'}}
          onPress={this._onPress3}
          accessibilityLabel="Bacon"
          accessibilityRole="checkbox"
          accessibilityState={{checked: this.state.checkbox3}}>
          <Image
            style={styles.image}
            source={
              this.state.checkbox3 ? checkImageSource : uncheckImageSource
            }
          />
          <Text>Bacon</Text>
        </TouchableOpacity>
      </View>
    );
  }
}

class AccessibilityRoleAndStateExample extends React.Component<{}> {
  render(): React.Node {
    return (
      <View>
        <View
          accessibilityLabel="element 1"
          accessibilityRole="alert"
          accessible={true}>
          <Text>Alert example</Text>
        </View>
        <CheckboxExample />
        <View
          accessibilityLabel="element 3"
          accessibilityRole="combobox"
          accessible={true}>
          <Text>Combobox example</Text>
        </View>
        <View
          accessibilityLabel="element 4"
          accessibilityRole="menu"
          accessible={true}>
          <Text>Menu example</Text>
        </View>
        <View
          accessibilityLabel="element 5"
          accessibilityRole="menubar"
          accessible={true}>
          <Text>Menu bar example</Text>
        </View>
        <View
          accessibilityLabel="element 6"
          accessibilityRole="menuitem"
          accessible={true}>
          <Text>Menu item example</Text>
        </View>
        <View
          accessibilityLabel="element 7"
          accessibilityRole="progressbar"
          accessible={true}>
          <Text>Progress bar example</Text>
        </View>
        <View
          accessibilityLabel="element 8"
          accessibilityRole="radio"
          accessible={true}>
          <Text>Radio button example</Text>
        </View>
        <View
          accessibilityLabel="element 9"
          accessibilityRole="radiogroup"
          accessible={true}>
          <Text>Radio group example</Text>
        </View>
        <View
          accessibilityLabel="element 10"
          accessibilityRole="scrollbar"
          accessible={true}>
          <Text>Scrollbar example</Text>
        </View>
        <View
          accessibilityLabel="element 11"
          accessibilityRole="spinbutton"
          accessible={true}>
          <Text>Spin button example</Text>
        </View>
        <SwitchExample />
        <View
          accessibilityLabel="element 13"
          accessibilityRole="tab"
          accessible={true}>
          <Text>Tab example</Text>
        </View>
        <View
          accessibilityLabel="element 14"
          accessibilityRole="tablist"
          accessible={true}>
          <Text>Tab list example</Text>
        </View>
        <View
          accessibilityLabel="element 15"
          accessibilityRole="timer"
          accessible={true}>
          <Text>Timer example</Text>
        </View>
        <View
          accessibilityLabel="element 16"
          accessibilityRole="toolbar"
          accessible={true}>
          <Text>Toolbar example</Text>
        </View>
        <View
          accessibilityLabel="element 17"
          accessibilityState={{busy: true}}
          accessible={true}>
          <Text>State busy example</Text>
        </View>
        <ExpandableElementExample />
        <SelectionExample />
        <RNTesterBlock title="Nested checkbox with delayed state change">
          <NestedCheckBox />
        </RNTesterBlock>
      </View>
    );
  }
}

class AccessibilityActionsExample extends React.Component<{}> {
  render(): React.Node {
    return (
      <View>
        <RNTesterBlock title="Non-touchable with activate action">
          <View
            accessible={true}
            accessibilityActions={[{name: 'activate'}]}
            onAccessibilityAction={event => {
              switch (event.nativeEvent.actionName) {
                case 'activate':
                  Alert.alert('Alert', 'View is clicked');
                  break;
              }
            }}>
            <Text>Click me</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="View with multiple actions">
          <View
            accessible={true}
            accessibilityActions={[
              {name: 'cut', label: 'cut label'},
              {name: 'copy', label: 'copy label'},
              {name: 'paste', label: 'paste label'},
            ]}
            onAccessibilityAction={event => {
              switch (event.nativeEvent.actionName) {
                case 'cut':
                  Alert.alert('Alert', 'cut action success');
                  break;
                case 'copy':
                  Alert.alert('Alert', 'copy action success');
                  break;
                case 'paste':
                  Alert.alert('Alert', 'paste action success');
                  break;
              }
            }}>
            <Text>This view supports many actions.</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="Adjustable with increment/decrement actions">
          <View
            accessible={true}
            accessibilityRole="adjustable"
            accessibilityActions={[{name: 'increment'}, {name: 'decrement'}]}
            onAccessibilityAction={event => {
              switch (event.nativeEvent.actionName) {
                case 'increment':
                  Alert.alert('Alert', 'increment action success');
                  break;
                case 'decrement':
                  Alert.alert('Alert', 'decrement action success');
                  break;
              }
            }}>
            <Text>Slider</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="TouchableWithoutFeedback with custom accessibility actions">
          <TouchableWithoutFeedback
            accessible={true}
            accessibilityActions={[
              {name: 'cut', label: 'cut label'},
              {name: 'copy', label: 'copy label'},
              {name: 'paste', label: 'paste label'},
            ]}
            onAccessibilityAction={event => {
              switch (event.nativeEvent.actionName) {
                case 'cut':
                  Alert.alert('Alert', 'cut action success');
                  break;
                case 'copy':
                  Alert.alert('Alert', 'copy action success');
                  break;
                case 'paste':
                  Alert.alert('Alert', 'paste action success');
                  break;
              }
            }}
            onPress={() => Alert.alert('Button has been pressed!')}
            accessibilityRole="button">
            <View>
              <Text>Click me</Text>
            </View>
          </TouchableWithoutFeedback>
        </RNTesterBlock>

        <RNTesterBlock title="Button with accessibility actions">
          <Button
            accessible={true}
            accessibilityActions={[
              {name: 'activate', label: 'activate label'},
              {name: 'copy', label: 'copy label'},
            ]}
            onAccessibilityAction={event => {
              switch (event.nativeEvent.actionName) {
                case 'activate':
                  Alert.alert('Alert', 'Activate accessiblity action');
                  break;
                case 'copy':
                  Alert.alert('Alert', 'copy action success');
                  break;
              }
            }}
            onPress={() => Alert.alert('Button has been pressed!')}
            title="Button with accessiblity action"
          />
        </RNTesterBlock>

        <RNTesterBlock title="Text with custom accessibility actions">
          <Text
            accessible={true}
            accessibilityActions={[
              {name: 'activate', label: 'activate label'},
              {name: 'copy', label: 'copy label'},
            ]}
            onAccessibilityAction={event => {
              switch (event.nativeEvent.actionName) {
                case 'activate':
                  Alert.alert('Alert', 'Activate accessiblity action');
                  break;
                case 'copy':
                  Alert.alert('Alert', 'copy action success');
                  break;
              }
            }}>
            Text
          </Text>
        </RNTesterBlock>
      </View>
    );
  }
}

function SliderAccessibilityExample(): React.Node {
  return (
    <View>
      <RNTesterBlock
        title="Disabled Slider via disabled"
        description="Verify with TalkBack/VoiceOver announces Slider as disabled">
        <Slider value={25} maximumValue={100} minimumValue={0} disabled />
      </RNTesterBlock>
      <RNTesterBlock
        title="Disabled Slider via accessibiltyState"
        description="Verify with TalkBack/VoiceOver announces Slider as disabled">
        <Slider
          value={75}
          maximumValue={100}
          minimumValue={0}
          accessibilityState={{disabled: true}}
        />
      </RNTesterBlock>
      <RNTesterBlock
        title="Selected Slider"
        description="Verify with TalkBack/VoiceOver announces Slider as selected">
        <Slider
          value={75}
          maximumValue={100}
          minimumValue={0}
          accessibilityState={{selected: true}}
        />
      </RNTesterBlock>
    </View>
  );
}

type FakeSliderExampleState = {
  current: number,
  textualValue: 'center' | 'left' | 'right',
};
class FakeSliderExample extends React.Component<{}, FakeSliderExampleState> {
  state: FakeSliderExampleState = {
    current: 50,
    textualValue: 'center',
  };

  increment: () => void = () => {
    let newValue = this.state.current + 2;
    if (newValue > 100) {
      newValue = 100;
    }
    this.setState({
      current: newValue,
    });
  };

  decrement: () => void = () => {
    let newValue = this.state.current - 2;
    if (newValue < 0) {
      newValue = 0;
    }
    this.setState({
      current: newValue,
    });
  };

  render(): React.Node {
    return (
      <View>
        <View
          accessible={true}
          accessibilityLabel="Fake Slider"
          accessibilityRole="adjustable"
          accessibilityActions={[{name: 'increment'}, {name: 'decrement'}]}
          onAccessibilityAction={event => {
            switch (event.nativeEvent.actionName) {
              case 'increment':
                this.increment();
                break;
              case 'decrement':
                this.decrement();
                break;
            }
          }}
          accessibilityValue={{
            min: 0,
            now: this.state.current,
            max: 100,
          }}>
          <Text>Fake Slider</Text>
        </View>
        <TouchableWithoutFeedback
          accessible={true}
          accessibilityLabel="Equalizer"
          accessibilityRole="adjustable"
          accessibilityActions={[{name: 'increment'}, {name: 'decrement'}]}
          onAccessibilityAction={event => {
            switch (event.nativeEvent.actionName) {
              case 'increment':
                if (this.state.textualValue === 'center') {
                  this.setState({textualValue: 'right'});
                } else if (this.state.textualValue === 'left') {
                  this.setState({textualValue: 'center'});
                }
                break;
              case 'decrement':
                if (this.state.textualValue === 'center') {
                  this.setState({textualValue: 'left'});
                } else if (this.state.textualValue === 'right') {
                  this.setState({textualValue: 'center'});
                }
                break;
            }
          }}
          accessibilityValue={{text: this.state.textualValue}}>
          <View>
            <Text>Equalizer</Text>
          </View>
        </TouchableWithoutFeedback>
      </View>
    );
  }
}

class AnnounceForAccessibility extends React.Component<{}> {
  _handleOnPress = () =>
    setTimeout(
      () => AccessibilityInfo.announceForAccessibility('Announcement Test'),
      1000,
    );

  _handleOnPressQueued = () =>
    setTimeout(
      () =>
        AccessibilityInfo.announceForAccessibilityWithOptions(
          'Queued Announcement Test',
          {queue: true},
        ),
      1000,
    );

  _handleOnPressQueueMultiple = () => {
    setTimeout(
      () =>
        AccessibilityInfo.announceForAccessibilityWithOptions(
          'First Queued Announcement Test',
          {queue: true},
        ),
      1000,
    );
    setTimeout(
      () =>
        AccessibilityInfo.announceForAccessibilityWithOptions(
          'Second Queued Announcement Test',
          {queue: true},
        ),
      1100,
    );
    setTimeout(
      () =>
        AccessibilityInfo.announceForAccessibilityWithOptions(
          'Third Queued Announcement Test',
          {queue: true},
        ),
      1200,
    );
  };

  render(): React.Node {
    return Platform.OS === 'ios' ? (
      <View>
        <Button
          onPress={this._handleOnPress}
          title="Announce for Accessibility Immediately"
        />
        <Button
          onPress={this._handleOnPressQueued}
          title="Announce for Accessibility Queued"
        />
        <Button
          onPress={this._handleOnPressQueueMultiple}
          title="Announce for Accessibility Queue Multiple"
        />
      </View>
    ) : (
      <View>
        <Button
          onPress={this._handleOnPress}
          title="Announce for Accessibility"
        />
      </View>
    );
  }
}

class SetAccessibilityFocusExample extends React.Component<{}> {
  render(): React.Node {
    const myRef: {current: React.ElementRef<any> | null} = createRef();

    const onClose = () => {
      if (myRef && myRef.current) {
        AccessibilityInfo.sendAccessibilityEvent_unstable(
          myRef.current,
          'focus',
        );
      }
    };

    return (
      <View>
        <Text>SetAccessibilityFocus on native element</Text>
        <Button
          ref={myRef}
          title={'Click'}
          onPress={() => {
            Alert.alert(
              'Set Accessibility Focus',
              'Press okay to proceed',
              [{text: 'Okay', onPress: onClose}],
              {cancelable: true},
            );
          }}
        />
      </View>
    );
  }
}

class EnabledExamples extends React.Component<{}> {
  render(): React.Node {
    return (
      <View>
        {Platform.OS === 'ios' ? (
          <>
            <RNTesterBlock title="isBoldTextEnabled()">
              <EnabledExample
                test="bold text"
                eventListener="boldTextChanged"
              />
            </RNTesterBlock>
            <RNTesterBlock title="isGrayScaleEnabled()">
              <EnabledExample
                test="gray scale"
                eventListener="grayscaleChanged"
              />
            </RNTesterBlock>
            <RNTesterBlock title="isInvertColorsEnabled()">
              <EnabledExample
                test="invert colors"
                eventListener="invertColorsChanged"
              />
            </RNTesterBlock>
            <RNTesterBlock title="isReduceTransparencyEnabled()">
              <EnabledExample
                test="reduce transparency"
                eventListener="reduceTransparencyChanged"
              />
            </RNTesterBlock>
          </>
        ) : null}

        {Platform.OS === 'android' ? (
          <RNTesterBlock
            title="isAccessibilityServiceEnabled()"
            description={
              'Event emitted whenever an accessibility service is enabled. This includes TalkBack as well as assistive technologies such as "Select to Speak".'
            }>
            <EnabledExample
              test="any accessibility service"
              eventListener="accessibilityServiceChanged"
            />
          </RNTesterBlock>
        ) : null}

        <RNTesterBlock title="isReduceMotionEnabled()">
          <EnabledExample
            test="reduce motion"
            eventListener="reduceMotionChanged"
          />
        </RNTesterBlock>

        <RNTesterBlock title="isScreenReaderEnabled()">
          <EnabledExample
            test="screen reader"
            eventListener="screenReaderChanged"
          />
        </RNTesterBlock>
      </View>
    );
  }
}

class EnabledExample extends React.Component<
  {
    eventListener:
      | 'reduceMotionChanged'
      | 'boldTextChanged'
      | 'grayscaleChanged'
      | 'invertColorsChanged'
      | 'reduceTransparencyChanged'
      | 'reduceMotionChanged'
      | 'screenReaderChanged'
      | 'accessibilityServiceChanged',
    test: string,
  },
  {
    isEnabled: boolean,
  },
> {
  state = {
    isEnabled: false,
  };
  _subscription: EventSubscription;
  componentDidMount() {
    this._subscription = AccessibilityInfo.addEventListener(
      this.props.eventListener,
      this._handleToggled,
    );

    switch (this.props.eventListener) {
      case 'reduceMotionChanged':
        return AccessibilityInfo.isReduceMotionEnabled().then(state => {
          this.setState({isEnabled: state});
        });
      case 'accessibilityServiceChanged':
        return AccessibilityInfo.isAccessibilityServiceEnabled().then(state => {
          this.setState({isEnabled: state});
        });
      default:
        return null;
    }
  }

  componentWillUnmount() {
    this._subscription?.remove();
  }

  _handleToggled = (isEnabled: void | PressEvent | boolean) => {
    if (!this.state.isEnabled) {
      this.setState({isEnabled: true});
    } else {
      this.setState({isEnabled: false});
    }
  };

  render(): React.Node {
    return (
      <View>
        <Text>
          The {this.props.test} is{' '}
          {this.state.isEnabled ? 'enabled' : 'disabled'}
        </Text>
        <Button
          title={this.state.isEnabled ? 'disable' : 'enable'}
          onPress={this._handleToggled}
        />
      </View>
    );
  }
}

class DisplayOptionsStatusExample extends React.Component<{}> {
  render(): React.Node {
    const isAndroid = Platform.OS === 'android';
    return (
      <View>
        <DisplayOptionStatusExample
          optionName={'Reduce Motion'}
          optionChecker={AccessibilityInfo.isReduceMotionEnabled}
          notification={'reduceMotionChanged'}
        />
        <DisplayOptionStatusExample
          optionName={'Screen Reader'}
          optionChecker={AccessibilityInfo.isScreenReaderEnabled}
          notification={'screenReaderChanged'}
        />
        {isAndroid ? null : (
          <>
            <DisplayOptionStatusExample
              optionName={'Bold Text'}
              optionChecker={AccessibilityInfo.isBoldTextEnabled}
              notification={'boldTextChanged'}
            />
            <DisplayOptionStatusExample
              optionName={'Grayscale'}
              optionChecker={AccessibilityInfo.isGrayscaleEnabled}
              notification={'grayscaleChanged'}
            />
            <DisplayOptionStatusExample
              optionName={'Invert Colors'}
              optionChecker={AccessibilityInfo.isInvertColorsEnabled}
              notification={'invertColorsChanged'}
            />
            <DisplayOptionStatusExample
              optionName={'Reduce Transparency'}
              optionChecker={AccessibilityInfo.isReduceTransparencyEnabled}
              notification={'reduceTransparencyChanged'}
            />
          </>
        )}
      </View>
    );
  }
}

function DisplayOptionStatusExample({optionName, optionChecker, notification}) {
  const [statusEnabled, setStatusEnabled] = React.useState(false);
  React.useEffect(() => {
    AccessibilityInfo.addEventListener(notification, setStatusEnabled);
    optionChecker().then(isEnabled => {
      setStatusEnabled(isEnabled);
    });
    return function cleanup() {
      AccessibilityInfo.removeEventListener(notification, setStatusEnabled);
    };
  }, [optionChecker, notification]);
  return (
    <View>
      <Text>
        {optionName}
        {' is '}
        {statusEnabled ? 'enabled' : 'disabled'}.
      </Text>
    </View>
  );
}

exports.title = 'Accessibility';
exports.documentationURL = 'https://reactnative.dev/docs/accessibilityinfo';
exports.description = 'Examples of using Accessibility APIs.';
exports.examples = [
  {
    title: 'Accessibility elements',
    render(): React.Element<typeof AccessibilityExample> {
      return <AccessibilityExample />;
    },
  },
  {
    title: 'New accessibility roles and states',
    render(): React.Element<typeof AccessibilityRoleAndStateExample> {
      return <AccessibilityRoleAndStateExample />;
    },
  },
  {
    title: 'Accessibility action examples',
    render(): React.Element<typeof AccessibilityActionsExample> {
      return <AccessibilityActionsExample />;
    },
  },
  {
    title: 'Slider Accessibility Examples',
    render(): React.Element<typeof SliderAccessibilityExample> {
      return <SliderAccessibilityExample />;
    },
  },
  {
    title: 'Fake Slider Example',
    render(): React.Element<typeof FakeSliderExample> {
      return <FakeSliderExample />;
    },
  },
  {
    title: 'Check if the display options are enabled',
    render(): React.Element<typeof DisplayOptionsStatusExample> {
      return <DisplayOptionsStatusExample />;
    },
  },
  {
    title: 'Check if the screen reader announces',
    render(): React.Element<typeof AnnounceForAccessibility> {
      return <AnnounceForAccessibility />;
    },
  },
  {
    title: 'Check if accessibility is focused',
    render(): React.Element<typeof SetAccessibilityFocusExample> {
      return <SetAccessibilityFocusExample />;
    },
  },
  {
    title: 'Check if these properties are enabled',
    render(): React.Element<typeof EnabledExamples> {
      return <EnabledExamples />;
    },
  },
  {
    title:
      'Check if accessibilityState disabled is announced when the screenreader focus moves on the image',
    render(): React.Element<typeof Image> {
      return (
        <Image
          accessible={true}
          accessibilityLabel="plain local image"
          accessibilityState={{disabled: true}}
          source={require('../../assets/like.png')}
          style={styles.disabledImage}
        />
      );
    },
  },
];

2022-03-01.09-20-28.mp4

@fabOnReact
Copy link
Owner Author

fabOnReact commented Mar 1, 2022

Testing accessibility android examples in main branch

CLICK TO OPEN SOURCECODE

/**
 * 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 (
      <RNTesterPage title={'Accessibility Android APIs'}>
        <RNTesterBlock title="LiveRegion">
          <TouchableWithoutFeedback onPress={this._addOne}>
            <View style={styles.embedded}>
              <Text>Click me</Text>
            </View>
          </TouchableWithoutFeedback>
          <View accessibilityLiveRegion="polite">
            <Text>Clicked {this.state.count} times</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="Overlapping views and importantForAccessibility property">
          <View style={styles.container}>
            <TouchableWithoutFeedback
              accessible={true}
              accessibilityLabel="First layout"
              importantForAccessibility={
                importantForAccessibilityValues[
                  this.state.backgroundImportantForAcc
                ]
              }>
              <View accessible={true} style={styles.touchableContainer}>
                <Text style={{fontSize: 25}}>Hello</Text>
              </View>
            </TouchableWithoutFeedback>
            <View
              style={{
                position: 'absolute',
                left: 10,
                top: 25,
                right: 10,
                height: 110,
                backgroundColor: 'yellow',
                opacity: 0.5,
              }}
              accessible={true}
              accessibilityLabel="Second layout"
              importantForAccessibility={
                importantForAccessibilityValues[
                  this.state.forgroundImportantForAcc
                ]
              }>
              <View accessible={true}>
                <Text style={{fontSize: 20}}>world</Text>
              </View>
            </View>
          </View>
          <TouchableWithoutFeedback
            onPress={this._changeBackgroundImportantForAcc}>
            <View style={styles.embedded}>
              <Text>
                Change importantForAccessibility for background layout.
              </Text>
            </View>
          </TouchableWithoutFeedback>
          <View accessible={true}>
            <Text>Background layout importantForAccessibility</Text>
            <Text>
              {
                importantForAccessibilityValues[
                  this.state.backgroundImportantForAcc
                ]
              }
            </Text>
          </View>
          <TouchableWithoutFeedback
            onPress={this._changeForgroundImportantForAcc}>
            <View style={styles.embedded}>
              <Text>
                Change importantForAccessibility for forground layout.
              </Text>
            </View>
          </TouchableWithoutFeedback>
          <View accessible={true}>
            <Text>Forground layout importantForAccessibility</Text>
            <Text>
              {
                importantForAccessibilityValues[
                  this.state.forgroundImportantForAcc
                ]
              }
            </Text>
          </View>
        </RNTesterBlock>
      </RNTesterPage>
    );
  }
}

const styles = StyleSheet.create({
  touchableContainer: {
    position: 'absolute',
    left: 10,
    top: 10,
    right: 10,
    height: 100,
    backgroundColor: 'green',
  },
  embedded: {
    backgroundColor: 'yellow',
    padding: 10,
  },
  container: {
    flex: 1,
    backgroundColor: 'white',
    padding: 10,
    height: 150,
  },
});

exports.title = 'AccessibilityAndroid';
exports.description = 'Android specific Accessibility APIs.';
exports.examples = [
  {
    title: 'Accessibility elements',
    render(): React.Element<typeof AccessibilityAndroidExample> {
      return <AccessibilityAndroidExample />;
    },
  },
];

accessibilityAndroidMain.mp4

@fabOnReact
Copy link
Owner Author

fabOnReact commented Mar 1, 2022

Testing accessibility examples in pr branch

CLICK TO OPEN SOURCECODE

/**
 * 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 (
      <View>
        <RNTesterBlock title="TextView without label">
          <Text>
            Text's accessibilityLabel is the raw text itself unless it is set
            explicitly.
          </Text>
        </RNTesterBlock>

        <RNTesterBlock title="TextView with label">
          <Text accessibilityLabel="I have label, so I read it instead of embedded text.">
            This text component's accessibilityLabel is set explicitly.
          </Text>
        </RNTesterBlock>

        <RNTesterBlock title="Nonaccessible view with TextViews">
          <View>
            <Text style={{color: 'green'}}>This is text one.</Text>
            <Text style={{color: 'blue'}}>This is text two.</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="Accessible view with TextViews wihout label">
          <View accessible={true}>
            <Text style={{color: 'green'}}>This is text one.</Text>
            <Text style={{color: 'blue'}}>This is text two.</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="Accessible view with TextViews with label">
          <View
            accessible={true}
            accessibilityLabel="I have label, so I read it instead of embedded text.">
            <Text style={{color: 'green'}}>This is text one.</Text>
            <Text style={{color: 'blue'}}>This is text two.</Text>
          </View>
        </RNTesterBlock>

        {/* Android screen readers will say the accessibility hint instead of the text
           since the view doesn't have a label. */}
        <RNTesterBlock title="Accessible view with TextViews with hint">
          <View accessibilityHint="Accessibility hint." accessible={true}>
            <Text style={{color: 'green'}}>This is text one.</Text>
            <Text style={{color: 'blue'}}>This is text two.</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="Accessible view TextViews with label and hint">
          <View
            accessibilityLabel="Accessibility label."
            accessibilityHint="Accessibility hint."
            accessible={true}>
            <Text style={{color: 'green'}}>This is text one.</Text>
            <Text style={{color: 'blue'}}>This is text two.</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="Text with accessibilityRole = header">
          <Text accessibilityRole="header">This is a title.</Text>
        </RNTesterBlock>

        <RNTesterBlock title="Touchable with accessibilityRole = link">
          <TouchableOpacity
            onPress={() => Alert.alert('Link has been clicked!')}
            accessibilityRole="link">
            <View>
              <Text>Click me</Text>
            </View>
          </TouchableOpacity>
        </RNTesterBlock>

        <RNTesterBlock title="Touchable with accessibilityRole = button">
          <TouchableOpacity
            onPress={() => Alert.alert('Button has been pressed!')}
            accessibilityRole="button">
            <Text>Click me</Text>
          </TouchableOpacity>
        </RNTesterBlock>

        <RNTesterBlock title="Disabled Touchable with role">
          <TouchableOpacity
            onPress={() => Alert.alert('Button has been pressed!')}
            accessibilityRole="button"
            accessibilityState={{disabled: true}}
            disabled={true}>
            <View>
              <Text>
                I am disabled. Clicking me will not trigger any action.
              </Text>
            </View>
          </TouchableOpacity>
        </RNTesterBlock>

        <RNTesterBlock title="Disabled TouchableOpacity">
          <TouchableOpacity
            onPress={() => Alert.alert('Disabled Button has been pressed!')}
            accessibilityLabel={'You are pressing Disabled TouchableOpacity'}
            accessibilityState={{disabled: true}}>
            <View>
              <Text>
                I am disabled. Clicking me will not trigger any action.
              </Text>
            </View>
          </TouchableOpacity>
        </RNTesterBlock>
        <RNTesterBlock title="View with multiple states">
          <View
            accessible={true}
            accessibilityState={{selected: true, disabled: true}}>
            <Text>This view is selected and disabled.</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="View with label, hint, role, and state">
          <View
            accessible={true}
            accessibilityLabel="Accessibility label."
            accessibilityRole="button"
            accessibilityState={{selected: true}}
            accessibilityHint="Accessibility hint.">
            <Text>Accessible view with label, hint, role, and state</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="TextInput with accessibilityLabelledBy attribute">
          <View>
            <Text nativeID="formLabel1">Mail Address</Text>
            <TextInput
              accessibilityLabel="input test1"
              accessibilityLabelledBy="formLabel1"
              style={styles.default}
            />
            <Text nativeID="formLabel2">First Name</Text>
            <TextInput
              accessibilityLabel="input test2"
              accessibilityLabelledBy={['formLabel2', 'formLabel3']}
              style={styles.default}
              value="Foo"
            />
          </View>
        </RNTesterBlock>
      </View>
    );
  }
}

class CheckboxExample extends React.Component<
  {},
  {
    checkboxState: boolean | 'mixed',
  },
> {
  state = {
    checkboxState: true,
  };

  _onCheckboxPress = () => {
    let checkboxState = false;
    if (this.state.checkboxState === false) {
      checkboxState = 'mixed';
    } else if (this.state.checkboxState === 'mixed') {
      checkboxState = true;
    } else {
      checkboxState = false;
    }

    this.setState({
      checkboxState: checkboxState,
    });
  };

  render() {
    return (
      <TouchableOpacity
        onPress={this._onCheckboxPress}
        accessibilityLabel="element 2"
        accessibilityRole="checkbox"
        accessibilityState={{checked: this.state.checkboxState}}
        accessibilityHint="click me to change state">
        <Text>Checkbox example</Text>
      </TouchableOpacity>
    );
  }
}

class SwitchExample extends React.Component<
  {},
  {
    switchState: boolean,
  },
> {
  state = {
    switchState: true,
  };

  _onSwitchToggle = () => {
    const switchState = !this.state.switchState;

    this.setState({
      switchState: switchState,
    });
  };

  render() {
    return (
      <TouchableOpacity
        onPress={this._onSwitchToggle}
        accessibilityLabel="element 12"
        accessibilityRole="switch"
        accessibilityState={{checked: this.state.switchState}}
        accessible={true}>
        <Text>Switch example</Text>
      </TouchableOpacity>
    );
  }
}

class SelectionExample extends React.Component<
  {},
  {
    isSelected: boolean,
    isEnabled: boolean,
  },
> {
  constructor(props: {}) {
    super(props);
    this.selectableElement = createRef();
  }
  selectableElement: {
    current: React.ElementRef<typeof TouchableOpacity> | null,
  };

  state = {
    isSelected: true,
    isEnabled: false,
  };

  render(): React.Node {
    const {isSelected, isEnabled} = this.state;
    let accessibilityHint = 'click me to select';
    if (isSelected) {
      accessibilityHint = 'click me to unselect';
    }
    if (!isEnabled) {
      accessibilityHint = 'use the button on the right to enable selection';
    }
    let buttonTitle = isEnabled ? 'Disable selection' : 'Enable selection';
    const touchableHint = ` (touching the TouchableOpacity will ${
      isSelected ? 'disable' : 'enable'
    } accessibilityState.selected)`;
    return (
      <View style={styles.containerAlignCenter}>
        <TouchableOpacity
          ref={this.selectableElement}
          accessible={true}
          onPress={() => {
            if (isEnabled) {
              this.setState({
                isSelected: !isSelected,
              });
            } else {
              console.warn('selection is disabled, please enable selection.');
            }
          }}
          accessibilityLabel="element 19"
          accessibilityState={{
            selected: isSelected,
            disabled: !isEnabled,
          }}
          style={styles.touchable}
          accessibilityHint={accessibilityHint}>
          <Text style={{color: 'white'}}>
            {`Selectable TouchableOpacity Example ${touchableHint}`}
          </Text>
        </TouchableOpacity>
        <TextInput
          accessibilityLabel="element 20"
          accessibilityState={{
            selected: isSelected,
          }}
          multiline={true}
          placeholder={`TextInput Example - ${
            isSelected ? 'enabled' : 'disabled'
          } selection`}
        />
        <Button
          onPress={() => {
            this.setState({
              isEnabled: !this.state.isEnabled,
            });
          }}
          title={buttonTitle}
        />
      </View>
    );
  }
}

class ExpandableElementExample extends React.Component<
  {},
  {
    expandState: boolean,
  },
> {
  state = {
    expandState: false,
  };

  _onElementPress = () => {
    const expandState = !this.state.expandState;

    this.setState({
      expandState: expandState,
    });
  };

  render() {
    return (
      <TouchableOpacity
        onPress={this._onElementPress}
        accessibilityLabel="element 18"
        accessibilityState={{expanded: this.state.expandState}}
        accessibilityHint="click me to change state">
        <Text>Expandable element example</Text>
      </TouchableOpacity>
    );
  }
}

class NestedCheckBox extends React.Component<
  {},
  {
    checkbox1: boolean | 'mixed',
    checkbox2: boolean | 'mixed',
    checkbox3: boolean | 'mixed',
  },
> {
  state = {
    checkbox1: false,
    checkbox2: false,
    checkbox3: false,
  };

  _onPress1 = () => {
    let checkbox1 = false;
    if (this.state.checkbox1 === false) {
      checkbox1 = true;
    } else if (this.state.checkbox1 === 'mixed') {
      checkbox1 = false;
    } else {
      checkbox1 = false;
    }
    setTimeout(() => {
      this.setState({
        checkbox1: checkbox1,
        checkbox2: checkbox1,
        checkbox3: checkbox1,
      });
    }, 2000);
  };

  _onPress2 = () => {
    const checkbox2 = !this.state.checkbox2;

    this.setState({
      checkbox2: checkbox2,
      checkbox1:
        checkbox2 && this.state.checkbox3
          ? true
          : checkbox2 || this.state.checkbox3
          ? 'mixed'
          : false,
    });
  };

  _onPress3 = () => {
    const checkbox3 = !this.state.checkbox3;

    this.setState({
      checkbox3: checkbox3,
      checkbox1:
        this.state.checkbox2 && checkbox3
          ? true
          : this.state.checkbox2 || checkbox3
          ? 'mixed'
          : false,
    });
  };

  render() {
    return (
      <View>
        <TouchableOpacity
          style={{flex: 1, flexDirection: 'row'}}
          onPress={this._onPress1}
          accessibilityLabel="Meat"
          accessibilityHint="State changes in 2 seconds after clicking."
          accessibilityRole="checkbox"
          accessibilityState={{checked: this.state.checkbox1}}>
          <Image
            style={styles.image}
            source={
              this.state.checkbox1 === 'mixed'
                ? mixedCheckboxImageSource
                : this.state.checkbox1
                ? checkImageSource
                : uncheckImageSource
            }
          />
          <Text>Meat</Text>
        </TouchableOpacity>
        <TouchableOpacity
          style={{flex: 1, flexDirection: 'row'}}
          onPress={this._onPress2}
          accessibilityLabel="Beef"
          accessibilityRole="checkbox"
          accessibilityState={{checked: this.state.checkbox2}}>
          <Image
            style={styles.image}
            source={
              this.state.checkbox2 ? checkImageSource : uncheckImageSource
            }
          />
          <Text>Beef</Text>
        </TouchableOpacity>
        <TouchableOpacity
          style={{flex: 1, flexDirection: 'row'}}
          onPress={this._onPress3}
          accessibilityLabel="Bacon"
          accessibilityRole="checkbox"
          accessibilityState={{checked: this.state.checkbox3}}>
          <Image
            style={styles.image}
            source={
              this.state.checkbox3 ? checkImageSource : uncheckImageSource
            }
          />
          <Text>Bacon</Text>
        </TouchableOpacity>
      </View>
    );
  }
}

class AccessibilityRoleAndStateExample extends React.Component<{}> {
  render(): React.Node {
    return (
      <View>
        <View
          accessibilityLabel="element 1"
          accessibilityRole="alert"
          accessible={true}>
          <Text>Alert example</Text>
        </View>
        <CheckboxExample />
        <View
          accessibilityLabel="element 3"
          accessibilityRole="combobox"
          accessible={true}>
          <Text>Combobox example</Text>
        </View>
        <View
          accessibilityLabel="element 4"
          accessibilityRole="menu"
          accessible={true}>
          <Text>Menu example</Text>
        </View>
        <View
          accessibilityLabel="element 5"
          accessibilityRole="menubar"
          accessible={true}>
          <Text>Menu bar example</Text>
        </View>
        <View
          accessibilityLabel="element 6"
          accessibilityRole="menuitem"
          accessible={true}>
          <Text>Menu item example</Text>
        </View>
        <View
          accessibilityLabel="element 7"
          accessibilityRole="progressbar"
          accessible={true}>
          <Text>Progress bar example</Text>
        </View>
        <View
          accessibilityLabel="element 8"
          accessibilityRole="radio"
          accessible={true}>
          <Text>Radio button example</Text>
        </View>
        <View
          accessibilityLabel="element 9"
          accessibilityRole="radiogroup"
          accessible={true}>
          <Text>Radio group example</Text>
        </View>
        <View
          accessibilityLabel="element 10"
          accessibilityRole="scrollbar"
          accessible={true}>
          <Text>Scrollbar example</Text>
        </View>
        <View
          accessibilityLabel="element 11"
          accessibilityRole="spinbutton"
          accessible={true}>
          <Text>Spin button example</Text>
        </View>
        <SwitchExample />
        <View
          accessibilityLabel="element 13"
          accessibilityRole="tab"
          accessible={true}>
          <Text>Tab example</Text>
        </View>
        <View
          accessibilityLabel="element 14"
          accessibilityRole="tablist"
          accessible={true}>
          <Text>Tab list example</Text>
        </View>
        <View
          accessibilityLabel="element 15"
          accessibilityRole="timer"
          accessible={true}>
          <Text>Timer example</Text>
        </View>
        <View
          accessibilityLabel="element 16"
          accessibilityRole="toolbar"
          accessible={true}>
          <Text>Toolbar example</Text>
        </View>
        <View
          accessibilityLabel="element 17"
          accessibilityState={{busy: true}}
          accessible={true}>
          <Text>State busy example</Text>
        </View>
        <ExpandableElementExample />
        <SelectionExample />
        <RNTesterBlock title="Nested checkbox with delayed state change">
          <NestedCheckBox />
        </RNTesterBlock>
      </View>
    );
  }
}

class AccessibilityActionsExample extends React.Component<{}> {
  render(): React.Node {
    return (
      <View>
        <RNTesterBlock title="Non-touchable with activate action">
          <View
            accessible={true}
            accessibilityActions={[{name: 'activate'}]}
            onAccessibilityAction={event => {
              switch (event.nativeEvent.actionName) {
                case 'activate':
                  Alert.alert('Alert', 'View is clicked');
                  break;
              }
            }}>
            <Text>Click me</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="View with multiple actions">
          <View
            accessible={true}
            accessibilityActions={[
              {name: 'cut', label: 'cut label'},
              {name: 'copy', label: 'copy label'},
              {name: 'paste', label: 'paste label'},
            ]}
            onAccessibilityAction={event => {
              switch (event.nativeEvent.actionName) {
                case 'cut':
                  Alert.alert('Alert', 'cut action success');
                  break;
                case 'copy':
                  Alert.alert('Alert', 'copy action success');
                  break;
                case 'paste':
                  Alert.alert('Alert', 'paste action success');
                  break;
              }
            }}>
            <Text>This view supports many actions.</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="Adjustable with increment/decrement actions">
          <View
            accessible={true}
            accessibilityRole="adjustable"
            accessibilityActions={[{name: 'increment'}, {name: 'decrement'}]}
            onAccessibilityAction={event => {
              switch (event.nativeEvent.actionName) {
                case 'increment':
                  Alert.alert('Alert', 'increment action success');
                  break;
                case 'decrement':
                  Alert.alert('Alert', 'decrement action success');
                  break;
              }
            }}>
            <Text>Slider</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="TouchableWithoutFeedback with custom accessibility actions">
          <TouchableWithoutFeedback
            accessible={true}
            accessibilityActions={[
              {name: 'cut', label: 'cut label'},
              {name: 'copy', label: 'copy label'},
              {name: 'paste', label: 'paste label'},
            ]}
            onAccessibilityAction={event => {
              switch (event.nativeEvent.actionName) {
                case 'cut':
                  Alert.alert('Alert', 'cut action success');
                  break;
                case 'copy':
                  Alert.alert('Alert', 'copy action success');
                  break;
                case 'paste':
                  Alert.alert('Alert', 'paste action success');
                  break;
              }
            }}
            onPress={() => Alert.alert('Button has been pressed!')}
            accessibilityRole="button">
            <View>
              <Text>Click me</Text>
            </View>
          </TouchableWithoutFeedback>
        </RNTesterBlock>

        <RNTesterBlock title="Button with accessibility actions">
          <Button
            accessible={true}
            accessibilityActions={[
              {name: 'activate', label: 'activate label'},
              {name: 'copy', label: 'copy label'},
            ]}
            onAccessibilityAction={event => {
              switch (event.nativeEvent.actionName) {
                case 'activate':
                  Alert.alert('Alert', 'Activate accessiblity action');
                  break;
                case 'copy':
                  Alert.alert('Alert', 'copy action success');
                  break;
              }
            }}
            onPress={() => Alert.alert('Button has been pressed!')}
            title="Button with accessiblity action"
          />
        </RNTesterBlock>

        <RNTesterBlock title="Text with custom accessibility actions">
          <Text
            accessible={true}
            accessibilityActions={[
              {name: 'activate', label: 'activate label'},
              {name: 'copy', label: 'copy label'},
            ]}
            onAccessibilityAction={event => {
              switch (event.nativeEvent.actionName) {
                case 'activate':
                  Alert.alert('Alert', 'Activate accessiblity action');
                  break;
                case 'copy':
                  Alert.alert('Alert', 'copy action success');
                  break;
              }
            }}>
            Text
          </Text>
        </RNTesterBlock>
      </View>
    );
  }
}

function SliderAccessibilityExample(): React.Node {
  return (
    <View>
      <RNTesterBlock
        title="Disabled Slider via disabled"
        description="Verify with TalkBack/VoiceOver announces Slider as disabled">
        <Slider value={25} maximumValue={100} minimumValue={0} disabled />
      </RNTesterBlock>
      <RNTesterBlock
        title="Disabled Slider via accessibiltyState"
        description="Verify with TalkBack/VoiceOver announces Slider as disabled">
        <Slider
          value={75}
          maximumValue={100}
          minimumValue={0}
          accessibilityState={{disabled: true}}
        />
      </RNTesterBlock>
      <RNTesterBlock
        title="Selected Slider"
        description="Verify with TalkBack/VoiceOver announces Slider as selected">
        <Slider
          value={75}
          maximumValue={100}
          minimumValue={0}
          accessibilityState={{selected: true}}
        />
      </RNTesterBlock>
    </View>
  );
}

type FakeSliderExampleState = {
  current: number,
  textualValue: 'center' | 'left' | 'right',
};
class FakeSliderExample extends React.Component<{}, FakeSliderExampleState> {
  state: FakeSliderExampleState = {
    current: 50,
    textualValue: 'center',
  };

  increment: () => void = () => {
    let newValue = this.state.current + 2;
    if (newValue > 100) {
      newValue = 100;
    }
    this.setState({
      current: newValue,
    });
  };

  decrement: () => void = () => {
    let newValue = this.state.current - 2;
    if (newValue < 0) {
      newValue = 0;
    }
    this.setState({
      current: newValue,
    });
  };

  render(): React.Node {
    return (
      <View>
        <View
          accessible={true}
          accessibilityLabel="Fake Slider"
          accessibilityRole="adjustable"
          accessibilityActions={[{name: 'increment'}, {name: 'decrement'}]}
          onAccessibilityAction={event => {
            switch (event.nativeEvent.actionName) {
              case 'increment':
                this.increment();
                break;
              case 'decrement':
                this.decrement();
                break;
            }
          }}
          accessibilityValue={{
            min: 0,
            now: this.state.current,
            max: 100,
          }}>
          <Text>Fake Slider</Text>
        </View>
        <TouchableWithoutFeedback
          accessible={true}
          accessibilityLabel="Equalizer"
          accessibilityRole="adjustable"
          accessibilityActions={[{name: 'increment'}, {name: 'decrement'}]}
          onAccessibilityAction={event => {
            switch (event.nativeEvent.actionName) {
              case 'increment':
                if (this.state.textualValue === 'center') {
                  this.setState({textualValue: 'right'});
                } else if (this.state.textualValue === 'left') {
                  this.setState({textualValue: 'center'});
                }
                break;
              case 'decrement':
                if (this.state.textualValue === 'center') {
                  this.setState({textualValue: 'left'});
                } else if (this.state.textualValue === 'right') {
                  this.setState({textualValue: 'center'});
                }
                break;
            }
          }}
          accessibilityValue={{text: this.state.textualValue}}>
          <View>
            <Text>Equalizer</Text>
          </View>
        </TouchableWithoutFeedback>
      </View>
    );
  }
}

class AnnounceForAccessibility extends React.Component<{}> {
  _handleOnPress = () =>
    setTimeout(
      () => AccessibilityInfo.announceForAccessibility('Announcement Test'),
      1000,
    );

  _handleOnPressQueued = () =>
    setTimeout(
      () =>
        AccessibilityInfo.announceForAccessibilityWithOptions(
          'Queued Announcement Test',
          {queue: true},
        ),
      1000,
    );

  _handleOnPressQueueMultiple = () => {
    setTimeout(
      () =>
        AccessibilityInfo.announceForAccessibilityWithOptions(
          'First Queued Announcement Test',
          {queue: true},
        ),
      1000,
    );
    setTimeout(
      () =>
        AccessibilityInfo.announceForAccessibilityWithOptions(
          'Second Queued Announcement Test',
          {queue: true},
        ),
      1100,
    );
    setTimeout(
      () =>
        AccessibilityInfo.announceForAccessibilityWithOptions(
          'Third Queued Announcement Test',
          {queue: true},
        ),
      1200,
    );
  };

  render(): React.Node {
    return Platform.OS === 'ios' ? (
      <View>
        <Button
          onPress={this._handleOnPress}
          title="Announce for Accessibility Immediately"
        />
        <Button
          onPress={this._handleOnPressQueued}
          title="Announce for Accessibility Queued"
        />
        <Button
          onPress={this._handleOnPressQueueMultiple}
          title="Announce for Accessibility Queue Multiple"
        />
      </View>
    ) : (
      <View>
        <Button
          onPress={this._handleOnPress}
          title="Announce for Accessibility"
        />
      </View>
    );
  }
}

class SetAccessibilityFocusExample extends React.Component<{}> {
  render(): React.Node {
    const myRef: {current: React.ElementRef<any> | null} = createRef();

    const onClose = () => {
      if (myRef && myRef.current) {
        AccessibilityInfo.sendAccessibilityEvent_unstable(
          myRef.current,
          'focus',
        );
      }
    };

    return (
      <View>
        <Text>SetAccessibilityFocus on native element</Text>
        <Button
          ref={myRef}
          title={'Click'}
          onPress={() => {
            Alert.alert(
              'Set Accessibility Focus',
              'Press okay to proceed',
              [{text: 'Okay', onPress: onClose}],
              {cancelable: true},
            );
          }}
        />
      </View>
    );
  }
}

class EnabledExamples extends React.Component<{}> {
  render(): React.Node {
    return (
      <View>
        {Platform.OS === 'ios' ? (
          <>
            <RNTesterBlock title="isBoldTextEnabled()">
              <EnabledExample
                test="bold text"
                eventListener="boldTextChanged"
              />
            </RNTesterBlock>
            <RNTesterBlock title="isGrayScaleEnabled()">
              <EnabledExample
                test="gray scale"
                eventListener="grayscaleChanged"
              />
            </RNTesterBlock>
            <RNTesterBlock title="isInvertColorsEnabled()">
              <EnabledExample
                test="invert colors"
                eventListener="invertColorsChanged"
              />
            </RNTesterBlock>
            <RNTesterBlock title="isReduceTransparencyEnabled()">
              <EnabledExample
                test="reduce transparency"
                eventListener="reduceTransparencyChanged"
              />
            </RNTesterBlock>
          </>
        ) : null}

        {Platform.OS === 'android' ? (
          <RNTesterBlock
            title="isAccessibilityServiceEnabled()"
            description={
              'Event emitted whenever an accessibility service is enabled. This includes TalkBack as well as assistive technologies such as "Select to Speak".'
            }>
            <EnabledExample
              test="any accessibility service"
              eventListener="accessibilityServiceChanged"
            />
          </RNTesterBlock>
        ) : null}

        <RNTesterBlock title="isReduceMotionEnabled()">
          <EnabledExample
            test="reduce motion"
            eventListener="reduceMotionChanged"
          />
        </RNTesterBlock>

        <RNTesterBlock title="isScreenReaderEnabled()">
          <EnabledExample
            test="screen reader"
            eventListener="screenReaderChanged"
          />
        </RNTesterBlock>
      </View>
    );
  }
}

class EnabledExample extends React.Component<
  {
    eventListener:
      | 'reduceMotionChanged'
      | 'boldTextChanged'
      | 'grayscaleChanged'
      | 'invertColorsChanged'
      | 'reduceTransparencyChanged'
      | 'reduceMotionChanged'
      | 'screenReaderChanged'
      | 'accessibilityServiceChanged',
    test: string,
  },
  {
    isEnabled: boolean,
  },
> {
  state = {
    isEnabled: false,
  };
  _subscription: EventSubscription;
  componentDidMount() {
    this._subscription = AccessibilityInfo.addEventListener(
      this.props.eventListener,
      this._handleToggled,
    );

    switch (this.props.eventListener) {
      case 'reduceMotionChanged':
        return AccessibilityInfo.isReduceMotionEnabled().then(state => {
          this.setState({isEnabled: state});
        });
      case 'accessibilityServiceChanged':
        return AccessibilityInfo.isAccessibilityServiceEnabled().then(state => {
          this.setState({isEnabled: state});
        });
      default:
        return null;
    }
  }

  componentWillUnmount() {
    this._subscription?.remove();
  }

  _handleToggled = (isEnabled: void | PressEvent | boolean) => {
    if (!this.state.isEnabled) {
      this.setState({isEnabled: true});
    } else {
      this.setState({isEnabled: false});
    }
  };

  render(): React.Node {
    return (
      <View>
        <Text>
          The {this.props.test} is{' '}
          {this.state.isEnabled ? 'enabled' : 'disabled'}
        </Text>
        <Button
          title={this.state.isEnabled ? 'disable' : 'enable'}
          onPress={this._handleToggled}
        />
      </View>
    );
  }
}

class DisplayOptionsStatusExample extends React.Component<{}> {
  render(): React.Node {
    const isAndroid = Platform.OS === 'android';
    return (
      <View>
        <DisplayOptionStatusExample
          optionName={'Reduce Motion'}
          optionChecker={AccessibilityInfo.isReduceMotionEnabled}
          notification={'reduceMotionChanged'}
        />
        <DisplayOptionStatusExample
          optionName={'Screen Reader'}
          optionChecker={AccessibilityInfo.isScreenReaderEnabled}
          notification={'screenReaderChanged'}
        />
        {isAndroid ? null : (
          <>
            <DisplayOptionStatusExample
              optionName={'Bold Text'}
              optionChecker={AccessibilityInfo.isBoldTextEnabled}
              notification={'boldTextChanged'}
            />
            <DisplayOptionStatusExample
              optionName={'Grayscale'}
              optionChecker={AccessibilityInfo.isGrayscaleEnabled}
              notification={'grayscaleChanged'}
            />
            <DisplayOptionStatusExample
              optionName={'Invert Colors'}
              optionChecker={AccessibilityInfo.isInvertColorsEnabled}
              notification={'invertColorsChanged'}
            />
            <DisplayOptionStatusExample
              optionName={'Reduce Transparency'}
              optionChecker={AccessibilityInfo.isReduceTransparencyEnabled}
              notification={'reduceTransparencyChanged'}
            />
          </>
        )}
      </View>
    );
  }
}

function DisplayOptionStatusExample({optionName, optionChecker, notification}) {
  const [statusEnabled, setStatusEnabled] = React.useState(false);
  React.useEffect(() => {
    AccessibilityInfo.addEventListener(notification, setStatusEnabled);
    optionChecker().then(isEnabled => {
      setStatusEnabled(isEnabled);
    });
    return function cleanup() {
      AccessibilityInfo.removeEventListener(notification, setStatusEnabled);
    };
  }, [optionChecker, notification]);
  return (
    <View>
      <Text>
        {optionName}
        {' is '}
        {statusEnabled ? 'enabled' : 'disabled'}.
      </Text>
    </View>
  );
}

exports.title = 'Accessibility';
exports.documentationURL = 'https://reactnative.dev/docs/accessibilityinfo';
exports.description = 'Examples of using Accessibility APIs.';
exports.examples = [
  {
    title: 'Accessibility elements',
    render(): React.Element<typeof AccessibilityExample> {
      return <AccessibilityExample />;
    },
  },
  {
    title: 'New accessibility roles and states',
    render(): React.Element<typeof AccessibilityRoleAndStateExample> {
      return <AccessibilityRoleAndStateExample />;
    },
  },
  {
    title: 'Accessibility action examples',
    render(): React.Element<typeof AccessibilityActionsExample> {
      return <AccessibilityActionsExample />;
    },
  },
  {
    title: 'Slider Accessibility Examples',
    render(): React.Element<typeof SliderAccessibilityExample> {
      return <SliderAccessibilityExample />;
    },
  },
  {
    title: 'Fake Slider Example',
    render(): React.Element<typeof FakeSliderExample> {
      return <FakeSliderExample />;
    },
  },
  {
    title: 'Check if the display options are enabled',
    render(): React.Element<typeof DisplayOptionsStatusExample> {
      return <DisplayOptionsStatusExample />;
    },
  },
  {
    title: 'Check if the screen reader announces',
    render(): React.Element<typeof AnnounceForAccessibility> {
      return <AnnounceForAccessibility />;
    },
  },
  {
    title: 'Check if accessibility is focused',
    render(): React.Element<typeof SetAccessibilityFocusExample> {
      return <SetAccessibilityFocusExample />;
    },
  },
  {
    title: 'Check if these properties are enabled',
    render(): React.Element<typeof EnabledExamples> {
      return <EnabledExamples />;
    },
  },
  {
    title:
      'Check if accessibilityState disabled is announced when the screenreader focus moves on the image',
    render(): React.Element<typeof Image> {
      return (
        <Image
          accessible={true}
          accessibilityLabel="plain local image"
          accessibilityState={{disabled: true}}
          source={require('../../assets/like.png')}
          style={styles.disabledImage}
        />
      );
    },
  },
];

accessibilityBranchLinks.mp4

@fabOnReact
Copy link
Owner Author

fabOnReact commented Mar 1, 2022

Testing accessibility android examples in pr branch

CLICK TO OPEN SOURCECODE

/**
 * 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 (
      <RNTesterPage title={'Accessibility Android APIs'}>
        <RNTesterBlock title="LiveRegion">
          <TouchableWithoutFeedback onPress={this._addOne}>
            <View style={styles.embedded}>
              <Text>Click me</Text>
            </View>
          </TouchableWithoutFeedback>
          <View accessibilityLiveRegion="polite">
            <Text>Clicked {this.state.count} times</Text>
          </View>
        </RNTesterBlock>

        <RNTesterBlock title="Overlapping views and importantForAccessibility property">
          <View style={styles.container}>
            <TouchableWithoutFeedback
              accessible={true}
              accessibilityLabel="First layout"
              importantForAccessibility={
                importantForAccessibilityValues[
                  this.state.backgroundImportantForAcc
                ]
              }>
              <View accessible={true} style={styles.touchableContainer}>
                <Text style={{fontSize: 25}}>Hello</Text>
              </View>
            </TouchableWithoutFeedback>
            <View
              style={{
                position: 'absolute',
                left: 10,
                top: 25,
                right: 10,
                height: 110,
                backgroundColor: 'yellow',
                opacity: 0.5,
              }}
              accessible={true}
              accessibilityLabel="Second layout"
              importantForAccessibility={
                importantForAccessibilityValues[
                  this.state.forgroundImportantForAcc
                ]
              }>
              <View accessible={true}>
                <Text style={{fontSize: 20}}>world</Text>
              </View>
            </View>
          </View>
          <TouchableWithoutFeedback
            onPress={this._changeBackgroundImportantForAcc}>
            <View style={styles.embedded}>
              <Text>
                Change importantForAccessibility for background layout.
              </Text>
            </View>
          </TouchableWithoutFeedback>
          <View accessible={true}>
            <Text>Background layout importantForAccessibility</Text>
            <Text>
              {
                importantForAccessibilityValues[
                  this.state.backgroundImportantForAcc
                ]
              }
            </Text>
          </View>
          <TouchableWithoutFeedback
            onPress={this._changeForgroundImportantForAcc}>
            <View style={styles.embedded}>
              <Text>
                Change importantForAccessibility for forground layout.
              </Text>
            </View>
          </TouchableWithoutFeedback>
          <View accessible={true}>
            <Text>Forground layout importantForAccessibility</Text>
            <Text>
              {
                importantForAccessibilityValues[
                  this.state.forgroundImportantForAcc
                ]
              }
            </Text>
          </View>
        </RNTesterBlock>
      </RNTesterPage>
    );
  }
}

const styles = StyleSheet.create({
  touchableContainer: {
    position: 'absolute',
    left: 10,
    top: 10,
    right: 10,
    height: 100,
    backgroundColor: 'green',
  },
  embedded: {
    backgroundColor: 'yellow',
    padding: 10,
  },
  container: {
    flex: 1,
    backgroundColor: 'white',
    padding: 10,
    height: 150,
  },
});

exports.title = 'AccessibilityAndroid';
exports.description = 'Android specific Accessibility APIs.';
exports.examples = [
  {
    title: 'Accessibility elements',
    render(): React.Element<typeof AccessibilityAndroidExample> {
      return <AccessibilityAndroidExample />;
    },
  },
];

CLICK TO OPEN VIDEO TESTS PART 1

accessibilityAndroidPrBranch1.mp4

CLICK TO OPEN VIDEO TESTS PART 2 - LINKS

2022-03-04.12-40-56.mp4

CLICK TO OPEN VIDEO TESTS PART 3 - LINKS

They should be focused in order from top to bottom after thecontents of the entire paragraph.

2022-03-04.12-47-55.mp4

@fabOnReact

This comment was marked as off-topic.

@fabOnReact

This comment was marked as outdated.

@fabOnReact

This comment was marked as off-topic.

@fabOnReact

This comment was marked as resolved.

@fabOnReact
Copy link
Owner Author

fabOnReact commented Mar 2, 2022

retrieve SPANS_START_KEY and SPANS_END_KEY from nodeInfo

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

List<Integer> starts = extrasIntList(SPANS_START_KEY);

https://github.com/androidx/androidx/blob/150112ca4e1e670fb4eb6756993f3a25df0a5da9/core/core/src/main/java/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.java#L2676

starts.get(i), ends.get(i), flags.get(i));

seems to be similar logic implemented in AccessibilityLinks

AccessibilityLinks constructor

https://github.com/fabriziobertoglio1987/react-native-notes/blob/4368f3a8ae3b5c11ffb2b5eb947f5a5aabb62524/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java#L616-L640

@fabOnReact

This comment was marked as off-topic.

@fabOnReact fabOnReact reopened this Mar 3, 2022
@fabOnReact

This comment was marked as off-topic.

@fabOnReact
Copy link
Owner Author

fabOnReact commented Mar 7, 2022

TalkBack focus moves through links IN THE CORRECT ORDER from top to bottom (PR Branch with link.id)

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:

  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 (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
2022-03-07.09-11-19.mp4

RESULT 2 (FAIL) - Links are NOT displayed in the correct order in the TalkBack menu

2022-03-07.09-13-23.mp4

@fabOnReact fabOnReact reopened this Mar 7, 2022
@fabOnReact
Copy link
Owner Author

fabOnReact commented Mar 7, 2022

TalkBack focus does NOT move through links in the correct order from top to bottom (PR Branch without link.id)

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:

  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 (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 (FAIL) - Swiping moves TalkBack focus in this wrong 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. Swipe right or down moves the focus to the last link link that spans multiple lines because the text is too long.
2022-03-07.08-07-55.mp4
RESULT 2 (FAIL) - Links are NOT displayed in the correct order in the TalkBack menu

2022-03-07.08-17-20.mp4

facebook-github-bot pushed a commit to facebook/react-native that referenced this issue Mar 29, 2022
Summary:
This issue fixes [32004][23]. The Pull Request was previously published by [blavalla][10] with [31757][24].
>This is a follow-up on [D23553222 (https://github.com/facebook/react-native/commit/b352e2da8137452f66717cf1cecb2e72abd727d7)][18], which made links functional by using [Talkback's Links menu][1]. 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.
The Implementation of the functionality consists of:

Retrieving the accessibility links and triggering the TalkBack Focus over the Text
1. nested Text components with accessibilityRole link are saved as [ReactClickableSpan][17] instances in Android native [TextView][20] ([more info][19])
1. If the TextView contains any [ClickableSpans][15] (which are [nested Text][14] components with role link), set a view tag and reset the accessibility delegate.
3. Obtain each link description, start, end, and position relative to the parent Text (id) from the Span as an [AccessibilityLink][16]
4. Use the [AccessibilityLink][16]  to display TalkBack focus over the link with the `getVirtualViewAt` method (more [info][13])

Implementing ExploreByTouchHelper to detect touches over links and to display TalkBack rectangle around them.
1. ReactAccessibilityDelegate inherits from [ExploreByTouchHelper][12]
2. If the [ReactTextView][21] has an accessibility delegate, trigger ExploreByTouchHelper method [dispatchHoverEvent][22]
3.  Implements the methods `getVirtualViewAt` and `onPopulateBoundsForVirtualView`.
     The two methods implements the following functionalities  (more [info][13]):
    * detecting the TalkBack onPress/focus on nested Text with accessibilityRole="link"
    * displaying TalkBack rectangle around nested Text with accessibilityRole="link"

## Changelog

[Android] [Added] - Make links independently focusable by Talkback

Pull Request resolved: #33215

Test Plan:
[1]. User Interacts with links through TalkBack default accessibility menu ([link][1])
[2]. The nested link becomes the next focusable element after the parent element that contains it. ([link][2])
[3]. Testing accessibility examples in pr branch ([link][3])
[4]. Testing accessibility android examples in pr branch ([link][4])
[7]. TalkBack focus moves through links in the correct order from top to bottom (PR Branch with [link.id][25]) ([link to video test][7]) ([discussion][26])
[8]. TalkBack focus does not move through links in the correct order from top to bottom (PR Branch without [link.id][25]) ([link to video test][8]) ([discussion][26])

Test on main branch
[5]. Testing accessibility examples in main branch ([link][5])
[6]. Testing accessibility android examples in main branch ([link][6])

[1]: fabOnReact/react-native-notes#9 (comment)
[2]: fabOnReact/react-native-notes#9 (comment)
[3]: fabOnReact/react-native-notes#9 (comment)
[4]: fabOnReact/react-native-notes#9 (comment)
[5]: fabOnReact/react-native-notes#9 (comment)
[6]: fabOnReact/react-native-notes#9 (comment)
[7]: fabOnReact/react-native-notes#9 (comment)
[8]: fabOnReact/react-native-notes#9 (comment)

[10]: https://github.com/blavalla "blavalla github profile"
[12]: https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/com/android/internal/widget/ExploreByTouchHelper.java#L48 "com/android/internal/widget/ExploreByTouchHelper.java#L48"
[13]: fabOnReact/react-native-notes#9 (comment) "explanation of getVirtualViewAt and onPopulateBoundsForVirtualView"
[14]: https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/android/text/Spannable.java#L3 "core/java/android/text/Spannable.java#L3"
[15]: https://github.com/fabriziobertoglio1987/react-native/blob/561266fc180b96d6337d6c6c5c3323522d66cc44/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java#L70-L71 "react/views/text/ReactTextViewManager.java#L70-L71"
[16]: https://github.com/fabriziobertoglio1987/react-native/blob/561266fc180b96d6337d6c6c5c3323522d66cc44/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java#L680-L685 "react/uimanager/ReactAccessibilityDelegate.java#L680-L685"
[17]: https://github.com/facebook/react-native/blob/561266fc180b96d6337d6c6c5c3323522d66cc44/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java#L126-L129 "react/views/text/TextLayoutManager.java#L126-L129"
[18]: b352e2d
[19]: #30375 (comment) "explanation on how nested Text are converted to Android Spans"
[20]: https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/android/widget/TextView.java#L214-L220 "core/java/android/widget/TextView.java#L214-L220"
[21]: https://github.com/facebook/react-native/blob/485cf6118b0ab0b59e078b96701b69ae64c4dfb7/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java#L577 "dispatchHoverEvent in ReactTextView"
[22]: https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/com/android/internal/widget/ExploreByTouchHelper.java#L120-L138 "dispatchHoverEvent in ExploreByTouchHelper"
[23]: #32004
[24]: #31757
[25]: https://github.com/fabriziobertoglio1987/react-native/blob/485cf6118b0ab0b59e078b96701b69ae64c4dfb7/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java#L648 "setting link.id in the AccessibilityLink constructor"
[26]: https://github.com/facebook/react-native/pull/33215/files/485cf6118b0ab0b59e078b96701b69ae64c4dfb7#r820014411 "comment on role of link.id"

Reviewed By: blavalla

Differential Revision: D34687371

Pulled By: philIip

fbshipit-source-id: 8e63c70e9318ad8d27317bd68497705e595dea0f
Saadnajmi pushed a commit to Saadnajmi/react-native-macos that referenced this issue Jan 15, 2023
Summary:
This issue fixes [32004][23]. The Pull Request was previously published by [blavalla][10] with [31757][24].
>This is a follow-up on [D23553222 (https://github.com/facebook/react-native/commit/b352e2da8137452f66717cf1cecb2e72abd727d7)][18], which made links functional by using [Talkback's Links menu][1]. 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.
The Implementation of the functionality consists of:

Retrieving the accessibility links and triggering the TalkBack Focus over the Text
1. nested Text components with accessibilityRole link are saved as [ReactClickableSpan][17] instances in Android native [TextView][20] ([more info][19])
1. If the TextView contains any [ClickableSpans][15] (which are [nested Text][14] components with role link), set a view tag and reset the accessibility delegate.
3. Obtain each link description, start, end, and position relative to the parent Text (id) from the Span as an [AccessibilityLink][16]
4. Use the [AccessibilityLink][16]  to display TalkBack focus over the link with the `getVirtualViewAt` method (more [info][13])

Implementing ExploreByTouchHelper to detect touches over links and to display TalkBack rectangle around them.
1. ReactAccessibilityDelegate inherits from [ExploreByTouchHelper][12]
2. If the [ReactTextView][21] has an accessibility delegate, trigger ExploreByTouchHelper method [dispatchHoverEvent][22]
3.  Implements the methods `getVirtualViewAt` and `onPopulateBoundsForVirtualView`.
     The two methods implements the following functionalities  (more [info][13]):
    * detecting the TalkBack onPress/focus on nested Text with accessibilityRole="link"
    * displaying TalkBack rectangle around nested Text with accessibilityRole="link"

## Changelog

[Android] [Added] - Make links independently focusable by Talkback

Pull Request resolved: facebook#33215

Test Plan:
[1]. User Interacts with links through TalkBack default accessibility menu ([link][1])
[2]. The nested link becomes the next focusable element after the parent element that contains it. ([link][2])
[3]. Testing accessibility examples in pr branch ([link][3])
[4]. Testing accessibility android examples in pr branch ([link][4])
[7]. TalkBack focus moves through links in the correct order from top to bottom (PR Branch with [link.id][25]) ([link to video test][7]) ([discussion][26])
[8]. TalkBack focus does not move through links in the correct order from top to bottom (PR Branch without [link.id][25]) ([link to video test][8]) ([discussion][26])

Test on main branch
[5]. Testing accessibility examples in main branch ([link][5])
[6]. Testing accessibility android examples in main branch ([link][6])

[1]: fabOnReact/react-native-notes#9 (comment)
[2]: fabOnReact/react-native-notes#9 (comment)
[3]: fabOnReact/react-native-notes#9 (comment)
[4]: fabOnReact/react-native-notes#9 (comment)
[5]: fabOnReact/react-native-notes#9 (comment)
[6]: fabOnReact/react-native-notes#9 (comment)
[7]: fabOnReact/react-native-notes#9 (comment)
[8]: fabOnReact/react-native-notes#9 (comment)

[10]: https://github.com/blavalla "blavalla github profile"
[12]: https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/com/android/internal/widget/ExploreByTouchHelper.java#L48 "com/android/internal/widget/ExploreByTouchHelper.java#L48"
[13]: fabOnReact/react-native-notes#9 (comment) "explanation of getVirtualViewAt and onPopulateBoundsForVirtualView"
[14]: https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/android/text/Spannable.java#L3 "core/java/android/text/Spannable.java#L3"
[15]: https://github.com/fabriziobertoglio1987/react-native/blob/561266fc180b96d6337d6c6c5c3323522d66cc44/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java#L70-L71 "react/views/text/ReactTextViewManager.java#L70-L71"
[16]: https://github.com/fabriziobertoglio1987/react-native/blob/561266fc180b96d6337d6c6c5c3323522d66cc44/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java#L680-L685 "react/uimanager/ReactAccessibilityDelegate.java#L680-L685"
[17]: https://github.com/facebook/react-native/blob/561266fc180b96d6337d6c6c5c3323522d66cc44/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java#L126-L129 "react/views/text/TextLayoutManager.java#L126-L129"
[18]: facebook@b352e2d
[19]: facebook#30375 (comment) "explanation on how nested Text are converted to Android Spans"
[20]: https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/android/widget/TextView.java#L214-L220 "core/java/android/widget/TextView.java#L214-L220"
[21]: https://github.com/facebook/react-native/blob/485cf6118b0ab0b59e078b96701b69ae64c4dfb7/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java#L577 "dispatchHoverEvent in ReactTextView"
[22]: https://github.com/aosp-mirror/platform_frameworks_base/blob/1ac46f932ef88a8f96d652580d8105e361ffc842/core/java/com/android/internal/widget/ExploreByTouchHelper.java#L120-L138 "dispatchHoverEvent in ExploreByTouchHelper"
[23]: facebook#32004
[24]: facebook#31757
[25]: https://github.com/fabriziobertoglio1987/react-native/blob/485cf6118b0ab0b59e078b96701b69ae64c4dfb7/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java#L648 "setting link.id in the AccessibilityLink constructor"
[26]: https://github.com/facebook/react-native/pull/33215/files/485cf6118b0ab0b59e078b96701b69ae64c4dfb7#r820014411 "comment on role of link.id"

Reviewed By: blavalla

Differential Revision: D34687371

Pulled By: philIip

fbshipit-source-id: 8e63c70e9318ad8d27317bd68497705e595dea0f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant