Skip to content

Commit 9d2737f

Browse files
committed
feat: accessibility/talkback support
1 parent 6caaa33 commit 9d2737f

17 files changed

Lines changed: 266 additions & 8 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## Added
11+
12+
- Accessibility (TalkBack) support
13+
1014
## [0.19.0] - 2026-01-11
1115

1216
## Added

android/src/main/java/com/reactnativeandroidwidget/RNWidget.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ public void drawWidget(int widgetId) throws Exception {
7676
Uri bitmapUri = saveBitmapToDisk(widgetId, "light", bitmap);
7777
remoteWidgetView.setImageViewUri(R.id.rn_widget_image_light, bitmapUri);
7878

79+
if (widgetWithViews.getRootAccessibilityLabel() != null) {
80+
remoteWidgetView.setContentDescription(R.id.rn_widget_clickable_container, widgetWithViews.getRootAccessibilityLabel());
81+
}
82+
7983
if (dark != null) {
8084
WidgetWithViews darkWidgetWithViews = WidgetFactory.buildWidgetFromRoot(
8185
appContext,
@@ -175,6 +179,10 @@ private void addClickableArea(RemoteViews widgetView, ViewGroup rootWidget, Clic
175179

176180
clickableRemoteView.setInt(R.id.rn_widget_clickable_area, "setMinimumHeight", offsetViewBounds.height());
177181

182+
if (clickableView.getAccessibilityLabel() != null) {
183+
clickableRemoteView.setContentDescription(R.id.rn_widget_clickable_area, clickableView.getAccessibilityLabel());
184+
}
185+
178186
registerClickTask(widgetId, clickableView, clickableRemoteView, R.id.rn_widget_clickable_area);
179187

180188
widgetView.addView(R.id.rn_widget_clickable_container, clickableRemoteView);
@@ -258,6 +266,12 @@ private Intent createCollectionRemoteAdapterIntent(int widgetId, int collectionI
258266
bundle.putInt("imageWidth", collectionViewItem.getBitmap().getWidth());
259267
bundle.putString("lightImageName", RNWidgetCollectionService.getImageName(widgetId, collectionId, i, "light"));
260268
bundle.putString("darkImageName", RNWidgetCollectionService.getImageName(widgetId, collectionId, i, "dark"));
269+
270+
// Add item accessibility label if available
271+
if (collectionViewItem.getItemAccessibilityLabel() != null) {
272+
bundle.putString("itemAccessibilityLabel", collectionViewItem.getItemAccessibilityLabel());
273+
}
274+
261275
collectionItemsBundle.add(bundle);
262276

263277
ArrayList<Bundle> clickableAreas = new ArrayList<>();
@@ -275,6 +289,11 @@ private Intent createCollectionRemoteAdapterIntent(int widgetId, int collectionI
275289
collectionViewMap.putInt("width", offsetViewBounds.width());
276290
collectionViewMap.putString("clickAction", clickableView.getClickAction());
277291
collectionViewMap.putBundle("clickActionData", Arguments.toBundle(clickableView.getClickActionData()));
292+
293+
// Add clickable area accessibility label if available
294+
if (clickableView.getAccessibilityLabel() != null) {
295+
collectionViewMap.putString("accessibilityLabel", clickableView.getAccessibilityLabel());
296+
}
278297

279298
clickableAreas.add(collectionViewMap);
280299
}

android/src/main/java/com/reactnativeandroidwidget/RNWidgetCollectionService.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ public RemoteViews getViewAt(int position) {
7777
Uri darkImageUri = RNWidgetImageProvider.getImageUri(mContext, bundle.getString("darkImageName"));
7878
listItemView.setImageViewUri(R.id.rn_widget_list_item_dark, darkImageUri);
7979

80+
// Set item accessibility label on ImageViews if available
81+
if (bundle.getString("itemAccessibilityLabel", null) != null) {
82+
String itemAccessibilityLabel = bundle.getString("itemAccessibilityLabel");
83+
listItemView.setContentDescription(R.id.rn_widget_list_item_light, itemAccessibilityLabel);
84+
listItemView.setContentDescription(R.id.rn_widget_list_item_dark, itemAccessibilityLabel);
85+
}
86+
8087
// Set a fill-intent which will be used to fill-in the pending intent template
8188
// which is set on the collection view in RNWidget.
8289
if (bundle.getString("clickAction", null) != null) {
@@ -109,6 +116,11 @@ private void addClickableArea(RemoteViews widgetView, Bundle clickableArea, int
109116

110117
clickableRemoteView.setInt(R.id.rn_widget_clickable_area, "setMinimumHeight", clickableArea.getInt("height"));
111118

119+
// Set accessibility label on clickable area if available
120+
if (clickableArea.getString("accessibilityLabel", null) != null) {
121+
clickableRemoteView.setContentDescription(R.id.rn_widget_clickable_area, clickableArea.getString("accessibilityLabel"));
122+
}
123+
112124
PendingIntent pendingIntent = createPendingIntent(clickableArea);
113125
clickableRemoteView.setOnClickPendingIntent(R.id.rn_widget_clickable_area, pendingIntent);
114126

android/src/main/java/com/reactnativeandroidwidget/builder/ClickableView.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,18 @@ public class ClickableView implements Comparable<ClickableView> {
99
private final View view;
1010
private final String clickAction;
1111
private final ReadableMap clickActionData;
12+
private final String accessibilityLabel;
1213

1314
public ClickableView(String id, View view, String clickAction, ReadableMap clickActionData) {
15+
this(id, view, clickAction, clickActionData, null);
16+
}
17+
18+
public ClickableView(String id, View view, String clickAction, ReadableMap clickActionData, String accessibilityLabel) {
1419
this.id = id;
1520
this.view = view;
1621
this.clickAction = clickAction;
1722
this.clickActionData = clickActionData;
23+
this.accessibilityLabel = accessibilityLabel;
1824
}
1925

2026
public String getId() {
@@ -33,6 +39,10 @@ public ReadableMap getClickActionData() {
3339
return clickActionData;
3440
}
3541

42+
public String getAccessibilityLabel() {
43+
return accessibilityLabel;
44+
}
45+
3646
@Override
3747
public int compareTo(ClickableView o) {
3848
return this.id.compareTo(o.getId());

android/src/main/java/com/reactnativeandroidwidget/builder/CollectionView.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,18 +61,25 @@ public void buildChildren(ReactApplicationContext appContext) throws Exception {
6161
List<ClickableView> clickableViews = widgetWithViews.getClickableViews();
6262
Bitmap bitmap = getBitmap(rootView.getChildAt(0));
6363

64+
// Extract item accessibility label from original config
65+
String itemAccessibilityLabel = null;
66+
if (configClone.getMap("props").hasKey("accessibilityLabel")) {
67+
itemAccessibilityLabel = configClone.getMap("props").getString("accessibilityLabel");
68+
}
69+
6470
if (configClone.getMap("props").hasKey("clickAction")) {
6571
renderedViews.add(
6672
new CollectionViewItem(
6773
rootView,
6874
bitmap,
6975
clickableViews,
7076
configClone.getMap("props").getString("clickAction"),
71-
configClone.getMap("props").getMap("clickActionData")
77+
configClone.getMap("props").getMap("clickActionData"),
78+
itemAccessibilityLabel
7279
)
7380
);
7481
} else {
75-
renderedViews.add(new CollectionViewItem(rootView, bitmap, clickableViews));
82+
renderedViews.add(new CollectionViewItem(rootView, bitmap, clickableViews, null, null, itemAccessibilityLabel));
7683
}
7784
}
7885
}

android/src/main/java/com/reactnativeandroidwidget/builder/CollectionViewItem.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import android.graphics.Bitmap;
44
import android.view.ViewGroup;
55

6+
import androidx.annotation.Nullable;
7+
68
import com.facebook.react.bridge.Arguments;
79
import com.facebook.react.bridge.ReadableMap;
810

@@ -14,8 +16,13 @@ public class CollectionViewItem {
1416
private final List<ClickableView> clickableViews;
1517
private final String clickAction;
1618
private final ReadableMap clickActionData;
19+
private final String itemAccessibilityLabel;
1720

1821
public CollectionViewItem(ViewGroup view, Bitmap bitmap, List<ClickableView> clickableViews, String clickAction, ReadableMap clickActionData) {
22+
this(view, bitmap, clickableViews, clickAction, clickActionData, null);
23+
}
24+
25+
public CollectionViewItem(ViewGroup view, Bitmap bitmap, List<ClickableView> clickableViews, String clickAction, ReadableMap clickActionData, @Nullable String itemAccessibilityLabel) {
1926
this.view = view;
2027
this.bitmap = bitmap;
2128
this.clickableViews = clickableViews;
@@ -25,10 +32,11 @@ public CollectionViewItem(ViewGroup view, Bitmap bitmap, List<ClickableView> cli
2532
} else {
2633
this.clickActionData = clickActionData;
2734
}
35+
this.itemAccessibilityLabel = itemAccessibilityLabel;
2836
}
2937

3038
public CollectionViewItem(ViewGroup view, Bitmap bitmap, List<ClickableView> clickableViews) {
31-
this(view, bitmap, clickableViews, null, null);
39+
this(view, bitmap, clickableViews, null, null, null);
3240
}
3341

3442
public ViewGroup getView() {
@@ -50,4 +58,9 @@ public String getClickAction() {
5058
public ReadableMap getClickActionData() {
5159
return clickActionData;
5260
}
61+
62+
@Nullable
63+
public String getItemAccessibilityLabel() {
64+
return itemAccessibilityLabel;
65+
}
5366
}

android/src/main/java/com/reactnativeandroidwidget/builder/WidgetFactory.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,26 @@
2929
public class WidgetFactory {
3030
private final List<ClickableView> clickableViews;
3131
private final List<CollectionView> collectionViews;
32+
private String rootAccessibilityLabel;
3233

3334
private WidgetFactory() {
3435
this.clickableViews = new ArrayList<>();
3536
this.collectionViews = new ArrayList<>();
37+
this.rootAccessibilityLabel = null;
3638
}
3739

3840
public static WidgetWithViews buildWidgetFromRoot(ReactApplicationContext context, ReadableMap config, int width, int height) throws Exception {
3941
WidgetFactory widgetFactory = new WidgetFactory();
4042

41-
View view = widgetFactory.buildWidget(context, widgetFactory.getRootConfig(config, width, height), "0");
43+
ReadableMap configClone = Arguments.makeNativeMap(config.toHashMap());
44+
45+
ReadableMap rootConfig = widgetFactory.getRootConfig(config, width, height);
46+
47+
if (configClone.hasKey("props") && configClone.getMap("props").hasKey("accessibilityLabel")) {
48+
widgetFactory.rootAccessibilityLabel = configClone.getMap("props").getString("accessibilityLabel");
49+
}
50+
51+
View view = widgetFactory.buildWidget(context, rootConfig, "0");
4252

4353
for (int i = 0; i < widgetFactory.collectionViews.size(); i++) {
4454
widgetFactory.collectionViews.get(i).buildChildren(context);
@@ -47,7 +57,7 @@ public static WidgetWithViews buildWidgetFromRoot(ReactApplicationContext contex
4757
ResourceUtils.clear();
4858

4959
Collections.sort(widgetFactory.clickableViews);
50-
return new WidgetWithViews(view, widgetFactory.clickableViews, widgetFactory.collectionViews);
60+
return new WidgetWithViews(view, widgetFactory.clickableViews, widgetFactory.collectionViews, widgetFactory.rootAccessibilityLabel);
5161
}
5262

5363
@NonNull
@@ -81,12 +91,17 @@ private View buildWidget(ReactApplicationContext context, ReadableMap config, St
8191
}
8292

8393
if (config.getMap("props").hasKey("clickAction")) {
94+
String accessibilityLabel = null;
95+
if (config.getMap("props").hasKey("accessibilityLabel")) {
96+
accessibilityLabel = config.getMap("props").getString("accessibilityLabel");
97+
}
8498
clickableViews.add(
8599
new ClickableView(
86100
id,
87101
view,
88102
config.getMap("props").getString("clickAction"),
89-
config.getMap("props").getMap("clickActionData")
103+
config.getMap("props").getMap("clickActionData"),
104+
accessibilityLabel
90105
)
91106
);
92107
}

android/src/main/java/com/reactnativeandroidwidget/builder/WidgetWithViews.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,25 @@
22

33
import android.view.View;
44

5+
import androidx.annotation.Nullable;
6+
57
import java.util.List;
68

79
public class WidgetWithViews {
810
private final View rootView;
911
private final List<ClickableView> clickableViews;
1012
private final List<CollectionView> collectionViews;
13+
private final String rootAccessibilityLabel;
1114

1215
WidgetWithViews(View rootView, List<ClickableView> clickableViews, List<CollectionView> collectionViews) {
16+
this(rootView, clickableViews, collectionViews, null);
17+
}
18+
19+
WidgetWithViews(View rootView, List<ClickableView> clickableViews, List<CollectionView> collectionViews, @Nullable String rootAccessibilityLabel) {
1320
this.rootView = rootView;
1421
this.clickableViews = clickableViews;
1522
this.collectionViews = collectionViews;
23+
this.rootAccessibilityLabel = rootAccessibilityLabel;
1624
}
1725

1826
public View getRootView() {
@@ -26,4 +34,9 @@ public List<ClickableView> getClickableViews() {
2634
public List<CollectionView> getCollectionViews() {
2735
return collectionViews;
2836
}
37+
38+
@Nullable
39+
public String getRootAccessibilityLabel() {
40+
return rootAccessibilityLabel;
41+
}
2942
}

docs/docs/demo.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,18 @@ You can download the demo app with the example widgets from the [Releases Page](
1313

1414
## List Widget Preview
1515

16+
This widget is accessible
17+
1618
![List Widget Preview](../../example-expo/assets/widget-preview/list.png)
1719

20+
## Dark Mode Widget Preview
21+
22+
![Dark Mode Widget Preview](../../example-expo/assets/widget-preview/darkmode.png)
23+
1824
## Resizable Music Widget Preview
1925

26+
This widget is accessible
27+
2028
![Resizable Music Widget Preview](../../example-expo/assets/widget-preview/resizable.png)
2129

2230
## Rotated Widget Preview

docs/docs/tutorial/congratulations.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
sidebar_position: 9
2+
sidebar_position: 10
33
---
44

55
# Congratulations!

0 commit comments

Comments
 (0)