Fix(#296): accessibility issues and add test tags to UI elements (#309)

This commit addresses the accessibility issues described in Issue 296:
1. Replaced `pointerInput` gestures with `combinedClickable(role = Role.Button)` in `ChatBubble.kt` and `ChatScreen.kt` to ensure screen readers announce interactive text elements and chat bubbles correctly.
2. Added `role = Role.Button` to the clickable modifiers in `SessionsScreen.kt`.
3. Introduced proper `testTag` modifiers throughout interactive components like `session_card`, `chat_bubble_user`, `chat_bubble_assistant`, and `clarify_option` to facilitate UI testing.
This commit is contained in:
M57 authored and GitHub committed 2026-06-22 21:36:46 +03:00
1 parent e5f0df9273
commit 8e040543de
3 files changed
+62 -34

No files matched your search

@@ -10,8 +10,10 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -51,11 +53,12 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
@@ -126,6 +129,7 @@ fun ChatBubble(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun UserBubble(
message: ChatMessage,
@@ -198,13 +202,14 @@ private fun UserBubble(
bottomEnd = 4.dp,
),
).background(brush = gradientBrush)
.pointerInput(message.content) {
detectTapGestures(
onLongPress = {
showCopyButton = true
},
)
},
.testTag("chat_bubble_user")
.combinedClickable(
role = Role.Button,
onClick = {},
onLongClick = {
showCopyButton = true
},
),
color = Color.Transparent,
tonalElevation = 0.dp,
) {
@@ -265,6 +270,7 @@ private fun UserBubble(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun AssistantBubble(
message: ChatMessage,
@@ -320,13 +326,14 @@ private fun AssistantBubble(
bottomStart = 16.dp,
bottomEnd = 16.dp,
),
).pointerInput(message.content) {
detectTapGestures(
onLongPress = {
showCopyButton = true
},
)
},
).testTag("chat_bubble_assistant")
.combinedClickable(
role = Role.Button,
onClick = {},
onLongClick = {
showCopyButton = true
},
),
color = bubbleColor,
border =
BorderStroke(
@@ -677,9 +684,9 @@ private fun ToolBubble(
textDecoration = TextDecoration.Underline,
),
modifier =
Modifier.pointerInput(Unit) {
detectTapGestures(onTap = { showRawJson = true })
},
Modifier
.testTag("chat_tool_show_raw")
.clickable(role = Role.Button) { showRawJson = true },
)
} else {
Text(
@@ -702,9 +709,9 @@ private fun ToolBubble(
textDecoration = TextDecoration.Underline,
),
modifier =
Modifier.pointerInput(Unit) {
detectTapGestures(onTap = { showRawJson = false })
},
Modifier
.testTag("chat_tool_show_parsed")
.clickable(role = Role.Button) { showRawJson = false },
)
}
}
@@ -18,9 +18,10 @@ import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -68,6 +69,7 @@ import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -88,10 +90,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
@@ -111,6 +113,7 @@ import com.m57.hermescontrol.ui.common.HermesScaffold
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ChatScreen(
modifier: Modifier = Modifier,
@@ -832,6 +835,7 @@ private fun ChatInputBar(
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ClarifyDialog(
clarify: ClarifyUi,
@@ -856,20 +860,34 @@ private fun ClarifyDialog(
)
Spacer(modifier = Modifier.height(4.dp))
clarify.options.forEach { option ->
FilledTonalButton(
onClick = { /* onTap handled by pointerInput */ },
Surface(
onClick = { typedText = option },
modifier =
Modifier
.fillMaxWidth()
.pointerInput(option) {
detectTapGestures(
onTap = { typedText = option },
onLongPress = { onOptionSelected(option) },
)
},
.testTag("clarify_option"),
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.secondaryContainer,
) {
Text(option)
Box(
modifier =
Modifier
.combinedClickable(
role = Role.Button,
onClick = { typedText = option },
onLongClick = { onOptionSelected(option) },
)
.padding(horizontal = 24.dp, vertical = 10.dp),
contentAlignment = Alignment.Center,
) {
Text(
text = option,
color = MaterialTheme.colorScheme.onSecondaryContainer,
style = MaterialTheme.typography.labelLarge,
)
}
}
Spacer(modifier = Modifier.height(4.dp))
}
}
@@ -24,7 +24,9 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -108,7 +110,8 @@ fun SessionsScreen(
modifier =
Modifier
.fillMaxWidth()
.clickable {
.testTag("session_card_${session.id}")
.clickable(role = Role.Button) {
NavigationController.pendingSessionId = session.id
NavigationController.navigateTo(ChatScreen)
},