Finish AccessibilityInfo implementation

Summary:
This PR is based on files ericvicenti gave me. Specifically, he gave me:
  - AccessibilityInfo.android.js
  - AccessibilityInfo.ios.js
  - AccessibilityInfoModule.java

Before this change, only a native iOS implementation of AccessibilityInfo existed. This change includes:
  - A native Android implementation of AccessibilityInfo.
  - JavaScript wrappers for the AccessibilityInfo module for both iOS and Android.
  - UIExplorer changes to illustrate how to use AccessibilityInfo on iOS and Android.
  - Documentation for the AccessibilityInfo APIs.

**Test plan (required)**

Tested the UIExplorer AccessibilityInfo example on iOS and Android with the screen reader both enabled and disabled.

Adam Comella
Microsoft Corp.
Closes https://github.com/facebook/react-native/pull/12273

Reviewed By: mkonicek

Differential Revision: D4527224

Pulled By: ericvicenti

fbshipit-source-id: d04638465ccbdbb35ecfc9504daaeb8e33aab57a
This commit is contained in:
Adam Comella 2017-02-27 18:28:23 -08:00 коммит произвёл Facebook Github Bot
Родитель 41f1bcc5ac
Коммит 04790f1a78
11 изменённых файлов: 411 добавлений и 0 удалений

Просмотреть файл

@ -25,6 +25,7 @@
var React = require('react');
var ReactNative = require('react-native');
var {
AccessibilityInfo,
StyleSheet,
Text,
View,
@ -45,8 +46,34 @@ class AccessibilityAndroidExample extends React.Component {
count: 0,
backgroundImportantForAcc: 0,
forgroundImportantForAcc: 0,
screenReaderEnabled: false,
};
componentDidMount() {
AccessibilityInfo.addEventListener(
'change',
this._handleScreenReaderToggled
);
AccessibilityInfo.fetch().done((isEnabled) => {
this.setState({
screenReaderEnabled: isEnabled
});
});
}
componentWillUnmount() {
AccessibilityInfo.removeEventListener(
'change',
this._handleScreenReaderToggled
);
}
_handleScreenReaderToggled = (isEnabled) => {
this.setState({
screenReaderEnabled: isEnabled,
});
}
_addOne = () => {
this.setState({
count: ++this.state.count,
@ -125,6 +152,12 @@ class AccessibilityAndroidExample extends React.Component {
</Text>
</UIExplorerBlock>
<UIExplorerBlock title="Check if the screen reader is enabled">
<Text>
The screen reader is {this.state.screenReaderEnabled ? 'enabled' : 'disabled'}.
</Text>
</UIExplorerBlock>
<UIExplorerBlock title="Overlapping views and importantForAccessibility property">
<View style={styles.container}>
<View

Просмотреть файл

@ -26,6 +26,7 @@
var React = require('react');
var ReactNative = require('react-native');
var {
AccessibilityInfo,
Text,
View,
} = ReactNative;
@ -64,6 +65,47 @@ class AccessibilityIOSExample extends React.Component {
}
}
class ScreenReaderStatusExample extends React.Component {
state = {
screenReaderEnabled: false,
}
componentDidMount() {
AccessibilityInfo.addEventListener(
'change',
this._handleScreenReaderToggled
);
AccessibilityInfo.fetch().done((isEnabled) => {
this.setState({
screenReaderEnabled: isEnabled
});
});
}
componentWillUnmount() {
AccessibilityInfo.removeEventListener(
'change',
this._handleScreenReaderToggled
);
}
_handleScreenReaderToggled = (isEnabled) => {
this.setState({
screenReaderEnabled: isEnabled,
});
}
render() {
return (
<View>
<Text>
The screen reader is {this.state.screenReaderEnabled ? 'enabled' : 'disabled'}.
</Text>
</View>
);
}
}
exports.title = 'AccessibilityIOS';
exports.description = 'Interface to show iOS\' accessibility samples';
exports.examples = [
@ -71,4 +113,8 @@ exports.examples = [
title: 'Accessibility elements',
render(): React.Element<any> { return <AccessibilityIOSExample />; }
},
{
title: 'Check if the screen reader is enabled',
render(): React.Element<any> { return <ScreenReaderStatusExample />; }
},
];

Просмотреть файл

@ -0,0 +1,66 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule AccessibilityInfo
* @flow
*/
'use strict';
var NativeModules = require('NativeModules');
var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');
var RCTAccessibilityInfo = NativeModules.AccessibilityInfo;
var TOUCH_EXPLORATION_EVENT = 'touchExplorationDidChange';
type ChangeEventName = $Enum<{
change: string,
}>;
var _subscriptions = new Map();
var AccessibilityInfo = {
fetch: function(): Promise {
return new Promise((resolve, reject) => {
RCTAccessibilityInfo.isTouchExplorationEnabled(
function(resp) {
resolve(resp);
}
);
});
},
addEventListener: function (
eventName: ChangeEventName,
handler: Function
): void {
var listener = RCTDeviceEventEmitter.addListener(
TOUCH_EXPLORATION_EVENT,
(enabled) => {
handler(enabled);
}
);
_subscriptions.set(handler, listener);
},
removeEventListener: function(
eventName: ChangeEventName,
handler: Function
): void {
var listener = _subscriptions.get(handler);
if (!listener) {
return;
}
listener.remove();
_subscriptions.delete(handler);
},
};
module.exports = AccessibilityInfo;

Просмотреть файл

@ -0,0 +1,132 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule AccessibilityInfo
* @flow
*/
'use strict';
var NativeModules = require('NativeModules');
var Promise = require('Promise');
var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');
var AccessibilityManager = NativeModules.AccessibilityManager;
var VOICE_OVER_EVENT = 'voiceOverDidChange';
type ChangeEventName = $Enum<{
change: string,
}>;
var _subscriptions = new Map();
/**
* Sometimes it's useful to know whether or not the device has a screen reader that is currently active. The
* `AccessibilityInfo` API is designed for this purpose. You can use it to query the current state of the
* screen reader as well as to register to be notified when the state of the screen reader changes.
*
* Here's a small example illustrating how to use `AccessibilityInfo`:
*
* ```javascript
* class ScreenReaderStatusExample extends React.Component {
* state = {
* screenReaderEnabled: false,
* }
*
* componentDidMount() {
* AccessibilityInfo.addEventListener(
* 'change',
* this._handleScreenReaderToggled
* );
* AccessibilityInfo.fetch().done((isEnabled) => {
* this.setState({
* screenReaderEnabled: isEnabled
* });
* });
* }
*
* componentWillUnmount() {
* AccessibilityInfo.removeEventListener(
* 'change',
* this._handleScreenReaderToggled
* );
* }
*
* _handleScreenReaderToggled = (isEnabled) => {
* this.setState({
* screenReaderEnabled: isEnabled,
* });
* }
*
* render() {
* return (
* <View>
* <Text>
* The screen reader is {this.state.screenReaderEnabled ? 'enabled' : 'disabled'}.
* </Text>
* </View>
* );
* }
* }
* ```
*/
var AccessibilityInfo = {
/**
* Query whether a screen reader is currently enabled. Returns a promise which
* resolves to a boolean. The result is `true` when a screen reader is enabled
* and `false` otherwise.
*/
fetch: function(): Promise {
return new Promise((resolve, reject) => {
AccessibilityManager.getCurrentVoiceOverState(
resolve,
reject
);
});
},
/**
* Add an event handler. Supported events:
*
* - `change`: Fires when the state of the screen reader changes. The argument
* to the event handler is a boolean. The boolean is `true` when a screen
* reader is enabled and `false` otherwise.
*/
addEventListener: function (
eventName: ChangeEventName,
handler: Function
): Object {
var listener = RCTDeviceEventEmitter.addListener(
VOICE_OVER_EVENT,
handler
);
_subscriptions.set(handler, listener);
return {
remove: AccessibilityInfo.removeEventListener.bind(null, eventName, handler),
};
},
/**
* Remove an event handler.
*/
removeEventListener: function(
eventName: ChangeEventName,
handler: Function
): void {
var listener = _subscriptions.get(handler);
if (!listener) {
return;
}
listener.remove();
_subscriptions.delete(handler);
},
};
module.exports = AccessibilityInfo;

Просмотреть файл

@ -28,6 +28,7 @@ if (__DEV__) {
// Export React, plus some native additions.
const ReactNative = {
// Components
get AccessibilityInfo() { return require('AccessibilityInfo'); },
get ActivityIndicator() { return require('ActivityIndicator'); },
get ART() { return require('ReactNativeART'); },
get Button() { return require('Button'); },

Просмотреть файл

@ -0,0 +1,98 @@
// Copyright 2004-present Facebook. All Rights Reserved.
package com.facebook.react.modules.accessibilityinfo;
import javax.annotation.Nullable;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.view.accessibility.AccessibilityManager;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.modules.core.DeviceEventManagerModule;
/**
* Module that monitors and provides information about the state of Touch Exploration service
* on the device. For API >= 19.
*/
@ReactModule(name = "AccessibilityInfo")
public class AccessibilityInfoModule extends ReactContextBaseJavaModule
implements LifecycleEventListener {
@TargetApi(19)
private class ReactTouchExplorationStateChangeListener
implements AccessibilityManager.TouchExplorationStateChangeListener {
@Override
public void onTouchExplorationStateChanged(boolean enabled) {
updateAndSendChangeEvent(enabled);
}
}
private @Nullable AccessibilityManager mAccessibilityManager;
private @Nullable ReactTouchExplorationStateChangeListener mTouchExplorationStateChangeListener;
private boolean mEnabled = false;
private static final String EVENT_NAME = "touchExplorationDidChange";
public AccessibilityInfoModule(ReactApplicationContext context) {
super(context);
mAccessibilityManager = (AccessibilityManager) getReactApplicationContext()
.getSystemService(Context.ACCESSIBILITY_SERVICE);
mEnabled = mAccessibilityManager.isTouchExplorationEnabled();
if (Build.VERSION.SDK_INT >= 19) {
mTouchExplorationStateChangeListener = new ReactTouchExplorationStateChangeListener();
}
}
@Override
public String getName() {
return "AccessibilityInfo";
}
@ReactMethod
public void isTouchExplorationEnabled(Callback successCallback) {
successCallback.invoke(mEnabled);
}
private void updateAndSendChangeEvent(boolean enabled) {
if (mEnabled != enabled) {
mEnabled = enabled;
getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(EVENT_NAME, mEnabled);
}
}
@Override
public void onHostResume() {
if (Build.VERSION.SDK_INT >= 19) {
mAccessibilityManager.addTouchExplorationStateChangeListener(
mTouchExplorationStateChangeListener);
}
updateAndSendChangeEvent(mAccessibilityManager.isTouchExplorationEnabled());
}
@Override
public void onHostPause() {
if (Build.VERSION.SDK_INT >= 19) {
mAccessibilityManager.removeTouchExplorationStateChangeListener(
mTouchExplorationStateChangeListener);
}
}
@Override
public void initialize() {
getReactApplicationContext().addLifecycleEventListener(this);
updateAndSendChangeEvent(mAccessibilityManager.isTouchExplorationEnabled());
}
@Override
public void onHostDestroy() {
}
}

Просмотреть файл

@ -0,0 +1,22 @@
include_defs("//ReactAndroid/DEFS")
android_library(
name = "accessibilityinfo",
srcs = glob(["**/*.java"]),
visibility = [
"PUBLIC",
],
deps = [
react_native_dep("libraries/fbcore/src/main/java/com/facebook/common/logging:logging"),
react_native_dep("third-party/java/infer-annotations:infer-annotations"),
react_native_dep("third-party/java/jsr-305:jsr-305"),
react_native_target("java/com/facebook/react/bridge:bridge"),
react_native_target("java/com/facebook/react/common:common"),
react_native_target("java/com/facebook/react/module/annotations:annotations"),
react_native_target("java/com/facebook/react/modules/core:core"),
],
)
project_config(
src_target = ":accessibilityinfo",
)

Просмотреть файл

@ -19,6 +19,7 @@ android_library(
react_native_target("java/com/facebook/react/devsupport:devsupport"),
react_native_target("java/com/facebook/react/flat:flat"),
react_native_target("java/com/facebook/react/module/model:model"),
react_native_target("java/com/facebook/react/modules/accessibilityinfo:accessibilityinfo"),
react_native_target("java/com/facebook/react/modules/appstate:appstate"),
react_native_target("java/com/facebook/react/modules/camera:camera"),
react_native_target("java/com/facebook/react/modules/clipboard:clipboard"),

Просмотреть файл

@ -36,6 +36,7 @@ import com.facebook.react.flat.RCTViewManager;
import com.facebook.react.flat.RCTViewPagerManager;
import com.facebook.react.flat.RCTVirtualTextManager;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.modules.accessibilityinfo.AccessibilityInfoModule;
import com.facebook.react.modules.appstate.AppStateModule;
import com.facebook.react.modules.camera.CameraRollManager;
import com.facebook.react.modules.camera.ImageEditingManager;
@ -102,6 +103,12 @@ public class MainReactPackage extends LazyReactPackage {
@Override
public List<ModuleSpec> getNativeModules(final ReactApplicationContext context) {
return Arrays.asList(
new ModuleSpec(AccessibilityInfoModule.class, new Provider<NativeModule>() {
@Override
public NativeModule get() {
return new AccessibilityInfoModule(context);
}
}),
new ModuleSpec(AppStateModule.class, new Provider<NativeModule>() {
@Override
public NativeModule get() {

Просмотреть файл

@ -11,6 +11,8 @@ previous: animations
## Native App Accessibility (iOS and Android)
Both iOS and Android provide APIs for making apps accessible to people with disabilities. In addition, both platforms provide bundled assistive technologies, like the screen readers VoiceOver (iOS) and TalkBack (Android) for the visually impaired. Similarly, in React Native we have included APIs designed to provide developers with support for making apps more accessible. Take note, iOS and Android differ slightly in their approaches, and thus the React Native implementations may vary by platform.
In addition to this documentation, you might find [this blog post](https://code.facebook.com/posts/435862739941212/making-react-native-apps-accessible/) about React Native accessibility to be useful.
## Making Apps Accessible
### Accessibility properties
@ -135,7 +137,9 @@ In the case of two overlapping UI components with the same parent, default acces
In the above example, the yellow layout and its descendants are completely invisible to TalkBack and all other accessibility services. So we can easily use overlapping views with the same parent without confusing TalkBack.
### Checking if a Screen Reader is Enabled
The `AccessibilityInfo` API allows you to determine whether or not a screen reader is currently active. See the [AccessibilityInfo documentation](docs/accessibilityinfo.html) for details.
### Sending Accessibility Events (Android)

Просмотреть файл

@ -47,6 +47,7 @@ const components = [
];
const apis = [
'../Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js',
'../Libraries/ActionSheetIOS/ActionSheetIOS.js',
'../Libraries/AdSupport/AdSupportIOS.js',
'../Libraries/Alert/Alert.js',