Skip to content

Commit 7e805ef

Browse files
committed
#1369 feat: improve TalkBack accessibility for reorderable list items
Add contentDescriptions to drag handle icons using the existing drag_handle_for string resource, and add custom accessibility actions ("Move up", "Move down") to trigger key and action list items so TalkBack users can reorder items without needing to perform drag gestures.
1 parent 1273c74 commit 7e805ef

6 files changed

Lines changed: 123 additions & 55 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## Unreleased
2+
3+
## Changed
4+
5+
- #1369 Add content descriptions to drag handles and custom "Move up"/"Move down" accessibility actions for trigger and action list items, improving TalkBack support for reordering.
6+
17
## [4.1.1](https://github.com/sds100/KeyMapper/releases/tag/v4.1.1)
28

39
#### 15 May 2026

base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ import androidx.compose.ui.geometry.Offset
3939
import androidx.compose.ui.graphics.Color
4040
import androidx.compose.ui.platform.LocalContext
4141
import androidx.compose.ui.res.stringResource
42+
import androidx.compose.ui.semantics.CustomAccessibilityAction
43+
import androidx.compose.ui.semantics.customActions
44+
import androidx.compose.ui.semantics.semantics
4245
import androidx.compose.ui.text.style.TextOverflow
4346
import androidx.compose.ui.tooling.preview.Preview
4447
import androidx.compose.ui.unit.dp
@@ -62,11 +65,16 @@ fun ActionListItem(
6265
onRemoveClick: () -> Unit = {},
6366
onFixClick: () -> Unit = {},
6467
onTestClick: () -> Unit = {},
68+
onMoveUp: (() -> Unit)? = null,
69+
onMoveDown: (() -> Unit)? = null,
6570
) {
6671
val draggableState = rememberDraggableState {
6772
dragDropState?.onDrag(Offset(0f, it))
6873
}
6974

75+
val moveUpLabel = stringResource(R.string.accessibility_action_move_up)
76+
val moveDownLabel = stringResource(R.string.accessibility_action_move_down)
77+
7078
Column(modifier = modifier.fillMaxWidth()) {
7179
ElevatedCard(
7280
modifier = Modifier
@@ -83,7 +91,19 @@ fun ActionListItem(
8391
dragDropState?.onDragStart(index, offset)
8492
},
8593
onDragStopped = { dragDropState?.onDragInterrupted() },
86-
),
94+
)
95+
.semantics {
96+
if (isReorderingEnabled) {
97+
customActions = buildList {
98+
onMoveUp?.let { action ->
99+
add(CustomAccessibilityAction(moveUpLabel) { action(); true })
100+
}
101+
onMoveDown?.let { action ->
102+
add(CustomAccessibilityAction(moveDownLabel) { action(); true })
103+
}
104+
}
105+
}
106+
},
87107
colors = CardDefaults.elevatedCardColors(
88108
containerColor = if (isDragging) {
89109
MaterialTheme.colorScheme.surfaceContainerHighest
@@ -102,7 +122,7 @@ fun ActionListItem(
102122
Icon(
103123
modifier = Modifier.size(24.dp),
104124
imageVector = Icons.Rounded.DragHandle,
105-
contentDescription = null,
125+
contentDescription = stringResource(R.string.drag_handle_for, model.text),
106126
tint = MaterialTheme.colorScheme.onSurface,
107127
)
108128
}

base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,16 @@ private fun ActionList(
314314
onRemoveClick = { onRemoveClick(model.id) },
315315
onFixClick = { onFixErrorClick(model.id) },
316316
onTestClick = { onTestClick(model.id) },
317+
onMoveUp = if (isReorderingEnabled && index > 0) {
318+
{ onMove(index, index - 1) }
319+
} else {
320+
null
321+
},
322+
onMoveDown = if (isReorderingEnabled && index < actionList.size - 1) {
323+
{ onMove(index, index + 1) }
324+
} else {
325+
null
326+
},
317327
)
318328
}
319329
}

base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,16 @@ private fun TriggerList(
482482
onEditClick = { onEditClick(model.id) },
483483
onRemoveClick = { onRemoveClick(model.id) },
484484
onFixClick = onFixErrorClick,
485+
onMoveUp = if (isReorderingEnabled && index > 0) {
486+
{ onMove(index, index - 1) }
487+
} else {
488+
null
489+
},
490+
onMoveDown = if (isReorderingEnabled && index < triggerList.size - 1) {
491+
{ onMove(index, index + 1) }
492+
} else {
493+
null
494+
},
485495
)
486496
}
487497
}

base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt

Lines changed: 73 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ import androidx.compose.ui.Alignment
3939
import androidx.compose.ui.Modifier
4040
import androidx.compose.ui.geometry.Offset
4141
import androidx.compose.ui.res.stringResource
42+
import androidx.compose.ui.semantics.CustomAccessibilityAction
43+
import androidx.compose.ui.semantics.customActions
44+
import androidx.compose.ui.semantics.semantics
4245
import androidx.compose.ui.text.style.TextOverflow
4346
import androidx.compose.ui.tooling.preview.Preview
4447
import androidx.compose.ui.unit.dp
@@ -60,11 +63,67 @@ fun TriggerKeyListItem(
6063
onEditClick: () -> Unit = {},
6164
onRemoveClick: () -> Unit = {},
6265
onFixClick: (TriggerError) -> Unit = {},
66+
onMoveUp: (() -> Unit)? = null,
67+
onMoveDown: (() -> Unit)? = null,
6368
) {
6469
val draggableState = rememberDraggableState {
6570
dragDropState?.onDrag(Offset(0f, it))
6671
}
6772

73+
val primaryText = when (model) {
74+
is TriggerKeyListItemModel.Assistant -> when (model.assistantType) {
75+
AssistantTriggerType.ANY -> stringResource(
76+
R.string.assistant_any_trigger_name,
77+
)
78+
79+
AssistantTriggerType.VOICE -> stringResource(
80+
R.string.assistant_voice_trigger_name,
81+
)
82+
83+
AssistantTriggerType.DEVICE -> stringResource(
84+
R.string.assistant_device_trigger_name,
85+
)
86+
}
87+
88+
is TriggerKeyListItemModel.FloatingButton -> if (model.buttonName.isBlank()) {
89+
stringResource(R.string.trigger_key_floating_button_description_empty)
90+
} else {
91+
stringResource(
92+
R.string.trigger_key_floating_button_description,
93+
model.buttonName,
94+
)
95+
}
96+
97+
is TriggerKeyListItemModel.KeyEvent -> model.keyName
98+
99+
is TriggerKeyListItemModel.EvdevEvent -> model.keyName
100+
101+
is TriggerKeyListItemModel.FloatingButtonDeleted -> stringResource(
102+
R.string.trigger_error_floating_button_deleted_title,
103+
)
104+
105+
is TriggerKeyListItemModel.FingerprintGesture -> when (model.gestureType) {
106+
FingerprintGestureType.SWIPE_UP -> stringResource(
107+
R.string.trigger_key_fingerprint_gesture_up,
108+
)
109+
110+
FingerprintGestureType.SWIPE_DOWN -> stringResource(
111+
R.string.trigger_key_fingerprint_gesture_down,
112+
)
113+
114+
FingerprintGestureType.SWIPE_LEFT -> stringResource(
115+
R.string.trigger_key_fingerprint_gesture_left,
116+
)
117+
118+
FingerprintGestureType.SWIPE_RIGHT -> stringResource(
119+
R.string.trigger_key_fingerprint_gesture_right,
120+
)
121+
}
122+
}
123+
124+
val moveUpLabel = stringResource(R.string.accessibility_action_move_up)
125+
val moveDownLabel = stringResource(R.string.accessibility_action_move_down)
126+
68127
Column(modifier = modifier.fillMaxWidth()) {
69128
ElevatedCard(
70129
modifier = Modifier
@@ -81,7 +140,19 @@ fun TriggerKeyListItem(
81140
dragDropState?.onDragStart(index, offset)
82141
},
83142
onDragStopped = { dragDropState?.onDragInterrupted() },
84-
),
143+
)
144+
.semantics {
145+
if (isReorderingEnabled) {
146+
customActions = buildList {
147+
onMoveUp?.let { action ->
148+
add(CustomAccessibilityAction(moveUpLabel) { action(); true })
149+
}
150+
onMoveDown?.let { action ->
151+
add(CustomAccessibilityAction(moveDownLabel) { action(); true })
152+
}
153+
}
154+
}
155+
},
85156
colors = CardDefaults.elevatedCardColors(
86157
containerColor = if (isDragging) {
87158
MaterialTheme.colorScheme.surfaceContainerHighest
@@ -100,7 +171,7 @@ fun TriggerKeyListItem(
100171
Icon(
101172
modifier = Modifier.size(24.dp),
102173
imageVector = Icons.Rounded.DragHandle,
103-
contentDescription = null,
174+
contentDescription = stringResource(R.string.drag_handle_for, primaryText),
104175
tint = MaterialTheme.colorScheme.onSurface,
105176
)
106177
}
@@ -127,57 +198,6 @@ fun TriggerKeyListItem(
127198
}
128199
}
129200

130-
val primaryText = when (model) {
131-
is TriggerKeyListItemModel.Assistant -> when (model.assistantType) {
132-
AssistantTriggerType.ANY -> stringResource(
133-
R.string.assistant_any_trigger_name,
134-
)
135-
136-
AssistantTriggerType.VOICE -> stringResource(
137-
R.string.assistant_voice_trigger_name,
138-
)
139-
140-
AssistantTriggerType.DEVICE -> stringResource(
141-
R.string.assistant_device_trigger_name,
142-
)
143-
}
144-
145-
is TriggerKeyListItemModel.FloatingButton -> if (model.buttonName.isBlank()) {
146-
stringResource(R.string.trigger_key_floating_button_description_empty)
147-
} else {
148-
stringResource(
149-
R.string.trigger_key_floating_button_description,
150-
model.buttonName,
151-
)
152-
}
153-
154-
is TriggerKeyListItemModel.KeyEvent -> model.keyName
155-
156-
is TriggerKeyListItemModel.EvdevEvent -> model.keyName
157-
158-
is TriggerKeyListItemModel.FloatingButtonDeleted -> stringResource(
159-
R.string.trigger_error_floating_button_deleted_title,
160-
)
161-
162-
is TriggerKeyListItemModel.FingerprintGesture -> when (model.gestureType) {
163-
FingerprintGestureType.SWIPE_UP -> stringResource(
164-
R.string.trigger_key_fingerprint_gesture_up,
165-
)
166-
167-
FingerprintGestureType.SWIPE_DOWN -> stringResource(
168-
R.string.trigger_key_fingerprint_gesture_down,
169-
)
170-
171-
FingerprintGestureType.SWIPE_LEFT -> stringResource(
172-
R.string.trigger_key_fingerprint_gesture_left,
173-
)
174-
175-
FingerprintGestureType.SWIPE_RIGHT -> stringResource(
176-
R.string.trigger_key_fingerprint_gesture_right,
177-
)
178-
}
179-
}
180-
181201
Spacer(Modifier.width(8.dp))
182202

183203
if (model.error == null) {

base/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,8 @@
486486
<string name="sorting_drag_and_drop_list_help">Drag the handles to adjust priorities. The item at the top is the most important. You must tap the item to enable sorting and toggle ascending/descending.</string>
487487
<string name="sorting_drag_and_drop_list_help_example">Example: To sort by Actions (ascending) first and Triggers (descending) second: tap and drag Actions to the first position and set it to ascending, then tap and drag Triggers to the second position and set it to descending.</string>
488488
<string name="drag_handle_for">Drag handle for %1$s</string>
489+
<string name="accessibility_action_move_up">Move up</string>
490+
<string name="accessibility_action_move_down">Move down</string>
489491
<string name="show_example">Show example</string>
490492

491493
<string name="dialog_title_request_notification_permission">Turn on notifications</string>

0 commit comments

Comments
 (0)