Zohar Chiprut
Zohar Chiprut

Reputation: 908

How to set properly RN AccessibilityInfo setAccessibilityFocus

React native App, testing in IOS. Desired behaviour: Inputing an invalid value in TextInput and clicking a submit button. Screen reader shell pronounce for the error and the focus shell return to the textInput. Setting the focus shell not open the keyboard.

Problem in the implementation: The return of the focus to the textInput seems to be before accessibility label is updated to the error message. Screen reader pronounce for the old accessibility Label. If touching the textInput after the pronounce of the Voice Over the updated accessibility Label with the error message is pronounce.

The code also available in expo snack, snack

import * as React from 'react';
import { AccessibilityInfo, TextInput, Button, Text, View, StyleSheet } from 'react-native';
import Constants from 'expo-constants';
import AssetExample from './components/AssetExample';

import { Card } from 'react-native-paper';

const errorBtn = "Click to simulate invalid text input";
const clearBtn = "Click to clear the error";
const errorLabel = "Wrong Number has being typed, try again";
const clearLabel = "Phone Number";
const description = 
"Enable Voice Over, If Error hapens on textInput, screen reader shell pronounce the error message and set focus to the textInput where the error happend"
const placeholder = "Type 10 digit phone number"

export default class App extends React.Component {
  state = {
           error: false,
           text: "", 
           buttonTitle: errorBtn
           }
errorMsg= () => {this.setState({error: true})}
clearMsg= () => {this.setState({error: false})}

onPress = () => {
  const TextInputNativeTag =   
                    this
                      .textOInputRef
                      ._inputRef
                      ._nativeTag
  if (this.state.buttonTitle == errorBtn) 
    this.errorMsg();
  if (this.state.buttonTitle == clearBtn) 
    this.clearMsg();
  if (this.textOInputRef) 
    AccessibilityInfo
    .setAccessibilityFocus(TextInputNativeTag);

  this.state.buttonTitle == errorBtn &&
  this.setState({buttonTitle:clearBtn});

  this.state.buttonTitle == clearBtn && 
  this.setState({buttonTitle:errorBtn}) ;
  }
  render() {
    return (
    <View>
      <View >
        <Text accessibilityRole='header' 
              style={styles.header}
        >
          Accessabiity Focuse Tester
        </Text>
        <Text style={styles.paragraph}>
          {description}
        </Text>
      </View>
        <View style={styles.body}>
          <View style={styles.sectionContainer}           >
            <Text 
              accessibilityRole='header' 
              style={styles.sectionTitle}
            >
              Accessability Label
            </Text>
            <Text style={
              styles.sectionDescription}
            >
              {this.state.error 
                ? errorLabel 
                : clearLabel 
              }
            </Text>
          </View>

          <View style={
            styles.sectionContainer}
          >
            <TextInput
                ref={r => 
                  this.textOInputRef = r
                }
                style={
                  {height: 40, 
                   borderColor: 'gray', 
                   borderWidth: 1}
                }
                accessibilityLabel={
                  this.state.error 
                   ? errorLabel 
                   : clearLabel 
                }
                underlineColorAndroid=
                  "transparent"
                keyboardType="numeric"
                returnKeyType="done"
                onChangeText={
                  text => 
                    this.setState({text})
                }
                placeholder={placeholder}
                placeholderTextColor=
                  "rgb(143,143,143)"
                value={this.state.text}
              />
          </View>

          <View style={styles.sectionContainer}>
            <Button 
              onPress={this.onPress} 
              title={this.state.buttonTitle}
              color="#841584"
              accessibilityLabel={this
                                  .state
                                  .buttonTitle}
            />
          </View>
        </View>
    </View>
  ); 
 }
}

const styles = StyleSheet.create({
  header: {
    margin: 24,
    fontSize: 18,
    textAlign: 'center',
    fontWeight: 'bold',
  },
  paragraph: {
    margin: 24,
    fontSize: 18,
    textAlign: 'center',
  },
  body: {
    backgroundColor: '#fff',
  },
  sectionContainer: {
    marginTop: 32,
    paddingHorizontal: 24,
  },
  sectionTitle: {
    fontSize: 24,
    fontWeight: '600',
    color: '#000',
  },
  sectionDescription: {
    marginTop: 8,
    fontSize: 18,
    fontWeight: '400',
  },
});

Upvotes: 1

Views: 2044

Answers (1)

Zohar Chiprut
Zohar Chiprut

Reputation: 908

When pressing a button, screen reader reads the button label during the operation of the onPress method. Therefore there was a need to schedule the render a time ahead. in addition, there is a need to schedule the setfocus to the textInput after the label is updated to the new value. following is the code and snack

import * as React from 'react';
import { AccessibilityInfo, TextInput, TouchableWithoutFeedback, Text, View, StyleSheet } from 'react-native';

const errorBtn = "Click to simulate invalid text input";
const clearBtn = "Click to clear the error";
const errorLabel = "Wrong Number has being typed, try again";
const clearLabel = "Phone Number";
const description = 
"Enable Voice Over, If Error hapens on textInput, screen reader shell pronounce the error message and set focus to the textInput where the error happend"
const placeholder = "Type 10 digit phone number"

export default class App extends React.Component {
state = {  
           error: false,
           text: "", 
           buttonTitle: errorBtn
           }
label=clearLabel;
accessabilityMsg=null;
textInputRef=null;

onPress = () => {
  const textInput = this.textInputRef && this.textInputRef._inputRef._nativeTag;
  if (this.state.buttonTitle == errorBtn) {
    console.log('onPress set label & state to error');
    setTimeout(()=>{
      this.setState({error: true,buttonTitle:clearBtn});
      setTimeout(()=>{textInput && AccessibilityInfo.setAccessibilityFocus(textInput)},500);
    },6000);
    this.label=errorLabel;
  } else {
    console.log('onPress set label & state to normal') 
    this.setState({error: false,buttonTitle:errorBtn});
    this.label=clearLabel;
  }
}

  render() {
    return (
    <View >
      <View >
        <Text accessibilityRole='header' 
              style={styles.header}
        >
          Accessabiity Focus Tester
        </Text>
        <Text style={styles.paragraph}>
          {description}
        </Text>
      </View>
        <View style={styles.body}>
          <View style={styles.sectionContainer}           >
            <Text 
              accessibilityRole='header' 
              style={styles.sectionTitle}
            >
              Accessability Label
            </Text>
            <Text style={
              styles.sectionDescription}
            >
              {this.label}
            </Text>
          </View>

          <View style={
            styles.sectionContainer}
          >
            <TextInput
                ref={r => 
                  this.textInputRef = r
                }
                style={
                  {height: 40, 
                   borderColor: 'gray', 
                   borderWidth: 1}
                }
                accessibilityLabel={this.label}
                underlineColorAndroid=
                  "transparent"
                keyboardType="numeric"
                returnKeyType="done"
                onChangeText={
                  text => 
                    this.setState({text})
                }
                placeholder={placeholder}
                placeholderTextColor=
                  "rgb(143,143,143)"
                value={this.state.text}
              />
          </View>

          <View style={styles.button}>
            <TouchableWithoutFeedback
              accessible
              accessibilityRole='button'
              accessibilityLabel={this.state.buttonTitle}
              onPress={this.onPress}
            >
              <Text> {this.state.buttonTitle} </Text>
            </TouchableWithoutFeedback>
          </View>
        </View>
    </View>
  ); 
 }
}

const styles = StyleSheet.create({
  header: {
    margin: 24,
    fontSize: 18,
    textAlign: 'center',
    fontWeight: 'bold',
  },
  paragraph: {
    margin: 24,
    fontSize: 18,
    textAlign: 'center',
  },
  body: {
    backgroundColor: '#fff',
  },
  sectionContainer: {
    marginTop: 32,
    paddingHorizontal: 24,
  },
  sectionTitle: {
    fontSize: 24,
    fontWeight: '600',
    color: '#000',
  },
  sectionDescription: {
    marginTop: 8,
    fontSize: 18,
    fontWeight: '400',
  },
  button: {
    margin: 24,
    alignItems: 'center',
    backgroundColor: '#DDDDDD',
    padding: 10,
    borderColor: '#000',
    borderWidth: 1
  },
});

Finally, it is still not a completed mission. We can't design an application based on a setTimout. Still a better approach need to be found. Thanks for any comment and post.

Upvotes: 1

Related Questions