Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add accessibilityValueDescription support. #26169

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Libraries/Components/View/ReactNativeViewAttributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const UIView = {
accessibilityLiveRegion: true,
accessibilityRole: true,
accessibilityState: true,
accessibilityValue: true,
accessibilityHint: true,
importantForAccessibility: true,
nativeID: true,
Expand Down
1 change: 1 addition & 0 deletions Libraries/Components/View/ReactNativeViewViewConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const ReactNativeViewConfig = {
accessibilityRole: true,
accessibilityStates: true, // TODO: Can be removed after next release
accessibilityState: true,
accessibilityValue: true,
accessibilityViewIsModal: true,
accessible: true,
alignContent: true,
Expand Down
22 changes: 22 additions & 0 deletions Libraries/Components/View/ViewAccessibility.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,25 @@ export type AccessibilityState = {
busy?: boolean,
expanded?: boolean,
};

export type AccessibilityValue = $ReadOnly<{|
/**
* The minimum value of this component's range. (should be an integer)
*/
min?: number,

/**
* The maximum value of this component's range. (should be an integer)
*/
max?: number,

/**
* The current value of this component's range. (should be an integer)
*/
now?: number,

/**
* A textual description of this component's value. (will override minimum, current, and maximum if set)
*/
text?: string,
|}>;
2 changes: 2 additions & 0 deletions Libraries/Components/View/ViewPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {TVViewProps} from '../AppleTV/TVViewPropTypes';
import type {
AccessibilityRole,
AccessibilityState,
AccessibilityValue,
AccessibilityActionEvent,
AccessibilityActionInfo,
} from './ViewAccessibility';
Expand Down Expand Up @@ -413,6 +414,7 @@ export type ViewProps = $ReadOnly<{|
* Indicates to accessibility services that UI Component is in a specific State.
*/
accessibilityState?: ?AccessibilityState,
accessibilityValue?: ?AccessibilityValue,

/**
* Provides an array of custom actions available for accessibility.
Expand Down
1 change: 1 addition & 0 deletions Libraries/DeprecatedPropTypes/DeprecatedViewPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ module.exports = {
>),

accessibilityState: PropTypes.object,
accessibilityValue: PropTypes.object,
/**
* Indicates to accessibility services whether the user should be notified
* when this view changes. Works for Android API >= 19 only.
Expand Down
89 changes: 89 additions & 0 deletions RNTester/js/examples/Accessibility/AccessibilityExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,89 @@ class AccessibilityActionsExample extends React.Component {
);
}
}

class FakeSliderExample extends React.Component {
state = {
current: 50,
textualValue: 'center',
};

increment = () => {
let newValue = this.state.current + 2;
if (newValue > 100) {
newValue = 100;
}
this.setState({
current: newValue,
});
};

decrement = () => {
let newValue = this.state.current - 2;
if (newValue < 0) {
newValue = 0;
}
this.setState({
current: newValue,
});
};

render() {
return (
<View>
<View
accessible={true}
accessibilityLabel="Fake Slider"
accessibilityRole="adjustable"
accessibilityActions={[{name: 'increment'}, {name: 'decrement'}]}
onAccessibilityAction={event => {
switch (event.nativeEvent.actionName) {
case 'increment':
this.increment();
break;
case 'decrement':
this.decrement();
break;
}
}}
accessibilityValue={{
min: 0,
now: this.state.current,
max: 100,
}}>
<Text>Fake Slider</Text>
</View>
<View
accessible={true}
accessibilityLabel="Equalizer"
accessibilityRole="adjustable"
accessibilityActions={[{name: 'increment'}, {name: 'decrement'}]}
onAccessibilityAction={event => {
switch (event.nativeEvent.actionName) {
case 'increment':
if (this.state.textualValue === 'center') {
this.setState({textualValue: 'right'});
} else if (this.state.textualValue === 'left') {
this.setState({textualValue: 'center'});
}
break;
case 'decrement':
if (this.state.textualValue === 'center') {
this.setState({textualValue: 'left'});
} else if (this.state.textualValue === 'right') {
this.setState({textualValue: 'center'});
}
break;
}
}}
accessibilityValue={{text: this.state.textualValue}}>
<Text>Equalizer</Text>
</View>
</View>
);
}
}

class ScreenReaderStatusExample extends React.Component<{}> {
state = {
screenReaderEnabled: false,
Expand Down Expand Up @@ -591,6 +674,12 @@ exports.examples = [
return <AccessibilityActionsExample />;
},
},
{
title: 'Fake Slider Example',
render(): React.Element<typeof FakeSliderExample> {
return <FakeSliderExample />;
},
},
{
title: 'Check if the screen reader is enabled',
render(): React.Element<typeof ScreenReaderStatusExample> {
Expand Down
20 changes: 20 additions & 0 deletions React/Views/RCTView.m
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,26 @@ - (NSString *)accessibilityValue
[valueComponents addObject:stateDescriptions[@"busy"]];
}
}

// handle accessibilityValue

if (self.accessibilityValueInternal) {
id min = self.accessibilityValueInternal[@"min"];
id now = self.accessibilityValueInternal[@"now"];
id max = self.accessibilityValueInternal[@"max"];
id text = self.accessibilityValueInternal[@"text"];
if (text && [text isKindOfClass:[NSString class]]) {
[valueComponents addObject:text];
} else if ([min isKindOfClass:[NSNumber class]] &&
[now isKindOfClass:[NSNumber class]] &&
[max isKindOfClass:[NSNumber class]] &&
([min intValue] < [max intValue]) &&
([min intValue] <= [now intValue] && [now intValue] <= [max intValue])) {
int val = ([now intValue]*100)/([max intValue]-[min intValue]);
[valueComponents addObject:[NSString stringWithFormat:@"%d percent", val]];
}
}

if (valueComponents.count > 0) {
return [valueComponents componentsJoinedByString:@", "];
}
Expand Down
1 change: 1 addition & 0 deletions React/Views/RCTViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ - (RCTShadowView *)shadowView
RCT_REMAP_VIEW_PROPERTY(accessibilityActions, reactAccessibilityElement.accessibilityActions, NSDictionaryArray)
RCT_REMAP_VIEW_PROPERTY(accessibilityLabel, reactAccessibilityElement.accessibilityLabel, NSString)
RCT_REMAP_VIEW_PROPERTY(accessibilityHint, reactAccessibilityElement.accessibilityHint, NSString)
RCT_REMAP_VIEW_PROPERTY(accessibilityValue, reactAccessibilityElement.accessibilityValueInternal, NSDictionary)
RCT_REMAP_VIEW_PROPERTY(accessibilityViewIsModal, reactAccessibilityElement.accessibilityViewIsModal, BOOL)
RCT_REMAP_VIEW_PROPERTY(accessibilityElementsHidden, reactAccessibilityElement.accessibilityElementsHidden, BOOL)
RCT_REMAP_VIEW_PROPERTY(accessibilityIgnoresInvertColors, reactAccessibilityElement.shouldAccessibilityIgnoresInvertColors, BOOL)
Expand Down
1 change: 1 addition & 0 deletions React/Views/UIView+React.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
@property (nonatomic, copy) NSString *accessibilityRole;
@property (nonatomic, copy) NSDictionary<NSString *, id> *accessibilityState;
@property (nonatomic, copy) NSArray <NSDictionary *> *accessibilityActions;
@property (nonatomic, copy) NSDictionary *accessibilityValueInternal;

/**
* Used in debugging to get a description of the view hierarchy rooted at
Expand Down
10 changes: 9 additions & 1 deletion React/Views/UIView+React.m
Original file line number Diff line number Diff line change
Expand Up @@ -327,8 +327,16 @@ - (void)setAccessibilityState:(NSDictionary<NSString *, id> *)accessibilityState
objc_setAssociatedObject(self, @selector(accessibilityState), accessibilityState, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

#pragma mark - Debug
- (NSDictionary<NSString *, id> *)accessibilityValueInternal
{
return objc_getAssociatedObject(self, _cmd);
}
- (void)setAccessibilityValueInternal:(NSDictionary<NSString *, id> *)accessibilityValue
{
objc_setAssociatedObject(self, @selector(accessibilityValueInternal), accessibilityValue, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

#pragma mark - Debug
- (void)react_addRecursiveDescriptionToString:(NSMutableString *)string atLevel:(NSUInteger)level
{
for (NSUInteger i = 0; i < level; i++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ private void updateViewContentDescription(@NonNull T view) {
final ReadableMap accessibilityState = (ReadableMap) view.getTag(R.id.accessibility_state);
final String accessibilityHint = (String) view.getTag(R.id.accessibility_hint);
final List<String> contentDescription = new ArrayList<>();
final ReadableMap accessibilityValue = (ReadableMap) view.getTag(R.id.accessibility_value);
if (accessibilityLabel != null) {
contentDescription.add(accessibilityLabel);
}
Expand All @@ -205,6 +206,12 @@ private void updateViewContentDescription(@NonNull T view) {
}
}
}
if (accessibilityValue != null && accessibilityValue.hasKey("text")) {
final Dynamic text = accessibilityValue.getDynamic("text");
if (text != null && text.getType() == ReadableType.String) {
contentDescription.add(text.asString());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By default, Talkback will present the range info first in the announcement string (it's essentially treated like "state"), so adding it here at the very end (when its text) seems a bit backwards.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intension is that text and RangeInfo would be mutually exclusive, so if there is text, there wouldn't be RangeInfo set. Do you still have a concern?

}
}
if (accessibilityHint != null) {
contentDescription.add(accessibilityHint);
}
Expand All @@ -223,6 +230,18 @@ public void setAccessibilityActions(T view, ReadableArray accessibilityActions)
view.setTag(R.id.accessibility_actions, accessibilityActions);
}

@ReactProp(name = ViewProps.ACCESSIBILITY_VALUE)
public void setAccessibilityValue(T view, ReadableMap accessibilityValue) {
if (accessibilityValue == null) {
return;
}

view.setTag(R.id.accessibility_value, accessibilityValue);
if (accessibilityValue.hasKey("text")) {
updateViewContentDescription(view);
}
}

@Override
@ReactProp(name = ViewProps.IMPORTANT_FOR_ACCESSIBILITY)
public void setImportantForAccessibility(
Expand Down
Loading