Skip to content

Commit dc95ce0

Browse files
authored
Improve channel list TalkBack accessibility (#6436)
* Fix duplicated TalkBack readout in channel preview `MessagePreviewContent` and `DraftPreviewContent` exposed a consolidated `text` semantic on the parent `Row` to read the sender+message (or "Draft:"+text) as one utterance, but the child `Text` composables still merged their own text into the surrounding clickable's accessibility tree, so TalkBack announced each preview twice. Switching to `clearAndSetSemantics` replaces the children and emits the composed preview once. * Fix TalkBack crash on mentions at the start of a message `tagUser` in `TextUtils` decrements the mention's start index by 1 to include the leading `@` in the highlighted span. When the mentioned user name appears at index 0 of the message text, that produces a span start of -1. Compose tolerates the invalid span at render time, but TalkBack asks the platform to convert the `AnnotatedString` to a spannable, where `Spannable.setSpan` strictly validates and throws `IndexOutOfBoundsException: setSpan (-1 ...) starts before 0`, crashing the app the moment an accessibility service tries to read the message list. Clamp the highlight start to 0 in that edge case so the span stays valid; visually the leading `@` only drops out of the highlight when no `@` actually precedes the name. * Make channel list rows accessible to TalkBack Channel rows previously announced as "Channel item, …" with the unread badge speaking just the digit, the mute icon silent, and no `onClickLabel` on the row so TalkBack fell back to the generic "double-tap to activate" hint with no verb. Push the a11y content down to the leaves so Compose's natural merge composes them in visual reading order: - The mute `Icon` (both inline and trailing-bottom variants) now carries a localized "muted" `contentDescription`. - `UnreadCountIndicator` overrides its inner numeric `Text` with a plural label ("N unread messages") via `clearAndSetSemantics` on the badge Box. Same component is used by `ThreadItem`, which benefits too. - The outer `Column` drops the static "Channel item" placeholder and declares `onClickLabel = "Open conversation"` / `onLongClickLabel = "Open conversation options"` on `combinedClickable`. Channel name, timestamp, read-status icon, and message preview already expose their own semantics, so the natural merge picks them up unchanged. Adds the four new strings across all 7 supported locales and drops the now-unused `stream_compose_cd_channel_item` placeholder. * Mark list-header title as a TalkBack heading The channel-list and thread-list header titles already render in heading typography but had no `heading()` semantics, so TalkBack's swipe-by-heading gesture skipped them. Add `Modifier.semantics { heading() }` to both the Connected title and the Offline "Disconnected" Text in `DefaultListHeaderCenterContent`. The Connecting branch stays untouched since it shows a transient loading indicator, not a heading. Because `DefaultListHeaderCenterContent` is shared between `ChannelListHeader` and `ThreadListHeader`, both screens gain heading navigation from this single change. * Include action verb in localized channel-item options label The English `stream_compose_channel_item_options` is "Open conversation options" — verb + noun. All seven locale translations dropped the verb ("Opciones de conversación", "Options de la conversation", and so on), which left the TalkBack action label inconsistent with the parallel `stream_compose_channel_item_open` ("Abrir conversación" / "Ouvrir la conversation" / …) where every locale already includes the verb. Re-add the verb so each `_options` label mirrors the verb-prefix convention its sibling `_open` label uses.
1 parent 70033c8 commit dc95ce0

13 files changed

Lines changed: 89 additions & 22 deletions

File tree

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,6 @@ import androidx.compose.ui.platform.LocalResources
4747
import androidx.compose.ui.platform.testTag
4848
import androidx.compose.ui.res.painterResource
4949
import androidx.compose.ui.res.stringResource
50-
import androidx.compose.ui.semantics.contentDescription
51-
import androidx.compose.ui.semantics.semantics
5250
import androidx.compose.ui.text.style.TextOverflow
5351
import androidx.compose.ui.tooling.preview.Preview
5452
import androidx.compose.ui.unit.dp
@@ -142,7 +140,8 @@ public fun ChannelItem(
142140
},
143141
) {
144142
val channel = channelItem.channel
145-
val description = stringResource(id = R.string.stream_compose_cd_channel_item)
143+
val openLabel = stringResource(R.string.stream_compose_channel_item_open)
144+
val optionsLabel = stringResource(R.string.stream_compose_channel_item_options)
146145

147146
val interactionSource = remember { MutableInteractionSource() }
148147
val isFocused by interactionSource.collectIsFocusedAsState()
@@ -154,13 +153,14 @@ public fun ChannelItem(
154153
.testTag("Stream_ChannelItem")
155154
.fillMaxWidth()
156155
.wrapContentHeight()
157-
.semantics { contentDescription = description }
158156
.applyIf(isFocused) { border(2.dp, ChatTheme.colors.borderUtilityFocused, shape) }
159157
.clip(shape)
160158
.applyIf(channelItem.isSelected) { background(ChatTheme.colors.backgroundUtilitySelected, shape) }
161159
.combinedClickable(
162160
onClick = { onChannelClick(channel) },
161+
onClickLabel = openLabel,
163162
onLongClick = { onChannelLongClick(channel) },
163+
onLongClickLabel = optionsLabel,
164164
indication = ripple(),
165165
interactionSource = interactionSource,
166166
),
@@ -283,7 +283,7 @@ private fun TitleRow(
283283
.testTag("Stream_ChannelMutedIcon")
284284
.size(16.dp),
285285
painter = painterResource(id = R.drawable.stream_design_ic_mute),
286-
contentDescription = null,
286+
contentDescription = stringResource(R.string.stream_compose_channel_item_muted),
287287
tint = ChatTheme.colors.textTertiary,
288288
)
289289
}
@@ -350,7 +350,7 @@ private fun MessageRow(
350350
.testTag("Stream_ChannelMutedIcon")
351351
.size(16.dp),
352352
painter = painterResource(id = R.drawable.stream_design_ic_mute),
353-
contentDescription = null,
353+
contentDescription = stringResource(R.string.stream_compose_channel_item_muted),
354354
tint = ChatTheme.colors.textTertiary,
355355
)
356356
}

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/HeaderScaffold.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import androidx.compose.ui.Modifier
3636
import androidx.compose.ui.draw.clip
3737
import androidx.compose.ui.platform.testTag
3838
import androidx.compose.ui.res.stringResource
39+
import androidx.compose.ui.semantics.heading
40+
import androidx.compose.ui.semantics.semantics
3941
import androidx.compose.ui.unit.dp
4042
import io.getstream.chat.android.compose.R
4143
import io.getstream.chat.android.compose.ui.components.avatar.AvatarSize
@@ -140,7 +142,8 @@ internal fun RowScope.DefaultListHeaderCenterContent(
140142
modifier = Modifier
141143
.weight(1f)
142144
.wrapContentWidth()
143-
.padding(horizontal = StreamTokens.spacingMd),
145+
.padding(horizontal = StreamTokens.spacingMd)
146+
.semantics { heading() },
144147
text = title,
145148
style = ChatTheme.typography.headingSmall,
146149
maxLines = 1,
@@ -154,7 +157,8 @@ internal fun RowScope.DefaultListHeaderCenterContent(
154157
modifier = Modifier
155158
.weight(1f)
156159
.wrapContentWidth()
157-
.padding(horizontal = StreamTokens.spacingMd),
160+
.padding(horizontal = StreamTokens.spacingMd)
161+
.semantics { heading() },
158162
text = stringResource(R.string.stream_compose_disconnected),
159163
style = ChatTheme.typography.headingSmall,
160164
maxLines = 1,

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/MessagePreviewContent.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import androidx.compose.ui.graphics.Color
2525
import androidx.compose.ui.platform.LocalContext
2626
import androidx.compose.ui.platform.testTag
2727
import androidx.compose.ui.res.stringResource
28-
import androidx.compose.ui.semantics.semantics
28+
import androidx.compose.ui.semantics.clearAndSetSemantics
2929
import androidx.compose.ui.semantics.text
3030
import androidx.compose.ui.text.AnnotatedString
3131
import androidx.compose.ui.text.TextStyle
@@ -64,7 +64,7 @@ internal fun MessagePreviewContent(
6464
Row(
6565
modifier = modifier
6666
.testTag("Stream_MessagePreview")
67-
.semantics { text = AnnotatedString(fullPreview) },
67+
.clearAndSetSemantics { text = AnnotatedString(fullPreview) },
6868
horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacing3xs),
6969
) {
7070
if (senderName != null) {
@@ -101,7 +101,7 @@ internal fun DraftPreviewContent(
101101
Row(
102102
modifier = modifier
103103
.testTag("Stream_MessagePreview")
104-
.semantics { text = AnnotatedString(fullPreview) },
104+
.clearAndSetSemantics { text = AnnotatedString(fullPreview) },
105105
horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacing3xs),
106106
) {
107107
Text(

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/channels/UnreadCountIndicator.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,13 @@ import androidx.compose.ui.Alignment
2727
import androidx.compose.ui.Modifier
2828
import androidx.compose.ui.graphics.Color
2929
import androidx.compose.ui.platform.testTag
30+
import androidx.compose.ui.res.pluralStringResource
31+
import androidx.compose.ui.semantics.clearAndSetSemantics
32+
import androidx.compose.ui.semantics.contentDescription
3033
import androidx.compose.ui.text.style.TextAlign
3134
import androidx.compose.ui.tooling.preview.Preview
3235
import androidx.compose.ui.unit.dp
36+
import io.getstream.chat.android.compose.R
3337
import io.getstream.chat.android.compose.ui.theme.ChatTheme
3438
import io.getstream.chat.android.compose.ui.theme.StreamTokens
3539

@@ -47,12 +51,18 @@ public fun UnreadCountIndicator(
4751
color: Color = ChatTheme.colors.accentPrimary,
4852
) {
4953
val displayText = if (unreadCount > LimitTooManyUnreadCount) UnreadCountMany else unreadCount.toString()
54+
val description = pluralStringResource(
55+
R.plurals.stream_compose_channel_item_unread,
56+
unreadCount,
57+
unreadCount,
58+
)
5059

5160
Box(
5261
modifier = modifier
5362
.defaultMinSize(minWidth = 20.dp, minHeight = 20.dp)
5463
.background(shape = CircleShape, color = color)
55-
.padding(horizontal = StreamTokens.spacing2xs), // 4dp horizontal content padding
64+
.padding(horizontal = StreamTokens.spacing2xs)
65+
.clearAndSetSemantics { contentDescription = description },
5666
contentAlignment = Alignment.Center,
5767
) {
5868
Text(

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/TextUtils.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,16 +226,21 @@ private fun AnnotatedString.Builder.tagUser(
226226

227227
if (start < 0) return@forEach
228228

229+
// Backtrack one position to include the leading `@`. Clamp to 0 when the mention is at the
230+
// start of the text and no `@` precedes it — otherwise the resulting -1 span start crashes
231+
// TalkBack's spannable conversion with `setSpan (-1 ...) starts before 0`.
232+
val styledStart = (start - 1).coerceAtLeast(0)
233+
229234
addStyle(
230235
style = SpanStyle(color = mentionsColor),
231-
start = start - 1, // -1 to include the @ symbol
236+
start = styledStart,
232237
end = end,
233238
)
234239

235240
addStringAnnotation(
236241
tag = AnnotationTagMention,
237242
annotation = userName,
238-
start = start - 1, // -1 to include the @ symbol
243+
start = styledStart,
239244
end = end,
240245
)
241246
}

stream-chat-android-compose/src/main/res/values-es/strings.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@
5656
<string name="stream_compose_avatar_overflow_count">"+%1$d"</string>
5757
<string name="stream_compose_block_user">"Bloquear usuario"</string>
5858
<string name="stream_compose_cancel">"Cancelar"</string>
59-
<string name="stream_compose_cd_channel_item">"Elemento de canal"</string>
59+
<string name="stream_compose_channel_item_muted">"silenciado"</string>
60+
<string name="stream_compose_channel_item_open">"Abrir conversación"</string>
61+
<string name="stream_compose_channel_item_options">"Abrir opciones de conversación"</string>
6062
<string name="stream_compose_cd_message_item">"Elemento de mensaje"</string>
6163
<string name="stream_compose_cd_play_button">"Botón de reproducción"</string>
6264
<string name="stream_compose_channel_list_draft">"Borrador: "</string>
@@ -259,6 +261,10 @@
259261
<string name="stream_compose_user_status_online">"Conectado"</string>
260262
<string name="stream_compose_video_preview">"Vídeo"</string>
261263
<string name="stream_compose_waiting_for_network">"Esperando conexión de red"</string>
264+
<plurals name="stream_compose_channel_item_unread">
265+
<item quantity="one">"%d mensaje sin leer"</item>
266+
<item quantity="other">"%d mensajes sin leer"</item>
267+
</plurals>
262268
<plurals name="stream_compose_channel_list_typing_many">
263269
<item quantity="one">"%d persona está escribiendo"</item>
264270
<item quantity="other">"%d personas están escribiendo"</item>

stream-chat-android-compose/src/main/res/values-fr/strings.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@
5656
<string name="stream_compose_avatar_overflow_count">"+%1$d"</string>
5757
<string name="stream_compose_block_user">"Bloquer l\'utilisateur"</string>
5858
<string name="stream_compose_cancel">"Annuler"</string>
59-
<string name="stream_compose_cd_channel_item">"Élément de canal"</string>
59+
<string name="stream_compose_channel_item_muted">"en sourdine"</string>
60+
<string name="stream_compose_channel_item_open">"Ouvrir la conversation"</string>
61+
<string name="stream_compose_channel_item_options">"Ouvrir les options de conversation"</string>
6062
<string name="stream_compose_cd_message_item">"Élément de message"</string>
6163
<string name="stream_compose_cd_play_button">"Bouton de lecture"</string>
6264
<string name="stream_compose_channel_list_draft">"Brouillon : "</string>
@@ -259,6 +261,10 @@
259261
<string name="stream_compose_user_status_online">"En ligne"</string>
260262
<string name="stream_compose_video_preview">"Vidéo"</string>
261263
<string name="stream_compose_waiting_for_network">"En attente de réseau"</string>
264+
<plurals name="stream_compose_channel_item_unread">
265+
<item quantity="one">"%d message non lu"</item>
266+
<item quantity="other">"%d messages non lus"</item>
267+
</plurals>
262268
<plurals name="stream_compose_channel_list_typing_many">
263269
<item quantity="one">"%d personne écrit"</item>
264270
<item quantity="other">"%d personnes écrivent"</item>

stream-chat-android-compose/src/main/res/values-hi/strings.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
limitations under the License.
1616
-->
1717
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
18+
<plurals name="stream_compose_channel_item_unread">
19+
<item quantity="one">"%d अपठित संदेश"</item>
20+
<item quantity="other">"%d अपठित संदेश"</item>
21+
</plurals>
1822
<plurals name="stream_compose_channel_list_typing_many">
1923
<item quantity="one">"%d व्यक्ति टाइप कर रहा है"</item>
2024
<item quantity="other">"%d लोग टाइप कर रहे हैं"</item>
@@ -112,7 +116,9 @@
112116
<string name="stream_compose_avatar_overflow_count">"+%1$d"</string>
113117
<string name="stream_compose_block_user">"उपयोगकर्ता को ब्लॉक करें"</string>
114118
<string name="stream_compose_cancel">"रद्द करें"</string>
115-
<string name="stream_compose_cd_channel_item">"चैनल आइटम"</string>
119+
<string name="stream_compose_channel_item_muted">"म्यूट किया गया"</string>
120+
<string name="stream_compose_channel_item_open">"बातचीत खोलें"</string>
121+
<string name="stream_compose_channel_item_options">"बातचीत के विकल्प खोलें"</string>
116122
<string name="stream_compose_cd_message_item">"मैसेज आइटम"</string>
117123
<string name="stream_compose_cd_play_button">"चलाएँ बटन"</string>
118124
<string name="stream_compose_channel_list_draft">"ड्राफ़्ट: "</string>

stream-chat-android-compose/src/main/res/values-in/strings.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@
5656
<string name="stream_compose_avatar_overflow_count">"+%1$d"</string>
5757
<string name="stream_compose_block_user">"Blokir pengguna"</string>
5858
<string name="stream_compose_cancel">"Batal"</string>
59-
<string name="stream_compose_cd_channel_item">"Item saluran"</string>
59+
<string name="stream_compose_channel_item_muted">"dibisukan"</string>
60+
<string name="stream_compose_channel_item_open">"Buka percakapan"</string>
61+
<string name="stream_compose_channel_item_options">"Buka opsi percakapan"</string>
6062
<string name="stream_compose_cd_message_item">"Item pesan"</string>
6163
<string name="stream_compose_cd_play_button">"Tombol putar"</string>
6264
<string name="stream_compose_channel_list_draft">"Draf: "</string>
@@ -259,6 +261,10 @@
259261
<string name="stream_compose_user_status_online">"Online"</string>
260262
<string name="stream_compose_video_preview">"Video"</string>
261263
<string name="stream_compose_waiting_for_network">"Menunggu jaringan"</string>
264+
<plurals name="stream_compose_channel_item_unread">
265+
<item quantity="one">"%d pesan belum dibaca"</item>
266+
<item quantity="other">"%d pesan belum dibaca"</item>
267+
</plurals>
262268
<plurals name="stream_compose_channel_list_typing_many">
263269
<item quantity="other">"%d orang sedang mengetik"</item>
264270
</plurals>

stream-chat-android-compose/src/main/res/values-it/strings.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
limitations under the License.
1616
-->
1717
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
18+
<plurals name="stream_compose_channel_item_unread">
19+
<item quantity="one">"%d messaggio non letto"</item>
20+
<item quantity="other">"%d messaggi non letti"</item>
21+
</plurals>
1822
<plurals name="stream_compose_channel_list_typing_many">
1923
<item quantity="one">"%d persona sta scrivendo"</item>
2024
<item quantity="other">"%d persone stanno scrivendo"</item>
@@ -112,7 +116,9 @@
112116
<string name="stream_compose_avatar_overflow_count">"+%1$d"</string>
113117
<string name="stream_compose_block_user">"Blocca utente"</string>
114118
<string name="stream_compose_cancel">"Annulla"</string>
115-
<string name="stream_compose_cd_channel_item">"Elemento canale"</string>
119+
<string name="stream_compose_channel_item_muted">"silenziato"</string>
120+
<string name="stream_compose_channel_item_open">"Apri conversazione"</string>
121+
<string name="stream_compose_channel_item_options">"Apri opzioni conversazione"</string>
116122
<string name="stream_compose_cd_message_item">"Elemento messaggio"</string>
117123
<string name="stream_compose_cd_play_button">"Pulsante riproduci"</string>
118124
<string name="stream_compose_channel_list_draft">"Bozza: "</string>

0 commit comments

Comments
 (0)