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