Skip to content

Commit 0d7308c

Browse files
author
Mike Young
authored
Add accessibility improvements for screen readers to announce custom units (#175)
1 parent a64722c commit 0d7308c

File tree

8 files changed

+144
-11
lines changed

8 files changed

+144
-11
lines changed

src/android/src/main/java/com/reactnativecommunity/slider/ReactSlider.java

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,18 @@
1111
import android.graphics.BitmapFactory;
1212
import android.graphics.drawable.BitmapDrawable;
1313
import android.os.Build;
14-
15-
import androidx.annotation.RequiresApi;
16-
import androidx.appcompat.widget.AppCompatSeekBar;
17-
1814
import android.util.AttributeSet;
19-
15+
import android.view.accessibility.AccessibilityEvent;
16+
import android.view.accessibility.AccessibilityManager;
17+
import androidx.appcompat.widget.AppCompatSeekBar;
2018
import java.net.URL;
19+
import java.util.List;
20+
import java.util.Timer;
21+
import java.util.TimerTask;
2122
import java.util.concurrent.Callable;
2223
import java.util.concurrent.ExecutorService;
2324
import java.util.concurrent.Executors;
2425
import java.util.concurrent.Future;
25-
2626
import javax.annotation.Nullable;
2727

2828
/**
@@ -60,6 +60,10 @@ public class ReactSlider extends AppCompatSeekBar {
6060

6161
private double mStepCalculated = 0;
6262

63+
private String mAccessibilityUnits;
64+
65+
private List<String> mAccessibilityIncrements;
66+
6367
public ReactSlider(Context context, @Nullable AttributeSet attrs, int style) {
6468
super(context, attrs, style);
6569
disableStateListAnimatorIfNeeded();
@@ -94,6 +98,64 @@ private void disableStateListAnimatorIfNeeded() {
9498
updateAll();
9599
}
96100

101+
void setAccessibilityUnits(String accessibilityUnits) {
102+
mAccessibilityUnits = accessibilityUnits;
103+
}
104+
105+
void setAccessibilityIncrements(List<String> accessibilityIncrements) {
106+
mAccessibilityIncrements = accessibilityIncrements;
107+
}
108+
109+
@Override
110+
public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
111+
super.onPopulateAccessibilityEvent(event);
112+
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED ||
113+
(event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED && this.isAccessibilityFocused())) {
114+
this.setupAccessibility();
115+
}
116+
}
117+
118+
@Override
119+
public void announceForAccessibility(CharSequence text) {
120+
Context ctx = this.getContext();
121+
final AccessibilityManager manager = (AccessibilityManager) ctx.getSystemService(Context.ACCESSIBILITY_SERVICE);
122+
123+
if (manager.isEnabled()) {
124+
final AccessibilityEvent e = AccessibilityEvent.obtain();
125+
e.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
126+
e.setClassName(this.getClass().getName());
127+
e.setPackageName(ctx.getPackageName());
128+
e.getText().add(text);
129+
130+
TimerTask task = new TimerTask() {
131+
@Override
132+
public void run() {
133+
manager.sendAccessibilityEvent(e);
134+
}
135+
};
136+
137+
Timer timer = new Timer();
138+
timer.schedule(task, 1000);
139+
}
140+
}
141+
142+
private void setupAccessibility() {
143+
if (mAccessibilityUnits != null && mAccessibilityIncrements != null && mAccessibilityIncrements.size() - 1 == (int)mMaxValue) {
144+
int index = (int)mValue;
145+
String sliderValue = mAccessibilityIncrements.get(index);
146+
int stringLength = mAccessibilityUnits.length();
147+
148+
String spokenUnits = mAccessibilityUnits;
149+
if (sliderValue != null && Integer.parseInt(sliderValue) == 1) {
150+
spokenUnits = spokenUnits.substring(0, stringLength - 1);
151+
}
152+
153+
this.announceForAccessibility(String.format("%s %s", sliderValue, spokenUnits));
154+
}
155+
}
156+
157+
158+
97159
/**
98160
* Convert SeekBar's native progress value (e.g. 0..100) to a value passed to JS (e.g. -1.0..2.5).
99161
*/

src/android/src/main/java/com/reactnativecommunity/slider/ReactSliderManager.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
import android.graphics.drawable.Drawable;
1313
import android.graphics.drawable.LayerDrawable;
1414
import android.view.View;
15-
import android.view.ViewGroup;
1615
import android.widget.SeekBar;
1716
import com.facebook.react.bridge.ReactContext;
17+
import com.facebook.react.bridge.ReadableArray;
1818
import com.facebook.react.bridge.ReadableMap;
1919
import com.facebook.react.common.MapBuilder;
2020
import com.facebook.react.uimanager.LayoutShadowNode;
@@ -27,8 +27,9 @@
2727
import com.facebook.yoga.YogaMeasureMode;
2828
import com.facebook.yoga.YogaMeasureOutput;
2929
import com.facebook.yoga.YogaNode;
30+
import java.util.ArrayList;
31+
import java.util.List;
3032
import java.util.Map;
31-
3233
import javax.annotation.Nullable;
3334

3435
/**
@@ -213,6 +214,21 @@ public void setInverted(ReactSlider view, boolean inverted) {
213214
else view.setScaleX(1f);
214215
}
215216

217+
@ReactProp(name = "accessibilityUnits")
218+
public void setAccessibilityUnits(ReactSlider view, String accessibilityUnits) {
219+
view.setAccessibilityUnits(accessibilityUnits);
220+
}
221+
222+
@ReactProp(name = "accessibilityIncrements")
223+
public void setAccessibilityIncrements(ReactSlider view, ReadableArray accessibilityIncrements) {
224+
List objectList = accessibilityIncrements.toArrayList();
225+
List<String> stringList = new ArrayList<>();
226+
for(Object item: objectList) {
227+
stringList.add((String)item);
228+
}
229+
view.setAccessibilityIncrements(stringList);
230+
}
231+
216232
@Override
217233
protected void addEventEmitters(final ThemedReactContext reactContext, final ReactSlider view) {
218234
view.setOnSeekBarChangeListener(ON_CHANGE_LISTENER);

src/ios/RNCSlider.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
@property (nonatomic, strong) UIImage *trackImage;
2222
@property (nonatomic, strong) UIImage *minimumTrackImage;
2323
@property (nonatomic, strong) UIImage *maximumTrackImage;
24-
2524
@property (nonatomic, strong) UIImage *thumbImage;
26-
25+
@property (nonatomic, strong) NSString *accessibilityUnits;
26+
@property (nonatomic, strong) NSArray *accessibilityIncrements;
2727

2828
@end

src/ios/RNCSlider.m

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,30 @@ - (void)setValue:(float)value
2121
{
2222
_unclippedValue = value;
2323
super.value = value;
24+
[self setupAccessibility:value];
2425
}
2526

2627
- (void)setValue:(float)value animated:(BOOL)animated
2728
{
2829
_unclippedValue = value;
2930
[super setValue:value animated:animated];
31+
[self setupAccessibility:value];
32+
}
33+
34+
- (void)setupAccessibility:(float)value
35+
{
36+
if (self.accessibilityUnits && self.accessibilityIncrements && [self.accessibilityIncrements count] - 1 == (int)self.maximumValue) {
37+
int index = (int)value;
38+
NSString *sliderValue = (NSString *)[self.accessibilityIncrements objectAtIndex:index];
39+
NSUInteger stringLength = [self.accessibilityUnits length];
40+
41+
NSString *spokenUnits = [NSString stringWithString:self.accessibilityUnits];
42+
if (sliderValue && [sliderValue intValue] == 1) {
43+
spokenUnits = [spokenUnits substringToIndex:stringLength-1];
44+
}
45+
46+
self.accessibilityValue = [NSString stringWithFormat:@"%@ %@", sliderValue, spokenUnits];
47+
}
3048
}
3149

3250
- (void)setMinimumValue:(float)minimumValue
@@ -104,7 +122,8 @@ - (void)setInverted:(BOOL)inverted
104122
}
105123
}
106124

107-
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
125+
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
126+
{
108127
return YES;
109128
}
110129

src/ios/RNCSliderManager.m

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ - (void)sliderTouchEnd:(RNCSlider *)sender
9999
RCT_EXPORT_VIEW_PROPERTY(thumbTintColor, UIColor);
100100
RCT_EXPORT_VIEW_PROPERTY(thumbImage, UIImage);
101101
RCT_EXPORT_VIEW_PROPERTY(inverted, BOOL);
102+
RCT_EXPORT_VIEW_PROPERTY(accessibilityUnits, NSString);
103+
RCT_EXPORT_VIEW_PROPERTY(accessibilityIncrements, NSArray);
104+
102105
RCT_CUSTOM_VIEW_PROPERTY(disabled, BOOL, RNCSlider)
103106
{
104107
if (json) {

src/js/RNCSliderNativeComponent.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ type Event = SyntheticEvent<
2727

2828
type NativeProps = $ReadOnly<{|
2929
...ViewProps,
30+
accessibilityUnits?: string,
31+
accessibilityIncrements?: Array<string>,
3032
disabled?: ?boolean,
3133
enabled?: ?boolean,
3234
inverted?: ?boolean,

src/js/Slider.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,22 @@ type Props = $ReadOnly<{|
147147
* Default value is false.
148148
*/
149149
inverted?: ?boolean,
150+
151+
/**
152+
* A string of one or more words to be announced by the screen reader.
153+
* Otherwise, it will announce the value as a percentage.
154+
* Requires passing a value to `accessibilityIncrements` to work correctly.
155+
* Should be a plural word, as singular units will be handled.
156+
*/
157+
accessibilityUnits?: string,
158+
159+
/**
160+
* An array of values that represent the different increments displayed
161+
* by the slider. All the values passed into this prop must be strings.
162+
* Requires passing a value to `accessibilityUnits` to work correctly.
163+
* The number of elements must be the same as `maximumValue`.
164+
*/
165+
accessibilityIncrements?: Array<string>,
150166
|}>;
151167

152168
/**

src/typings/index.d.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,21 @@ export interface SliderProps extends SliderPropsIOS, SliderPropsAndroid {
108108
*/
109109
inverted?: boolean;
110110

111+
/**
112+
* A string of one or more words to be announced by the screen reader.
113+
* Otherwise, it will announce the value as a percentage.
114+
* Requires passing a value to `accessibilityIncrements` to work correctly.
115+
* Should be a plural word, as singular units will be handled.
116+
*/
117+
accessibilityUnits?: string;
118+
119+
/**
120+
* A string of one or more words to be announced by the screen reader.
121+
* Otherwise, it will announce the value as a percentage.
122+
* Requires passing a value to `accessibilityIncrements` to work correctly.
123+
* Should be a plural word, as singular units will be handled.
124+
*/
125+
accessibilityIncrements?: Array<string>;
111126
}
112127

113128
/**

0 commit comments

Comments
 (0)