Skip to content

Commit 7df3eea

Browse files
Marc Mulcahyfacebook-github-bot
Marc Mulcahy
authored andcommitted
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: #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
1 parent 7c8e266 commit 7df3eea

File tree

14 files changed

+268
-3
lines changed

14 files changed

+268
-3
lines changed

Libraries/Components/View/ReactNativeViewAttributes.js

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const UIView = {
2020
accessibilityLiveRegion: true,
2121
accessibilityRole: true,
2222
accessibilityState: true,
23+
accessibilityValue: true,
2324
accessibilityHint: true,
2425
importantForAccessibility: true,
2526
nativeID: true,

Libraries/Components/View/ReactNativeViewViewConfig.js

+1
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ const ReactNativeViewConfig = {
123123
accessibilityRole: true,
124124
accessibilityStates: true, // TODO: Can be removed after next release
125125
accessibilityState: true,
126+
accessibilityValue: true,
126127
accessibilityViewIsModal: true,
127128
accessible: true,
128129
alignContent: true,

Libraries/Components/View/ViewAccessibility.js

+22
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,25 @@ export type AccessibilityState = {
6262
busy?: boolean,
6363
expanded?: boolean,
6464
};
65+
66+
export type AccessibilityValue = $ReadOnly<{|
67+
/**
68+
* The minimum value of this component's range. (should be an integer)
69+
*/
70+
min?: number,
71+
72+
/**
73+
* The maximum value of this component's range. (should be an integer)
74+
*/
75+
max?: number,
76+
77+
/**
78+
* The current value of this component's range. (should be an integer)
79+
*/
80+
now?: number,
81+
82+
/**
83+
* A textual description of this component's value. (will override minimum, current, and maximum if set)
84+
*/
85+
text?: string,
86+
|}>;

Libraries/Components/View/ViewPropTypes.js

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {TVViewProps} from '../AppleTV/TVViewPropTypes';
1818
import type {
1919
AccessibilityRole,
2020
AccessibilityState,
21+
AccessibilityValue,
2122
AccessibilityActionEvent,
2223
AccessibilityActionInfo,
2324
} from './ViewAccessibility';
@@ -413,6 +414,7 @@ export type ViewProps = $ReadOnly<{|
413414
* Indicates to accessibility services that UI Component is in a specific State.
414415
*/
415416
accessibilityState?: ?AccessibilityState,
417+
accessibilityValue?: ?AccessibilityValue,
416418

417419
/**
418420
* Provides an array of custom actions available for accessibility.

Libraries/DeprecatedPropTypes/DeprecatedViewPropTypes.js

+1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ module.exports = {
102102
>),
103103

104104
accessibilityState: PropTypes.object,
105+
accessibilityValue: PropTypes.object,
105106
/**
106107
* Indicates to accessibility services whether the user should be notified
107108
* when this view changes. Works for Android API >= 19 only.

RNTester/js/examples/Accessibility/AccessibilityExample.js

+89
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,89 @@ class AccessibilityActionsExample extends React.Component {
512512
);
513513
}
514514
}
515+
516+
class FakeSliderExample extends React.Component {
517+
state = {
518+
current: 50,
519+
textualValue: 'center',
520+
};
521+
522+
increment = () => {
523+
let newValue = this.state.current + 2;
524+
if (newValue > 100) {
525+
newValue = 100;
526+
}
527+
this.setState({
528+
current: newValue,
529+
});
530+
};
531+
532+
decrement = () => {
533+
let newValue = this.state.current - 2;
534+
if (newValue < 0) {
535+
newValue = 0;
536+
}
537+
this.setState({
538+
current: newValue,
539+
});
540+
};
541+
542+
render() {
543+
return (
544+
<View>
545+
<View
546+
accessible={true}
547+
accessibilityLabel="Fake Slider"
548+
accessibilityRole="adjustable"
549+
accessibilityActions={[{name: 'increment'}, {name: 'decrement'}]}
550+
onAccessibilityAction={event => {
551+
switch (event.nativeEvent.actionName) {
552+
case 'increment':
553+
this.increment();
554+
break;
555+
case 'decrement':
556+
this.decrement();
557+
break;
558+
}
559+
}}
560+
accessibilityValue={{
561+
min: 0,
562+
now: this.state.current,
563+
max: 100,
564+
}}>
565+
<Text>Fake Slider</Text>
566+
</View>
567+
<View
568+
accessible={true}
569+
accessibilityLabel="Equalizer"
570+
accessibilityRole="adjustable"
571+
accessibilityActions={[{name: 'increment'}, {name: 'decrement'}]}
572+
onAccessibilityAction={event => {
573+
switch (event.nativeEvent.actionName) {
574+
case 'increment':
575+
if (this.state.textualValue === 'center') {
576+
this.setState({textualValue: 'right'});
577+
} else if (this.state.textualValue === 'left') {
578+
this.setState({textualValue: 'center'});
579+
}
580+
break;
581+
case 'decrement':
582+
if (this.state.textualValue === 'center') {
583+
this.setState({textualValue: 'left'});
584+
} else if (this.state.textualValue === 'right') {
585+
this.setState({textualValue: 'center'});
586+
}
587+
break;
588+
}
589+
}}
590+
accessibilityValue={{text: this.state.textualValue}}>
591+
<Text>Equalizer</Text>
592+
</View>
593+
</View>
594+
);
595+
}
596+
}
597+
515598
class ScreenReaderStatusExample extends React.Component<{}> {
516599
state = {
517600
screenReaderEnabled: false,
@@ -591,6 +674,12 @@ exports.examples = [
591674
return <AccessibilityActionsExample />;
592675
},
593676
},
677+
{
678+
title: 'Fake Slider Example',
679+
render(): React.Element<typeof FakeSliderExample> {
680+
return <FakeSliderExample />;
681+
},
682+
},
594683
{
595684
title: 'Check if the screen reader is enabled',
596685
render(): React.Element<typeof ScreenReaderStatusExample> {

React/Views/RCTView.m

+20
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,26 @@ - (NSString *)accessibilityValue
271271
[valueComponents addObject:stateDescriptions[@"busy"]];
272272
}
273273
}
274+
275+
// handle accessibilityValue
276+
277+
if (self.accessibilityValueInternal) {
278+
id min = self.accessibilityValueInternal[@"min"];
279+
id now = self.accessibilityValueInternal[@"now"];
280+
id max = self.accessibilityValueInternal[@"max"];
281+
id text = self.accessibilityValueInternal[@"text"];
282+
if (text && [text isKindOfClass:[NSString class]]) {
283+
[valueComponents addObject:text];
284+
} else if ([min isKindOfClass:[NSNumber class]] &&
285+
[now isKindOfClass:[NSNumber class]] &&
286+
[max isKindOfClass:[NSNumber class]] &&
287+
([min intValue] < [max intValue]) &&
288+
([min intValue] <= [now intValue] && [now intValue] <= [max intValue])) {
289+
int val = ([now intValue]*100)/([max intValue]-[min intValue]);
290+
[valueComponents addObject:[NSString stringWithFormat:@"%d percent", val]];
291+
}
292+
}
293+
274294
if (valueComponents.count > 0) {
275295
return [valueComponents componentsJoinedByString:@", "];
276296
}

React/Views/RCTViewManager.m

+1
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ - (RCTShadowView *)shadowView
126126
RCT_REMAP_VIEW_PROPERTY(accessibilityActions, reactAccessibilityElement.accessibilityActions, NSDictionaryArray)
127127
RCT_REMAP_VIEW_PROPERTY(accessibilityLabel, reactAccessibilityElement.accessibilityLabel, NSString)
128128
RCT_REMAP_VIEW_PROPERTY(accessibilityHint, reactAccessibilityElement.accessibilityHint, NSString)
129+
RCT_REMAP_VIEW_PROPERTY(accessibilityValue, reactAccessibilityElement.accessibilityValueInternal, NSDictionary)
129130
RCT_REMAP_VIEW_PROPERTY(accessibilityViewIsModal, reactAccessibilityElement.accessibilityViewIsModal, BOOL)
130131
RCT_REMAP_VIEW_PROPERTY(accessibilityElementsHidden, reactAccessibilityElement.accessibilityElementsHidden, BOOL)
131132
RCT_REMAP_VIEW_PROPERTY(accessibilityIgnoresInvertColors, reactAccessibilityElement.shouldAccessibilityIgnoresInvertColors, BOOL)

React/Views/UIView+React.h

+1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
@property (nonatomic, copy) NSString *accessibilityRole;
120120
@property (nonatomic, copy) NSDictionary<NSString *, id> *accessibilityState;
121121
@property (nonatomic, copy) NSArray <NSDictionary *> *accessibilityActions;
122+
@property (nonatomic, copy) NSDictionary *accessibilityValueInternal;
122123

123124
/**
124125
* Used in debugging to get a description of the view hierarchy rooted at

React/Views/UIView+React.m

+9-1
Original file line numberDiff line numberDiff line change
@@ -327,8 +327,16 @@ - (void)setAccessibilityState:(NSDictionary<NSString *, id> *)accessibilityState
327327
objc_setAssociatedObject(self, @selector(accessibilityState), accessibilityState, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
328328
}
329329

330-
#pragma mark - Debug
330+
- (NSDictionary<NSString *, id> *)accessibilityValueInternal
331+
{
332+
return objc_getAssociatedObject(self, _cmd);
333+
}
334+
- (void)setAccessibilityValueInternal:(NSDictionary<NSString *, id> *)accessibilityValue
335+
{
336+
objc_setAssociatedObject(self, @selector(accessibilityValueInternal), accessibilityValue, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
337+
}
331338

339+
#pragma mark - Debug
332340
- (void)react_addRecursiveDescriptionToString:(NSMutableString *)string atLevel:(NSUInteger)level
333341
{
334342
for (NSUInteger i = 0; i < level; i++) {

ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java

+19
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ private void updateViewContentDescription(@NonNull T view) {
179179
final ReadableMap accessibilityState = (ReadableMap) view.getTag(R.id.accessibility_state);
180180
final String accessibilityHint = (String) view.getTag(R.id.accessibility_hint);
181181
final List<String> contentDescription = new ArrayList<>();
182+
final ReadableMap accessibilityValue = (ReadableMap) view.getTag(R.id.accessibility_value);
182183
if (accessibilityLabel != null) {
183184
contentDescription.add(accessibilityLabel);
184185
}
@@ -205,6 +206,12 @@ private void updateViewContentDescription(@NonNull T view) {
205206
}
206207
}
207208
}
209+
if (accessibilityValue != null && accessibilityValue.hasKey("text")) {
210+
final Dynamic text = accessibilityValue.getDynamic("text");
211+
if (text != null && text.getType() == ReadableType.String) {
212+
contentDescription.add(text.asString());
213+
}
214+
}
208215
if (accessibilityHint != null) {
209216
contentDescription.add(accessibilityHint);
210217
}
@@ -223,6 +230,18 @@ public void setAccessibilityActions(T view, ReadableArray accessibilityActions)
223230
view.setTag(R.id.accessibility_actions, accessibilityActions);
224231
}
225232

233+
@ReactProp(name = ViewProps.ACCESSIBILITY_VALUE)
234+
public void setAccessibilityValue(T view, ReadableMap accessibilityValue) {
235+
if (accessibilityValue == null) {
236+
return;
237+
}
238+
239+
view.setTag(R.id.accessibility_value, accessibilityValue);
240+
if (accessibilityValue.hasKey("text")) {
241+
updateViewContentDescription(view);
242+
}
243+
}
244+
226245
@Override
227246
@ReactProp(name = ViewProps.IMPORTANT_FOR_ACCESSIBILITY)
228247
public void setImportantForAccessibility(

0 commit comments

Comments
 (0)