mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Add accessibilityValueDescription support. (#26169)
Summary: React Native components need a mechanism to specify their value to assistive technologies. This PR adds the notion of accessibilityValueDescription-- a property which either contains a textual description of a component's value, or for range-based components, such as sliders and progress bars, it contains range information (minimum, current, and maximum). On iOS, the range-based info if present is converted into a percentage and added to the accessibilityValue property of the UIView. If text is present as part of the accessibilityValueDescription, it is used instead of the range-based information. On Android, any range-based information in accessibilityValueDescription is exposed in the AccessibilityNodeInfo's RangeInfo. Text which is part of accessibilityValueDescription is appended to the content description. ## Changelog [GENERAL] [Change] - add accessibilityValuedescription property. Pull Request resolved: https://github.com/facebook/react-native/pull/26169 Test Plan: Added two new accessibility examples to RNTester, one which uses text and another which uses range-based info in accessibilityValueDescription. Verified that they both behave correctly on both Android and iOS. Differential Revision: D17444730 Pulled By: cpojer fbshipit-source-id: 1fb3252a90f88f7cafe1cbf7db08c03f14cc2321
This commit is contained in:
committed by
Facebook Github Bot
parent
7c8e266e39
commit
7df3eea1a7
+98
-2
@@ -7,15 +7,18 @@ package com.facebook.react.uimanager;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.URLSpan;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.AccessibilityDelegateCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.RangeInfoCompat;
|
||||
import com.facebook.react.R;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.Dynamic;
|
||||
@@ -36,6 +39,8 @@ public class ReactAccessibilityDelegate extends AccessibilityDelegateCompat {
|
||||
|
||||
private static final String TAG = "ReactAccessibilityDelegate";
|
||||
private static int sCounter = 0x3f000000;
|
||||
private static final int TIMEOUT_SEND_ACCESSIBILITY_EVENT = 200;
|
||||
private static final int SEND_EVENT = 1;
|
||||
|
||||
public static final HashMap<String, Integer> sActionIdMap = new HashMap<>();
|
||||
|
||||
@@ -46,6 +51,21 @@ public class ReactAccessibilityDelegate extends AccessibilityDelegateCompat {
|
||||
sActionIdMap.put("decrement", AccessibilityActionCompat.ACTION_SCROLL_BACKWARD.getId());
|
||||
}
|
||||
|
||||
private Handler mHandler;
|
||||
|
||||
/**
|
||||
* Schedule a command for sending an accessibility event. </br> Note: A command is used to ensure
|
||||
* that accessibility events are sent at most one in a given time frame to save system resources
|
||||
* while the progress changes quickly.
|
||||
*/
|
||||
private void scheduleAccessibilityEventSender(View host) {
|
||||
if (mHandler.hasMessages(SEND_EVENT, host)) {
|
||||
mHandler.removeMessages(SEND_EVENT, host);
|
||||
}
|
||||
Message msg = mHandler.obtainMessage(SEND_EVENT, host);
|
||||
mHandler.sendMessageDelayed(msg, TIMEOUT_SEND_ACCESSIBILITY_EVENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* These roles are defined by Google's TalkBack screen reader, and this list should be kept up to
|
||||
* date with their implementation. Details can be seen in their source code here:
|
||||
@@ -148,6 +168,14 @@ public class ReactAccessibilityDelegate extends AccessibilityDelegateCompat {
|
||||
public ReactAccessibilityDelegate() {
|
||||
super();
|
||||
mAccessibilityActionsMap = new HashMap<Integer, String>();
|
||||
mHandler =
|
||||
new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
View host = (View) msg.obj;
|
||||
host.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -185,6 +213,61 @@ public class ReactAccessibilityDelegate extends AccessibilityDelegateCompat {
|
||||
info.addAction(accessibilityAction);
|
||||
}
|
||||
}
|
||||
|
||||
// Process accessibilityValue
|
||||
|
||||
final ReadableMap accessibilityValue = (ReadableMap) host.getTag(R.id.accessibility_value);
|
||||
if (accessibilityValue != null
|
||||
&& accessibilityValue.hasKey("min")
|
||||
&& accessibilityValue.hasKey("now")
|
||||
&& accessibilityValue.hasKey("max")) {
|
||||
final Dynamic minDynamic = accessibilityValue.getDynamic("min");
|
||||
final Dynamic nowDynamic = accessibilityValue.getDynamic("now");
|
||||
final Dynamic maxDynamic = accessibilityValue.getDynamic("max");
|
||||
if (minDynamic != null
|
||||
&& minDynamic.getType() == ReadableType.Number
|
||||
&& nowDynamic != null
|
||||
&& nowDynamic.getType() == ReadableType.Number
|
||||
&& maxDynamic != null
|
||||
&& maxDynamic.getType() == ReadableType.Number) {
|
||||
final int min = minDynamic.asInt();
|
||||
final int now = nowDynamic.asInt();
|
||||
final int max = maxDynamic.asInt();
|
||||
if (max > min && now >= min && max >= now) {
|
||||
info.setRangeInfo(RangeInfoCompat.obtain(RangeInfoCompat.RANGE_TYPE_INT, min, max, now));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
|
||||
super.onInitializeAccessibilityEvent(host, event);
|
||||
// Set item count and current item index on accessibility events for adjustable
|
||||
// in order to make Talkback announce the value of the adjustable
|
||||
final ReadableMap accessibilityValue = (ReadableMap) host.getTag(R.id.accessibility_value);
|
||||
if (accessibilityValue != null
|
||||
&& accessibilityValue.hasKey("min")
|
||||
&& accessibilityValue.hasKey("now")
|
||||
&& accessibilityValue.hasKey("max")) {
|
||||
final Dynamic minDynamic = accessibilityValue.getDynamic("min");
|
||||
final Dynamic nowDynamic = accessibilityValue.getDynamic("now");
|
||||
final Dynamic maxDynamic = accessibilityValue.getDynamic("max");
|
||||
if (minDynamic != null
|
||||
&& minDynamic.getType() == ReadableType.Number
|
||||
&& nowDynamic != null
|
||||
&& nowDynamic.getType() == ReadableType.Number
|
||||
&& maxDynamic != null
|
||||
&& maxDynamic.getType() == ReadableType.Number) {
|
||||
final int min = minDynamic.asInt();
|
||||
final int now = nowDynamic.asInt();
|
||||
final int max = maxDynamic.asInt();
|
||||
if (max > min && now >= min && max >= now) {
|
||||
event.setItemCount(max - min);
|
||||
event.setCurrentItemIndex(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -196,6 +279,20 @@ public class ReactAccessibilityDelegate extends AccessibilityDelegateCompat {
|
||||
reactContext
|
||||
.getJSModule(RCTEventEmitter.class)
|
||||
.receiveEvent(host.getId(), "topAccessibilityAction", event);
|
||||
|
||||
// In order to make Talkback announce the change of the adjustable's value,
|
||||
// schedule to send a TYPE_VIEW_SELECTED event after performing the scroll actions.
|
||||
final AccessibilityRole accessibilityRole =
|
||||
(AccessibilityRole) host.getTag(R.id.accessibility_role);
|
||||
final ReadableMap accessibilityValue = (ReadableMap) host.getTag(R.id.accessibility_value);
|
||||
if (accessibilityRole == AccessibilityRole.ADJUSTABLE
|
||||
&& (action == AccessibilityActionCompat.ACTION_SCROLL_FORWARD.getId()
|
||||
|| action == AccessibilityActionCompat.ACTION_SCROLL_BACKWARD.getId())) {
|
||||
if (accessibilityValue != null && !accessibilityValue.hasKey("text")) {
|
||||
scheduleAccessibilityEventSender(host);
|
||||
}
|
||||
return super.performAccessibilityAction(host, action, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.performAccessibilityAction(host, action, args);
|
||||
@@ -203,7 +300,6 @@ public class ReactAccessibilityDelegate extends AccessibilityDelegateCompat {
|
||||
|
||||
private static void setState(
|
||||
AccessibilityNodeInfoCompat info, ReadableMap accessibilityState, Context context) {
|
||||
Log.d(TAG, "setState " + accessibilityState);
|
||||
final ReadableMapKeySetIterator i = accessibilityState.keySetIterator();
|
||||
while (i.hasNextKey()) {
|
||||
final String state = i.nextKey();
|
||||
|
||||
Reference in New Issue
Block a user