From 7e805ef1ca3e92d340c4a59c612ffa03ec7614be Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 17 May 2026 00:22:13 +0000 Subject: [PATCH] #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. --- CHANGELOG.md | 6 + .../keymapper/base/actions/ActionListItem.kt | 24 +++- .../keymapper/base/actions/ActionsScreen.kt | 10 ++ .../base/trigger/BaseTriggerScreen.kt | 10 ++ .../base/trigger/TriggerKeyListItem.kt | 126 ++++++++++-------- base/src/main/res/values/strings.xml | 2 + 6 files changed, 123 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23d05d7c4f..3bbeb134f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +## Changed + +- #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. + ## [4.1.1](https://github.com/sds100/KeyMapper/releases/tag/v4.1.1) #### 15 May 2026 diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt index f842381aae..e4e00967d4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionListItem.kt @@ -39,6 +39,9 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.CustomAccessibilityAction +import androidx.compose.ui.semantics.customActions +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -62,11 +65,16 @@ fun ActionListItem( onRemoveClick: () -> Unit = {}, onFixClick: () -> Unit = {}, onTestClick: () -> Unit = {}, + onMoveUp: (() -> Unit)? = null, + onMoveDown: (() -> Unit)? = null, ) { val draggableState = rememberDraggableState { dragDropState?.onDrag(Offset(0f, it)) } + val moveUpLabel = stringResource(R.string.accessibility_action_move_up) + val moveDownLabel = stringResource(R.string.accessibility_action_move_down) + Column(modifier = modifier.fillMaxWidth()) { ElevatedCard( modifier = Modifier @@ -83,7 +91,19 @@ fun ActionListItem( dragDropState?.onDragStart(index, offset) }, onDragStopped = { dragDropState?.onDragInterrupted() }, - ), + ) + .semantics { + if (isReorderingEnabled) { + customActions = buildList { + onMoveUp?.let { action -> + add(CustomAccessibilityAction(moveUpLabel) { action(); true }) + } + onMoveDown?.let { action -> + add(CustomAccessibilityAction(moveDownLabel) { action(); true }) + } + } + } + }, colors = CardDefaults.elevatedCardColors( containerColor = if (isDragging) { MaterialTheme.colorScheme.surfaceContainerHighest @@ -102,7 +122,7 @@ fun ActionListItem( Icon( modifier = Modifier.size(24.dp), imageVector = Icons.Rounded.DragHandle, - contentDescription = null, + contentDescription = stringResource(R.string.drag_handle_for, model.text), tint = MaterialTheme.colorScheme.onSurface, ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt index b7fec30f2c..4198aa2287 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionsScreen.kt @@ -314,6 +314,16 @@ private fun ActionList( onRemoveClick = { onRemoveClick(model.id) }, onFixClick = { onFixErrorClick(model.id) }, onTestClick = { onTestClick(model.id) }, + onMoveUp = if (isReorderingEnabled && index > 0) { + { onMove(index, index - 1) } + } else { + null + }, + onMoveDown = if (isReorderingEnabled && index < actionList.size - 1) { + { onMove(index, index + 1) } + } else { + null + }, ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt index 735ae05da8..2415f99184 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt @@ -482,6 +482,16 @@ private fun TriggerList( onEditClick = { onEditClick(model.id) }, onRemoveClick = { onRemoveClick(model.id) }, onFixClick = onFixErrorClick, + onMoveUp = if (isReorderingEnabled && index > 0) { + { onMove(index, index - 1) } + } else { + null + }, + onMoveDown = if (isReorderingEnabled && index < triggerList.size - 1) { + { onMove(index, index + 1) } + } else { + null + }, ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt index 8053721ef3..a1e4ee66a4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt @@ -39,6 +39,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.CustomAccessibilityAction +import androidx.compose.ui.semantics.customActions +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -60,11 +63,67 @@ fun TriggerKeyListItem( onEditClick: () -> Unit = {}, onRemoveClick: () -> Unit = {}, onFixClick: (TriggerError) -> Unit = {}, + onMoveUp: (() -> Unit)? = null, + onMoveDown: (() -> Unit)? = null, ) { val draggableState = rememberDraggableState { dragDropState?.onDrag(Offset(0f, it)) } + val primaryText = when (model) { + is TriggerKeyListItemModel.Assistant -> when (model.assistantType) { + AssistantTriggerType.ANY -> stringResource( + R.string.assistant_any_trigger_name, + ) + + AssistantTriggerType.VOICE -> stringResource( + R.string.assistant_voice_trigger_name, + ) + + AssistantTriggerType.DEVICE -> stringResource( + R.string.assistant_device_trigger_name, + ) + } + + is TriggerKeyListItemModel.FloatingButton -> if (model.buttonName.isBlank()) { + stringResource(R.string.trigger_key_floating_button_description_empty) + } else { + stringResource( + R.string.trigger_key_floating_button_description, + model.buttonName, + ) + } + + is TriggerKeyListItemModel.KeyEvent -> model.keyName + + is TriggerKeyListItemModel.EvdevEvent -> model.keyName + + is TriggerKeyListItemModel.FloatingButtonDeleted -> stringResource( + R.string.trigger_error_floating_button_deleted_title, + ) + + is TriggerKeyListItemModel.FingerprintGesture -> when (model.gestureType) { + FingerprintGestureType.SWIPE_UP -> stringResource( + R.string.trigger_key_fingerprint_gesture_up, + ) + + FingerprintGestureType.SWIPE_DOWN -> stringResource( + R.string.trigger_key_fingerprint_gesture_down, + ) + + FingerprintGestureType.SWIPE_LEFT -> stringResource( + R.string.trigger_key_fingerprint_gesture_left, + ) + + FingerprintGestureType.SWIPE_RIGHT -> stringResource( + R.string.trigger_key_fingerprint_gesture_right, + ) + } + } + + val moveUpLabel = stringResource(R.string.accessibility_action_move_up) + val moveDownLabel = stringResource(R.string.accessibility_action_move_down) + Column(modifier = modifier.fillMaxWidth()) { ElevatedCard( modifier = Modifier @@ -81,7 +140,19 @@ fun TriggerKeyListItem( dragDropState?.onDragStart(index, offset) }, onDragStopped = { dragDropState?.onDragInterrupted() }, - ), + ) + .semantics { + if (isReorderingEnabled) { + customActions = buildList { + onMoveUp?.let { action -> + add(CustomAccessibilityAction(moveUpLabel) { action(); true }) + } + onMoveDown?.let { action -> + add(CustomAccessibilityAction(moveDownLabel) { action(); true }) + } + } + } + }, colors = CardDefaults.elevatedCardColors( containerColor = if (isDragging) { MaterialTheme.colorScheme.surfaceContainerHighest @@ -100,7 +171,7 @@ fun TriggerKeyListItem( Icon( modifier = Modifier.size(24.dp), imageVector = Icons.Rounded.DragHandle, - contentDescription = null, + contentDescription = stringResource(R.string.drag_handle_for, primaryText), tint = MaterialTheme.colorScheme.onSurface, ) } @@ -127,57 +198,6 @@ fun TriggerKeyListItem( } } - val primaryText = when (model) { - is TriggerKeyListItemModel.Assistant -> when (model.assistantType) { - AssistantTriggerType.ANY -> stringResource( - R.string.assistant_any_trigger_name, - ) - - AssistantTriggerType.VOICE -> stringResource( - R.string.assistant_voice_trigger_name, - ) - - AssistantTriggerType.DEVICE -> stringResource( - R.string.assistant_device_trigger_name, - ) - } - - is TriggerKeyListItemModel.FloatingButton -> if (model.buttonName.isBlank()) { - stringResource(R.string.trigger_key_floating_button_description_empty) - } else { - stringResource( - R.string.trigger_key_floating_button_description, - model.buttonName, - ) - } - - is TriggerKeyListItemModel.KeyEvent -> model.keyName - - is TriggerKeyListItemModel.EvdevEvent -> model.keyName - - is TriggerKeyListItemModel.FloatingButtonDeleted -> stringResource( - R.string.trigger_error_floating_button_deleted_title, - ) - - is TriggerKeyListItemModel.FingerprintGesture -> when (model.gestureType) { - FingerprintGestureType.SWIPE_UP -> stringResource( - R.string.trigger_key_fingerprint_gesture_up, - ) - - FingerprintGestureType.SWIPE_DOWN -> stringResource( - R.string.trigger_key_fingerprint_gesture_down, - ) - - FingerprintGestureType.SWIPE_LEFT -> stringResource( - R.string.trigger_key_fingerprint_gesture_left, - ) - - FingerprintGestureType.SWIPE_RIGHT -> stringResource( - R.string.trigger_key_fingerprint_gesture_right, - ) - } - } - Spacer(Modifier.width(8.dp)) if (model.error == null) { diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index a081813537..c7ff462120 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -486,6 +486,8 @@ 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. 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. Drag handle for %1$s + Move up + Move down Show example Turn on notifications