From 8e040543debef47a192bcfca7a5e9529df8a6841 Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Mon, 22 Jun 2026 21:36:46 +0300 Subject: [PATCH] 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. --- .../m57/hermescontrol/ui/chat/ChatBubble.kt | 51 +++++++++++-------- .../m57/hermescontrol/ui/chat/ChatScreen.kt | 40 +++++++++++---- .../ui/sessions/SessionsScreen.kt | 5 +- 3 files changed, 62 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt index 78fecff1..c138a7b2 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatBubble.kt @@ -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 }, ) } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt index 34ad5b68..c0a9a245 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt @@ -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)) } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt index 97b3fb34..b635dc11 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsScreen.kt @@ -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) },