mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
fix(ui): stable LazyColumn keys, semantic roles, and content descriptions
Two Compose correctness/accessibility fixes from the UI audit:
* feature/messaging Reaction.kt: three items() blocks in LazyRow/
LazyColumn had no key= parameter, which caused incorrect state and
animation when the underlying lists reorder. Add stable keys: Map.Entry
iterations use it.key; the reaction list uses a composite
packetId:userId:emoji:timestamp because packetId defaults to 0 for
pending/local reactions.
* Accessibility pass across core/ui and feature/{settings,node,
wifi-provision}: add role=Role.Button + onClickLabel to clickable
Box/Column/Row/Text widgets that were rendering as plain containers
to TalkBack (RegularPreference, IndoorAirQuality, NodeFilterTextField,
ClickableTextField trailing icon). Add contentDescription (via
stringResource) to meaningful Close/Filter/PhoneAndroid icons that
previously passed null. Replace hardcoded English strings in
contentDescription slots with six new keys in core/resources
(export_tak_data_package, mpwrd_os translatable=false, clear_time_zone,
filter_icon, remove_filter, show_iaq_legend).
Roughly 200 insertions across 10 files; no behavior change other than
screen-reader output and stable list-item identity.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
df3b5365f9
commit
387acd7a2f
9 changed files with 79 additions and 17 deletions
|
|
@ -1265,4 +1265,8 @@
|
|||
<string name="desktop_tray_show">Show Meshtastic</string>
|
||||
<string name="desktop_tray_quit">Quit</string>
|
||||
<string name="desktop_notification_title">Meshtastic</string>
|
||||
<string name="export_tak_data_package">Export TAK Data Package</string>
|
||||
<string name="mpwrd_os" translatable="false">mPWRD-OS</string>
|
||||
<string name="clear_time_zone">Clear time zone</string>
|
||||
<string name="show_iaq_legend">Show air quality legend</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ fun ClickableTextField(
|
|||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
isError: Boolean = false,
|
||||
trailingIconContentDescription: String? = null,
|
||||
) {
|
||||
val source = remember { MutableInteractionSource() }
|
||||
val isPressed by source.collectIsPressedAsState()
|
||||
|
|
@ -49,7 +50,7 @@ fun ClickableTextField(
|
|||
enabled = enabled,
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(label)) },
|
||||
trailingIcon = { Icon(trailingIcon, null) },
|
||||
trailingIcon = { Icon(trailingIcon, trailingIconContentDescription) },
|
||||
isError = isError,
|
||||
interactionSource = source,
|
||||
modifier = modifier,
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -58,6 +59,7 @@ import org.meshtastic.core.resources.preview_gauge
|
|||
import org.meshtastic.core.resources.preview_gradient
|
||||
import org.meshtastic.core.resources.preview_pill
|
||||
import org.meshtastic.core.resources.preview_text
|
||||
import org.meshtastic.core.resources.show_iaq_legend
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.ThumbUp
|
||||
import org.meshtastic.core.ui.icon.Warning
|
||||
|
|
@ -120,13 +122,18 @@ fun IndoorAirQuality(iaq: Int?, displayMode: IaqDisplayMode = IaqDisplayMode.Pil
|
|||
Column {
|
||||
when (displayMode) {
|
||||
IaqDisplayMode.Pill -> {
|
||||
val legendLabel = stringResource(Res.string.show_iaq_legend)
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.clip(RoundedCornerShape(10.dp))
|
||||
.background(iaqEnum.color)
|
||||
.width(125.dp)
|
||||
.height(30.dp)
|
||||
.clickable { isLegendOpen = true },
|
||||
.clickable(
|
||||
onClickLabel = legendLabel,
|
||||
role = Role.Button,
|
||||
onClick = { isLegendOpen = true },
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(4.dp).align(Alignment.CenterStart),
|
||||
|
|
@ -144,7 +151,15 @@ fun IndoorAirQuality(iaq: Int?, displayMode: IaqDisplayMode = IaqDisplayMode.Pil
|
|||
}
|
||||
|
||||
IaqDisplayMode.Dot -> {
|
||||
Column(modifier = Modifier.clickable { isLegendOpen = true }) {
|
||||
val legendLabel = stringResource(Res.string.show_iaq_legend)
|
||||
Column(
|
||||
modifier =
|
||||
Modifier.clickable(
|
||||
onClickLabel = legendLabel,
|
||||
role = Role.Button,
|
||||
onClick = { isLegendOpen = true },
|
||||
),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(text = "$iaq")
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
|
@ -154,17 +169,30 @@ fun IndoorAirQuality(iaq: Int?, displayMode: IaqDisplayMode = IaqDisplayMode.Pil
|
|||
}
|
||||
|
||||
IaqDisplayMode.Text -> {
|
||||
val legendLabel = stringResource(Res.string.show_iaq_legend)
|
||||
Text(
|
||||
text = getIaqDescriptionWithRange(iaqEnum),
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier.clickable { isLegendOpen = true },
|
||||
modifier =
|
||||
Modifier.clickable(
|
||||
onClickLabel = legendLabel,
|
||||
role = Role.Button,
|
||||
onClick = { isLegendOpen = true },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
IaqDisplayMode.Gauge -> {
|
||||
val legendLabel = stringResource(Res.string.show_iaq_legend)
|
||||
CircularProgressIndicator(
|
||||
progress = { iaq / 500f },
|
||||
modifier = Modifier.size(60.dp).clickable { isLegendOpen = true },
|
||||
modifier =
|
||||
Modifier.size(60.dp)
|
||||
.clickable(
|
||||
onClickLabel = legendLabel,
|
||||
role = Role.Button,
|
||||
onClick = { isLegendOpen = true },
|
||||
),
|
||||
strokeWidth = 8.dp,
|
||||
color = iaqEnum.color,
|
||||
)
|
||||
|
|
@ -172,9 +200,15 @@ fun IndoorAirQuality(iaq: Int?, displayMode: IaqDisplayMode = IaqDisplayMode.Pil
|
|||
}
|
||||
|
||||
IaqDisplayMode.Gradient -> {
|
||||
val legendLabel = stringResource(Res.string.show_iaq_legend)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.clickable { isLegendOpen = true },
|
||||
modifier =
|
||||
Modifier.clickable(
|
||||
onClickLabel = legendLabel,
|
||||
role = Role.Button,
|
||||
onClick = { isLegendOpen = true },
|
||||
),
|
||||
) {
|
||||
LinearProgressIndicator(
|
||||
progress = { iaq / 500f },
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -80,7 +81,13 @@ fun RegularPreference(
|
|||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
}
|
||||
|
||||
Column(modifier = modifier.fillMaxWidth().clickable(enabled = enabled, onClick = onClick).padding(all = 16.dp)) {
|
||||
Column(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = enabled, onClick = onClick, role = Role.Button)
|
||||
.padding(all = 16.dp),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
FlowRow(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ internal fun ReactionRow(
|
|||
|
||||
AnimatedVisibility(emojiGroups.isNotEmpty(), modifier = modifier) {
|
||||
LazyRow(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
items(emojiGroups.entries.toList()) { entry ->
|
||||
items(emojiGroups.entries.toList(), key = { it.key }) { entry ->
|
||||
val emoji = entry.key
|
||||
val reactions = entry.value
|
||||
val localReaction = reactions.find { it.user.id == DataPacket.ID_LOCAL || it.user.id == myId }
|
||||
|
|
@ -237,7 +237,7 @@ internal fun ReactionDialog(
|
|||
}
|
||||
|
||||
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
items(groupedEmojis.entries.toList()) { entry ->
|
||||
items(groupedEmojis.entries.toList(), key = { it.key }) { entry ->
|
||||
val emoji = entry.key
|
||||
val reactions = entry.value
|
||||
val localReaction = reactions.find { it.user.id == DataPacket.ID_LOCAL || it.user.id == myId }
|
||||
|
|
@ -265,7 +265,7 @@ internal fun ReactionDialog(
|
|||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
items(filteredReactions) { reaction ->
|
||||
items(filteredReactions, key = { reaction -> "${reaction.user.id}:${reaction.emoji}" }) { reaction ->
|
||||
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusEvent
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
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.TextAlign
|
||||
|
|
@ -56,6 +57,7 @@ import androidx.compose.ui.unit.dp
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.NodeSortOption
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.clear
|
||||
import org.meshtastic.core.resources.desc_node_filter_clear
|
||||
import org.meshtastic.core.resources.node_filter_exclude_infrastructure
|
||||
import org.meshtastic.core.resources.node_filter_exclude_mqtt
|
||||
|
|
@ -178,14 +180,19 @@ private fun NodeFilterTextField(filterText: String, onTextChange: (String) -> Un
|
|||
onValueChange = onTextChange,
|
||||
trailingIcon = {
|
||||
if (filterText.isNotEmpty() || isFocused) {
|
||||
val clearLabel = stringResource(Res.string.clear)
|
||||
Icon(
|
||||
MeshtasticIcons.Close,
|
||||
contentDescription = stringResource(Res.string.desc_node_filter_clear),
|
||||
modifier =
|
||||
Modifier.clickable {
|
||||
onTextChange("")
|
||||
focusManager.clearFocus()
|
||||
},
|
||||
Modifier.clickable(
|
||||
onClickLabel = clearLabel,
|
||||
role = Role.Button,
|
||||
onClick = {
|
||||
onTextChange("")
|
||||
focusManager.clearFocus()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ import org.meshtastic.core.resources.are_you_sure
|
|||
import org.meshtastic.core.resources.button_gpio
|
||||
import org.meshtastic.core.resources.buzzer_gpio
|
||||
import org.meshtastic.core.resources.cancel
|
||||
import org.meshtastic.core.resources.clear_time_zone
|
||||
import org.meshtastic.core.resources.config_device_doubleTapAsButtonPress_summary
|
||||
import org.meshtastic.core.resources.config_device_ledHeartbeatEnabled_summary
|
||||
import org.meshtastic.core.resources.config_device_tripleClickAsAdHocPing_summary
|
||||
|
|
@ -269,7 +270,10 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit
|
|||
onValueChanged = { formState.value = formState.value.copy(tzdef = it) },
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { formState.value = formState.value.copy(tzdef = "") }) {
|
||||
Icon(imageVector = MeshtasticIcons.Close, contentDescription = null)
|
||||
Icon(
|
||||
imageVector = MeshtasticIcons.Close,
|
||||
contentDescription = stringResource(Res.string.clear_time_zone),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import org.meshtastic.core.model.getColorFrom
|
|||
import org.meshtastic.core.model.getStringResFrom
|
||||
import org.meshtastic.core.repository.TakPrefs
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.export_tak_data_package
|
||||
import org.meshtastic.core.resources.tak
|
||||
import org.meshtastic.core.resources.tak_config
|
||||
import org.meshtastic.core.resources.tak_role
|
||||
|
|
@ -74,7 +75,10 @@ fun TAKConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
|||
onBack = onBack,
|
||||
actions = {
|
||||
IconButton(onClick = { exportLauncher("Meshtastic_TAK_Server.zip") }) {
|
||||
Icon(imageVector = MeshtasticIcons.Share, contentDescription = "Export TAK Data Package")
|
||||
Icon(
|
||||
imageVector = MeshtasticIcons.Share,
|
||||
contentDescription = stringResource(Res.string.export_tak_data_package),
|
||||
)
|
||||
}
|
||||
},
|
||||
configState = formState,
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ import org.meshtastic.core.resources.back
|
|||
import org.meshtastic.core.resources.cancel
|
||||
import org.meshtastic.core.resources.hide_password
|
||||
import org.meshtastic.core.resources.img_mpwrd_logo
|
||||
import org.meshtastic.core.resources.mpwrd_os
|
||||
import org.meshtastic.core.resources.password
|
||||
import org.meshtastic.core.resources.show_password
|
||||
import org.meshtastic.core.resources.wifi_provision_available_networks
|
||||
|
|
@ -513,7 +514,7 @@ internal fun MpwrdDisclaimerBanner() {
|
|||
) {
|
||||
Image(
|
||||
painter = painterResource(Res.drawable.img_mpwrd_logo),
|
||||
contentDescription = "mPWRD-OS",
|
||||
contentDescription = stringResource(Res.string.mpwrd_os),
|
||||
modifier = Modifier.size(MPWRD_LOGO_SIZE_DP.dp).clip(RoundedCornerShape(8.dp)),
|
||||
)
|
||||
AutoLinkText(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue