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:
Родитель
41f1bcc5ac
Коммит
04790f1a78
|
@ -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',
|
||||
|
|
Загрузка…
Ссылка в новой задаче