From 5b86518cb19e3ff0ce2a1a87ceb5bf2948aad5dc Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 30 Sep 2026 11:12:23 -0400 Subject: [PATCH 1/2] fix(a11y): announce selection state on picker rows The currency, token, group-currency, and group-invite pickers mark the picked row with the ic_checked/ic_unchecked image, but gave the image an empty or null description and the row no selection state, so TalkBack read every row the same. The single-choice rows (ListRowItem, TokenBalanceRow with the Checkbox style, AllCurrenciesCard) now use Modifier.selectable with Role.RadioButton, and TalkBack says "selected" or "not selected". A TokenBalanceRow that shows the mark without taking taps sets `selected` through semantics instead. The group invite list is multi-select, so its rows use Modifier.toggleable with Role.Checkbox and are read as "checked" or "not checked". The check images stay decorative. Currencies without a rate were not clickable before; they are now disabled selectable rows, so TalkBack also reads them as disabled. --- .../flipcash/app/core/ui/TokenBalanceRow.kt | 24 +++++++++++++++---- .../internal/screens/GroupInviteSheet.kt | 8 ++++++- .../screens/SelectGroupCurrencySheet.kt | 5 ++-- .../internal/components/ListRowItem.kt | 16 +++++++------ 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/TokenBalanceRow.kt b/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/TokenBalanceRow.kt index 5b7484f45b..8f0f20b716 100644 --- a/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/TokenBalanceRow.kt +++ b/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/TokenBalanceRow.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -23,6 +24,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.selected +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp @@ -182,13 +186,25 @@ fun TokenBalanceRow( ) { val exchange = LocalExchange.current val backgroundColor = CodeTheme.colors.background + // The checkbox mark is drawn, not announced, so the row carries the selection instead. + val selection = isSelected.takeIf { styling.selectionStyle == TokenSelectionStyle.Checkbox } + val isClickable = onClick != null && isEnabled Row( modifier = Modifier - .addIf(onClick != null && isEnabled) { - Modifier.clickable { - onClick?.invoke() + .addIf(isClickable) { + if (selection != null) { + Modifier.selectable(selected = selection, role = Role.RadioButton) { + onClick?.invoke() + } + } else { + Modifier.clickable { + onClick?.invoke() + } } } + .addIf(!isClickable && selection != null) { + Modifier.semantics(mergeDescendants = true) { selected = selection == true } + } .then(modifier) .addIf(!isEnabled) { Modifier.drawWithContent { @@ -325,7 +341,7 @@ fun TokenBalanceRow( if (isSelected) R.drawable.ic_checked else R.drawable.ic_unchecked ), - contentDescription = "" + contentDescription = null ) } } diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/GroupInviteSheet.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/GroupInviteSheet.kt index e5adf3cc0f..31fcbb8aea 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/GroupInviteSheet.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/GroupInviteSheet.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.input.rememberTextFieldState @@ -389,7 +390,12 @@ private fun RecentChatRow( Row( modifier = Modifier .fillMaxWidth() - .clickable(enabled = enabled, role = Role.Checkbox, onClick = onClick) + .toggleable( + value = selected, + enabled = enabled, + role = Role.Checkbox, + onValueChange = { onClick() }, + ) .padding(horizontal = ListInset), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(RowGap), diff --git a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/screens/SelectGroupCurrencySheet.kt b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/screens/SelectGroupCurrencySheet.kt index f84f9fed83..6fe6a57ef5 100644 --- a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/screens/SelectGroupCurrencySheet.kt +++ b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/screens/SelectGroupCurrencySheet.kt @@ -3,7 +3,6 @@ package com.flipcash.app.tipping.internal.screens import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -11,6 +10,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon @@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp @@ -179,7 +180,7 @@ private fun AllCurrenciesCard( color = if (selected) White50 else CardOutline, shape = shape, ) - .clickable(onClick = onClick) + .selectable(selected = selected, role = Role.RadioButton, onClick = onClick) .padding( horizontal = CodeTheme.dimens.inset - CardMargin, vertical = CodeTheme.dimens.grid.x3, diff --git a/apps/flipcash/shared/region-selection/ui/src/main/kotlin/com/flipcash/app/currency/internal/components/ListRowItem.kt b/apps/flipcash/shared/region-selection/ui/src/main/kotlin/com/flipcash/app/currency/internal/components/ListRowItem.kt index 2d72bb4dee..e288c7a7ee 100644 --- a/apps/flipcash/shared/region-selection/ui/src/main/kotlin/com/flipcash/app/currency/internal/components/ListRowItem.kt +++ b/apps/flipcash/shared/region-selection/ui/src/main/kotlin/com/flipcash/app/currency/internal/components/ListRowItem.kt @@ -24,7 +24,8 @@ import com.flipcash.app.currency.internal.RegionListItem import com.flipcash.features.currency.R import com.getcode.theme.CodeTheme import com.getcode.ui.components.SwipeActionRow -import androidx.compose.foundation.clickable +import androidx.compose.foundation.selection.selectable +import androidx.compose.ui.semantics.Role @Composable internal fun ListRowItem( @@ -39,11 +40,12 @@ internal fun ListRowItem( modifier = Modifier .fillMaxSize() .background(CodeTheme.colors.background) - .let { - if (item.currency.rate > 0) { - it.clickable { onClick() } - } else it - } + .selectable( + selected = isSelected, + enabled = item.currency.rate > 0, + role = Role.RadioButton, + onClick = onClick, + ) ) { Box( modifier = Modifier.fillMaxSize() @@ -88,7 +90,7 @@ internal fun ListRowItem( if (isSelected) R.drawable.ic_checked else R.drawable.ic_unchecked ), - contentDescription = "" + contentDescription = null ) } } From 6d4f975d14831d9dc11f7e1904a0c9c2b420fcf4 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 30 Sep 2026 11:18:46 -0400 Subject: [PATCH 2/2] fix(a11y): make the currency flag images decorative The flag sits next to the currency name, so an empty description only gave TalkBack an extra unlabelled image to land on. --- .../src/main/kotlin/com/flipcash/app/core/ui/TokenBalanceRow.kt | 2 +- .../flipcash/app/currency/internal/components/ListRowItem.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/TokenBalanceRow.kt b/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/TokenBalanceRow.kt index 8f0f20b716..b8ebb49a66 100644 --- a/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/TokenBalanceRow.kt +++ b/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/TokenBalanceRow.kt @@ -267,7 +267,7 @@ fun TokenBalanceRow( .width(styling.flagSize) .clip(CircleShape), painter = painterResource(it), - contentDescription = "" + contentDescription = null ) } } diff --git a/apps/flipcash/shared/region-selection/ui/src/main/kotlin/com/flipcash/app/currency/internal/components/ListRowItem.kt b/apps/flipcash/shared/region-selection/ui/src/main/kotlin/com/flipcash/app/currency/internal/components/ListRowItem.kt index e288c7a7ee..74dac2af06 100644 --- a/apps/flipcash/shared/region-selection/ui/src/main/kotlin/com/flipcash/app/currency/internal/components/ListRowItem.kt +++ b/apps/flipcash/shared/region-selection/ui/src/main/kotlin/com/flipcash/app/currency/internal/components/ListRowItem.kt @@ -65,7 +65,7 @@ internal fun ListRowItem( .clip(CodeTheme.shapes.large) .align(Alignment.CenterVertically), painter = painterResource(resId), - contentDescription = "" + contentDescription = null ) } Column(