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:
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)
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user