Skip to content

Commit 0090ab3

Browse files
Estevão Lucasfacebook-github-bot
Estevão Lucas
authored andcommitted
- Add support for "reduce motion" into AccessibilityInfo (#23839)
Summary: This PR adds `isReduceMotionEnabled()` to `AccessibilityInfo` in other to add support for "reduce motion", exposing the Operational System's settings option. Additionally, it adds a new event, `reduceMotionChanged`, in order to listen for this flag's update. With this feature, developers will be able to disable or reduce animations, _**something that will be required as soon as WCAG 2.1 draft got approved**._ See [WCAG 2.1 — 2.3.3 Animations from Interaction criteria](https://knowbility.org/blog/2018/WCAG21-233Animations/) It's exposed by [`UIAccessibility`' isReduceMotionEnabled ](https://developer.apple.com/documentation/uikit/uiaccessibility/1615133-isreducemotionenabled ) on iOS and [Settings.Global.TRANSITION_ANIMATION_SCALE](https://developer.android.com/reference/android/provider/Settings.Global#TRANSITION_ANIMATION_SCALE) on Android. Up until now, `AccessibilityInfo` only exposes screen reader flag. By adding this second accessibility option, it's a good opportunity to rename `fetch` method to an appropriate name, `isScreenReaderEnabled`, as well as rename `change` event to `screenReaderChanged`, which will make it clearer and more specific. (In case it's approved, a follow-up PR could exposes [more iOS acessibility flags](https://developer.apple.com/documentation/uikit/uiaccessibility), such as `isShakeToUndoEnabled`, `isReduceTransparencyEnabled`, `isGrayscaleEnabled`, `isInvertColorsEnabled`) (iOS code inspired by [phonegap-mobile-accessibility](https://github.com/phonegap/phonegap-mobile-accessibility). And Android by [Flutter](https://github.com/flutter/engine/blob/master/shell/platform/android/io/flutter/view/AccessibilityBridge.java )) Pull Request resolved: #23839 Differential Revision: D14406227 Pulled By: hramos fbshipit-source-id: adf43be84c488522bf1e29d862681220ad193883
1 parent 8e490d4 commit 0090ab3

File tree

5 files changed

+181
-30
lines changed

5 files changed

+181
-30
lines changed

Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js

+38-12
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@ const UIManager = require('UIManager');
1616

1717
const RCTAccessibilityInfo = NativeModules.AccessibilityInfo;
1818

19+
const REDUCE_MOTION_EVENT = 'reduceMotionDidChange';
1920
const TOUCH_EXPLORATION_EVENT = 'touchExplorationDidChange';
2021

2122
type ChangeEventName = $Enum<{
2223
change: string,
24+
reduceMotionChanged: string,
25+
screenReaderChanged: string,
2326
}>;
2427

2528
const _subscriptions = new Map();
@@ -35,26 +38,49 @@ const _subscriptions = new Map();
3538
*/
3639

3740
const AccessibilityInfo = {
38-
/* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found
39-
* when making Flow check .android.js files. */
40-
fetch: function(): Promise {
41+
isReduceMotionEnabled: function(): Promise<boolean> {
4142
return new Promise((resolve, reject) => {
42-
RCTAccessibilityInfo.isTouchExplorationEnabled(function(resp) {
43-
resolve(resp);
44-
});
43+
RCTAccessibilityInfo.isReduceMotionEnabled(resolve);
4544
});
4645
},
4746

47+
isScreenReaderEnabled: function(): Promise<boolean> {
48+
return new Promise((resolve, reject) => {
49+
RCTAccessibilityInfo.isTouchExplorationEnabled(resolve);
50+
});
51+
},
52+
53+
/**
54+
* Deprecated
55+
*
56+
* Same as `isScreenReaderEnabled`
57+
*/
58+
get fetch() {
59+
return this.isScreenReaderEnabled;
60+
},
61+
4862
addEventListener: function(
4963
eventName: ChangeEventName,
5064
handler: Function,
5165
): void {
52-
const listener = RCTDeviceEventEmitter.addListener(
53-
TOUCH_EXPLORATION_EVENT,
54-
enabled => {
55-
handler(enabled);
56-
},
57-
);
66+
let listener;
67+
68+
if (eventName === 'change' || eventName === 'screenReaderChanged') {
69+
listener = RCTDeviceEventEmitter.addListener(
70+
TOUCH_EXPLORATION_EVENT,
71+
enabled => {
72+
handler(enabled);
73+
},
74+
);
75+
} else if (eventName === 'reduceMotionChanged') {
76+
listener = RCTDeviceEventEmitter.addListener(
77+
REDUCE_MOTION_EVENT,
78+
enabled => {
79+
handler(enabled);
80+
},
81+
);
82+
}
83+
5884
_subscriptions.set(handler, listener);
5985
},
6086

Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js

+42-7
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');
1616

1717
const AccessibilityManager = NativeModules.AccessibilityManager;
1818

19-
const VOICE_OVER_EVENT = 'voiceOverDidChange';
2019
const ANNOUNCEMENT_DID_FINISH_EVENT = 'announcementDidFinish';
20+
const REDUCE_MOTION_EVENT = 'reduceMotionDidChange';
21+
const VOICE_OVER_EVENT = 'voiceOverDidChange';
2122

2223
type ChangeEventName = $Enum<{
23-
change: string,
2424
announcementFinished: string,
25+
change: string,
26+
reduceMotionChanged: string,
27+
screenReaderChanged: string,
2528
}>;
2629

2730
const _subscriptions = new Map();
@@ -37,23 +40,50 @@ const _subscriptions = new Map();
3740
*/
3841
const AccessibilityInfo = {
3942
/**
40-
* Query whether a screen reader is currently enabled.
43+
* Query whether a reduce motion is currently enabled.
4144
*
4245
* Returns a promise which resolves to a boolean.
4346
* The result is `true` when a screen reader is enabledand `false` otherwise.
4447
*
45-
* See http://facebook.github.io/react-native/docs/accessibilityinfo.html#fetch
48+
* See http://facebook.github.io/react-native/docs/accessibilityinfo.html#isReduceMotionEnabled
4649
*/
47-
fetch: function(): Promise {
50+
isReduceMotionEnabled: function(): Promise {
51+
return new Promise((resolve, reject) => {
52+
AccessibilityManager.getReduceMotionState(resolve, reject);
53+
});
54+
},
55+
56+
/**
57+
* Query whether a screen reader is currently enabled.
58+
*
59+
* Returns a promise which resolves to a boolean.
60+
* The result is `true` when a screen reader is enabled and `false` otherwise.
61+
*
62+
* See http://facebook.github.io/react-native/docs/accessibilityinfo.html#isScreenReaderEnabled
63+
*/
64+
isScreenReaderEnabled: function(): Promise {
4865
return new Promise((resolve, reject) => {
4966
AccessibilityManager.getCurrentVoiceOverState(resolve, reject);
5067
});
5168
},
5269

70+
/**
71+
* Deprecated
72+
*
73+
* Same as `isScreenReaderEnabled`
74+
*/
75+
get fetch() {
76+
return this.isScreenReaderEnabled;
77+
},
78+
5379
/**
5480
* Add an event handler. Supported events:
5581
*
56-
* - `change`: Fires when the state of the screen reader changes. The argument
82+
* - `reduceMotionChanged`: Fires when the state of the reduce motion toggle changes.
83+
* The argument to the event handler is a boolean. The boolean is `true` when a reduce
84+
* motion is enabled (or when "Transition Animation Scale" in "Developer options" is
85+
* "Animation off") and `false` otherwise.
86+
* - `screenReaderChanged`: Fires when the state of the screen reader changes. The argument
5787
* to the event handler is a boolean. The boolean is `true` when a screen
5888
* reader is enabled and `false` otherwise.
5989
* - `announcementFinished`: iOS-only event. Fires when the screen reader has
@@ -71,8 +101,13 @@ const AccessibilityInfo = {
71101
): Object {
72102
let listener;
73103

74-
if (eventName === 'change') {
104+
if (eventName === 'change' || eventName === 'screenReaderChanged') {
75105
listener = RCTDeviceEventEmitter.addListener(VOICE_OVER_EVENT, handler);
106+
} else if (eventName === 'reduceMotionChanged') {
107+
listener = RCTDeviceEventEmitter.addListener(
108+
REDUCE_MOTION_EVENT,
109+
handler,
110+
);
76111
} else if (eventName === 'announcementFinished') {
77112
listener = RCTDeviceEventEmitter.addListener(
78113
ANNOUNCEMENT_DID_FINISH_EVENT,

React/Modules/RCTAccessibilityManager.h

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ extern NSString *const RCTAccessibilityManagerDidUpdateMultiplierNotification; /
1919
/// map from UIKit categories to multipliers
2020
@property (nonatomic, copy) NSDictionary<NSString *, NSNumber *> *multipliers;
2121

22+
@property (nonatomic, assign) BOOL isReduceMotionEnabled;
2223
@property (nonatomic, assign) BOOL isVoiceOverEnabled;
2324

2425
@end

React/Modules/RCTAccessibilityManager.m

+25
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,13 @@ - (instancetype)init
7676
name:UIAccessibilityAnnouncementDidFinishNotification
7777
object:nil];
7878

79+
[[NSNotificationCenter defaultCenter] addObserver:self
80+
selector:@selector(reduceMotionStatusDidChange:)
81+
name:UIAccessibilityReduceMotionStatusDidChangeNotification
82+
object:nil];
83+
7984
self.contentSizeCategory = RCTSharedApplication().preferredContentSizeCategory;
85+
_isReduceMotionEnabled = UIAccessibilityIsReduceMotionEnabled();
8086
_isVoiceOverEnabled = UIAccessibilityIsVoiceOverRunning();
8187
}
8288
return self;
@@ -119,6 +125,19 @@ - (void)accessibilityAnnouncementDidFinish:(__unused NSNotification *)notificati
119125
#pragma clang diagnostic pop
120126
}
121127

128+
- (void)reduceMotionStatusDidChange:(__unused NSNotification *)notification
129+
{
130+
BOOL newReduceMotionEnabled = UIAccessibilityIsReduceMotionEnabled();
131+
if (_isReduceMotionEnabled != newReduceMotionEnabled) {
132+
_isReduceMotionEnabled = newReduceMotionEnabled;
133+
#pragma clang diagnostic push
134+
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
135+
[_bridge.eventDispatcher sendDeviceEventWithName:@"reduceMotionDidChange"
136+
body:@(_isReduceMotionEnabled)];
137+
#pragma clang diagnostic pop
138+
}
139+
}
140+
122141
- (void)setContentSizeCategory:(NSString *)contentSizeCategory
123142
{
124143
if (_contentSizeCategory != contentSizeCategory) {
@@ -207,6 +226,12 @@ - (void)setMultipliers:(NSDictionary<NSString *, NSNumber *> *)multipliers
207226
callback(@[@(_isVoiceOverEnabled)]);
208227
}
209228

229+
RCT_EXPORT_METHOD(getReduceMotionState:(RCTResponseSenderBlock)callback
230+
error:(__unused RCTResponseSenderBlock)error)
231+
{
232+
callback(@[@(_isReduceMotionEnabled)]);
233+
}
234+
210235
@end
211236

212237
@implementation RCTBridge (RCTAccessibilityManager)

ReactAndroid/src/main/java/com/facebook/react/modules/accessibilityinfo/AccessibilityInfoModule.java

+75-11
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@
99

1010
import android.annotation.TargetApi;
1111
import android.content.Context;
12+
import android.content.ContentResolver;
13+
import android.database.ContentObserver;
14+
import android.net.Uri;
1215
import android.os.Build;
16+
import android.os.Handler;
17+
import android.os.Looper;
18+
import android.provider.Settings;
1319
import android.view.accessibility.AccessibilityManager;
1420

1521
import com.facebook.react.bridge.Callback;
@@ -36,21 +42,42 @@ private class ReactTouchExplorationStateChangeListener
3642

3743
@Override
3844
public void onTouchExplorationStateChanged(boolean enabled) {
39-
updateAndSendChangeEvent(enabled);
45+
updateAndSendTouchExplorationChangeEvent(enabled);
4046
}
4147
}
4248

49+
// Listener that is notified when the global TRANSITION_ANIMATION_SCALE.
50+
private final ContentObserver animationScaleObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
51+
@Override
52+
public void onChange(boolean selfChange) {
53+
this.onChange(selfChange, null);
54+
}
55+
56+
@Override
57+
public void onChange(boolean selfChange, Uri uri) {
58+
if (getReactApplicationContext().hasActiveCatalystInstance()) {
59+
AccessibilityInfoModule.this.updateAndSendReduceMotionChangeEvent();
60+
}
61+
}
62+
};
63+
4364
private @Nullable AccessibilityManager mAccessibilityManager;
4465
private @Nullable ReactTouchExplorationStateChangeListener mTouchExplorationStateChangeListener;
45-
private boolean mEnabled = false;
66+
private final ContentResolver mContentResolver;
67+
private boolean mReduceMotionEnabled = false;
68+
private boolean mTouchExplorationEnabled = false;
4669

47-
private static final String EVENT_NAME = "touchExplorationDidChange";
70+
private static final String REDUCE_MOTION_EVENT_NAME = "reduceMotionDidChange";
71+
private static final String TOUCH_EXPLORATION_EVENT_NAME = "touchExplorationDidChange";
4872

4973
public AccessibilityInfoModule(ReactApplicationContext context) {
5074
super(context);
5175
Context appContext = context.getApplicationContext();
5276
mAccessibilityManager = (AccessibilityManager) appContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
53-
mEnabled = mAccessibilityManager.isTouchExplorationEnabled();
77+
mContentResolver = getReactApplicationContext().getContentResolver();
78+
mTouchExplorationEnabled = mAccessibilityManager.isTouchExplorationEnabled();
79+
mReduceMotionEnabled = this.getIsReduceMotionEnabledValue();
80+
5481
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
5582
mTouchExplorationStateChangeListener = new ReactTouchExplorationStateChangeListener();
5683
}
@@ -61,16 +88,41 @@ public String getName() {
6188
return "AccessibilityInfo";
6289
}
6390

91+
private boolean getIsReduceMotionEnabledValue() {
92+
String value = Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 ? null
93+
: Settings.Global.getString(
94+
mContentResolver,
95+
Settings.Global.TRANSITION_ANIMATION_SCALE
96+
);
97+
98+
return value != null && value.equals("0.0");
99+
}
100+
101+
@ReactMethod
102+
public void isReduceMotionEnabled(Callback successCallback) {
103+
successCallback.invoke(mReduceMotionEnabled);
104+
}
105+
64106
@ReactMethod
65107
public void isTouchExplorationEnabled(Callback successCallback) {
66-
successCallback.invoke(mEnabled);
108+
successCallback.invoke(mTouchExplorationEnabled);
109+
}
110+
111+
private void updateAndSendReduceMotionChangeEvent() {
112+
boolean isReduceMotionEnabled = this.getIsReduceMotionEnabledValue();
113+
114+
if (mReduceMotionEnabled != isReduceMotionEnabled) {
115+
mReduceMotionEnabled = isReduceMotionEnabled;
116+
getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
117+
.emit(REDUCE_MOTION_EVENT_NAME, mReduceMotionEnabled);
118+
}
67119
}
68120

69-
private void updateAndSendChangeEvent(boolean enabled) {
70-
if (mEnabled != enabled) {
71-
mEnabled = enabled;
121+
private void updateAndSendTouchExplorationChangeEvent(boolean enabled) {
122+
if (mTouchExplorationEnabled != enabled) {
123+
mTouchExplorationEnabled = enabled;
72124
getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
73-
.emit(EVENT_NAME, mEnabled);
125+
.emit(TOUCH_EXPLORATION_EVENT_NAME, mTouchExplorationEnabled);
74126
}
75127
}
76128

@@ -80,7 +132,14 @@ public void onHostResume() {
80132
mAccessibilityManager.addTouchExplorationStateChangeListener(
81133
mTouchExplorationStateChangeListener);
82134
}
83-
updateAndSendChangeEvent(mAccessibilityManager.isTouchExplorationEnabled());
135+
136+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
137+
Uri transitionUri = Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE);
138+
mContentResolver.registerContentObserver(transitionUri, false, animationScaleObserver);
139+
}
140+
141+
updateAndSendTouchExplorationChangeEvent(mAccessibilityManager.isTouchExplorationEnabled());
142+
updateAndSendReduceMotionChangeEvent();
84143
}
85144

86145
@Override
@@ -89,12 +148,17 @@ public void onHostPause() {
89148
mAccessibilityManager.removeTouchExplorationStateChangeListener(
90149
mTouchExplorationStateChangeListener);
91150
}
151+
152+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
153+
mContentResolver.unregisterContentObserver(animationScaleObserver);
154+
}
92155
}
93156

94157
@Override
95158
public void initialize() {
96159
getReactApplicationContext().addLifecycleEventListener(this);
97-
updateAndSendChangeEvent(mAccessibilityManager.isTouchExplorationEnabled());
160+
updateAndSendTouchExplorationChangeEvent(mAccessibilityManager.isTouchExplorationEnabled());
161+
updateAndSendReduceMotionChangeEvent();
98162
}
99163

100164
@Override

0 commit comments

Comments
 (0)