Skip to content

Commit bcc03be

Browse files
authored
Improve poll dialogs TalkBack accessibility (#6466)
* Improve poll results TalkBack accessibility * Improve poll answers and more-options dialog TalkBack accessibility * Improve poll option votes dialog TalkBack accessibility * Extract shared poll option voting row * Remove redundant PollOptionItem wrapper After extracting PollOptionVotingRow, PollOptionItem became a pass-through that forwards twelve parameters with no transformation. Inline the call site to use PollOptionVotingRow directly and drop the wrapper along with its LongParameterList suppression.
1 parent 94eeb70 commit bcc03be

8 files changed

Lines changed: 288 additions & 273 deletions

File tree

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

Lines changed: 2 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -16,76 +16,55 @@
1616

1717
package io.getstream.chat.android.compose.ui.components.messages
1818

19-
import androidx.compose.animation.core.animateFloatAsState
2019
import androidx.compose.foundation.BorderStroke
2120
import androidx.compose.foundation.background
2221
import androidx.compose.foundation.layout.Arrangement
2322
import androidx.compose.foundation.layout.Box
2423
import androidx.compose.foundation.layout.Column
2524
import androidx.compose.foundation.layout.PaddingValues
26-
import androidx.compose.foundation.layout.Row
2725
import androidx.compose.foundation.layout.fillMaxWidth
28-
import androidx.compose.foundation.layout.height
29-
import androidx.compose.foundation.layout.heightIn
3026
import androidx.compose.foundation.layout.padding
3127
import androidx.compose.foundation.layout.size
32-
import androidx.compose.foundation.selection.toggleable
33-
import androidx.compose.foundation.shape.RoundedCornerShape
3428
import androidx.compose.material3.AlertDialog
3529
import androidx.compose.material3.ButtonDefaults
36-
import androidx.compose.material3.LinearProgressIndicator
3730
import androidx.compose.material3.Text
3831
import androidx.compose.material3.TextButton
3932
import androidx.compose.runtime.Composable
4033
import androidx.compose.runtime.LaunchedEffect
4134
import androidx.compose.runtime.MutableState
42-
import androidx.compose.runtime.getValue
4335
import androidx.compose.runtime.mutableStateOf
4436
import androidx.compose.runtime.remember
4537
import androidx.compose.ui.Alignment
4638
import androidx.compose.ui.Modifier
47-
import androidx.compose.ui.draw.clip
4839
import androidx.compose.ui.focus.FocusRequester
4940
import androidx.compose.ui.focus.focusRequester
50-
import androidx.compose.ui.graphics.StrokeCap
5141
import androidx.compose.ui.platform.LocalContext
5242
import androidx.compose.ui.res.pluralStringResource
5343
import androidx.compose.ui.res.stringResource
54-
import androidx.compose.ui.semantics.Role
55-
import androidx.compose.ui.semantics.clearAndSetSemantics
56-
import androidx.compose.ui.semantics.contentDescription
57-
import androidx.compose.ui.semantics.hideFromAccessibility
58-
import androidx.compose.ui.semantics.semantics
59-
import androidx.compose.ui.text.style.TextOverflow
6044
import androidx.compose.ui.tooling.preview.Preview
6145
import androidx.compose.ui.unit.dp
6246
import io.getstream.chat.android.compose.R
63-
import io.getstream.chat.android.compose.ui.components.avatar.AvatarSize
64-
import io.getstream.chat.android.compose.ui.components.avatar.UserAvatarStack
6547
import io.getstream.chat.android.compose.ui.components.button.StreamButtonSize
6648
import io.getstream.chat.android.compose.ui.components.button.StreamButtonStyle
6749
import io.getstream.chat.android.compose.ui.components.button.StreamButtonStyleDefaults
6850
import io.getstream.chat.android.compose.ui.components.button.StreamTextButton
69-
import io.getstream.chat.android.compose.ui.components.common.RadioCheck
7051
import io.getstream.chat.android.compose.ui.components.composer.InputField
7152
import io.getstream.chat.android.compose.ui.components.poll.AddAnswerDialog
53+
import io.getstream.chat.android.compose.ui.components.poll.PollOptionVotingRow
7254
import io.getstream.chat.android.compose.ui.theme.ChatTheme
7355
import io.getstream.chat.android.compose.ui.theme.MessageBubbleParams
7456
import io.getstream.chat.android.compose.ui.theme.MessageFailedIconParams
7557
import io.getstream.chat.android.compose.ui.theme.MessageStyling
7658
import io.getstream.chat.android.compose.ui.theme.MessageStyling.PollStyle
7759
import io.getstream.chat.android.compose.ui.theme.StreamTokens
78-
import io.getstream.chat.android.compose.ui.util.applyIf
7960
import io.getstream.chat.android.compose.ui.util.isErrorOrFailed
8061
import io.getstream.chat.android.compose.ui.util.passiveRipple
8162
import io.getstream.chat.android.compose.util.extensions.toSet
8263
import io.getstream.chat.android.models.ChannelCapabilities
8364
import io.getstream.chat.android.models.Message
8465
import io.getstream.chat.android.models.Option
8566
import io.getstream.chat.android.models.Poll
86-
import io.getstream.chat.android.models.User
8767
import io.getstream.chat.android.models.Vote
88-
import io.getstream.chat.android.models.VotingVisibility
8968
import io.getstream.chat.android.previewdata.PreviewMessageData
9069
import io.getstream.chat.android.ui.common.state.messages.list.MessageItemState
9170
import io.getstream.chat.android.ui.common.state.messages.poll.PollSelectionType
@@ -260,7 +239,7 @@ private fun PollMessageContent(
260239
)
261240
val voteCount = poll.voteCountsByOption[option.id] ?: 0
262241

263-
PollOptionItem(
242+
PollOptionVotingRow(
264243
modifier = Modifier.padding(padding),
265244
poll = poll,
266245
option = option,
@@ -418,115 +397,6 @@ private fun NewOptionDialog(
418397
)
419398
}
420399

421-
@Suppress("LongParameterList", "LongMethod")
422-
@Composable
423-
private fun PollOptionItem(
424-
modifier: Modifier = Modifier,
425-
poll: Poll,
426-
option: Option,
427-
voteCount: Int,
428-
totalVoteCount: Int,
429-
users: List<User>,
430-
checkedCount: Int,
431-
checked: Boolean,
432-
style: PollStyle,
433-
onCastVote: () -> Unit,
434-
onRemoveVote: () -> Unit,
435-
) {
436-
val typography = ChatTheme.typography
437-
val toggleRole = if (poll.maxVotesAllowed == 1) Role.RadioButton else Role.Checkbox
438-
val onToggle: (Boolean) -> Unit = { enabled ->
439-
val canVote = poll.maxVotesAllowed?.let { checkedCount < it } ?: true
440-
if (enabled && canVote && !checked) {
441-
onCastVote.invoke()
442-
} else if (!enabled) {
443-
onRemoveVote.invoke()
444-
}
445-
}
446-
447-
Row(
448-
modifier = modifier
449-
.fillMaxWidth()
450-
.applyIf(!poll.closed) {
451-
toggleable(
452-
value = checked,
453-
role = toggleRole,
454-
onValueChange = onToggle,
455-
)
456-
}
457-
.semantics(mergeDescendants = true) {},
458-
horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacingSm),
459-
verticalAlignment = Alignment.CenterVertically,
460-
) {
461-
if (!poll.closed) {
462-
RadioCheck(
463-
modifier = Modifier.semantics { hideFromAccessibility() },
464-
checked = checked,
465-
onCheckedChange = onToggle,
466-
borderColor = style.outlineColor,
467-
)
468-
}
469-
470-
Column(verticalArrangement = Arrangement.spacedBy(StreamTokens.spacing2xs)) {
471-
Row(Modifier.heightIn(min = AvatarSize.ExtraSmall)) {
472-
Text(
473-
modifier = Modifier.weight(1f),
474-
text = option.text,
475-
style = typography.captionDefault,
476-
color = style.textColor,
477-
maxLines = 2,
478-
overflow = TextOverflow.Ellipsis,
479-
)
480-
481-
if (users.isNotEmpty() && poll.votingVisibility != VotingVisibility.ANONYMOUS) {
482-
UserAvatarStack(
483-
overlap = StreamTokens.spacingXs,
484-
users = users.take(MaxStackedAvatars),
485-
avatarSize = AvatarSize.ExtraSmall,
486-
modifier = Modifier.padding(start = StreamTokens.spacingXs, end = StreamTokens.spacing2xs),
487-
)
488-
}
489-
490-
val voteCountDescription = pluralStringResource(
491-
R.plurals.stream_compose_poll_vote_counts,
492-
voteCount,
493-
voteCount,
494-
)
495-
Text(
496-
modifier = Modifier
497-
.align(Alignment.CenterVertically)
498-
.semantics { contentDescription = voteCountDescription },
499-
text = voteCount.toString(),
500-
style = typography.metadataDefault,
501-
color = style.textColor,
502-
)
503-
}
504-
505-
val progress by animateFloatAsState(
506-
targetValue = if (voteCount == 0 || totalVoteCount == 0) {
507-
0f
508-
} else {
509-
voteCount / totalVoteCount.toFloat()
510-
},
511-
)
512-
513-
LinearProgressIndicator(
514-
modifier = Modifier
515-
.fillMaxWidth()
516-
.height(8.dp)
517-
.clip(RoundedCornerShape(4.dp))
518-
.clearAndSetSemantics {},
519-
progress = { progress },
520-
color = style.progressColor,
521-
trackColor = style.trackColor,
522-
gapSize = 0.dp,
523-
strokeCap = StrokeCap.Square,
524-
drawStopIndicator = { /* Don't draw the stop indicator */ },
525-
)
526-
}
527-
}
528-
}
529-
530400
@Composable
531401
private fun EndPollConfirmationDialog(
532402
onConfirm: () -> Unit,
@@ -568,8 +438,6 @@ private fun EndPollConfirmationDialog(
568438
)
569439
}
570440

571-
private const val MaxStackedAvatars = 3
572-
573441
@Composable
574442
private fun PollOptionButton(
575443
text: String,

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

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import androidx.compose.ui.focus.FocusRequester
5656
import androidx.compose.ui.focus.focusRequester
5757
import androidx.compose.ui.res.painterResource
5858
import androidx.compose.ui.res.stringResource
59+
import androidx.compose.ui.semantics.semantics
5960
import androidx.compose.ui.tooling.preview.Preview
6061
import androidx.compose.ui.unit.dp
6162
import androidx.compose.ui.window.Popup
@@ -206,40 +207,45 @@ internal fun PollAnswersItem(
206207
contentPadding = PaddingValues(StreamTokens.spacingMd),
207208
verticalArrangement = Arrangement.spacedBy(StreamTokens.spacingXs),
208209
) {
209-
Text(
210-
text = answer.text,
211-
color = colors.textPrimary,
212-
style = typography.bodyDefault,
213-
)
214-
215-
Row(
216-
verticalAlignment = Alignment.CenterVertically,
217-
horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacingXs),
210+
Column(
211+
modifier = Modifier.semantics(mergeDescendants = true) {},
212+
verticalArrangement = Arrangement.spacedBy(StreamTokens.spacingXs),
218213
) {
219-
val user = answer.user?.takeIf { showAvatar }
220-
if (user != null) {
221-
ChatTheme.componentFactory.UserAvatar(
222-
params = UserAvatarParams(
223-
modifier = Modifier.size(AvatarSize.ExtraSmall),
224-
user = user,
225-
showIndicator = false,
226-
showBorder = false,
227-
),
228-
)
214+
Text(
215+
text = answer.text,
216+
color = colors.textPrimary,
217+
style = typography.bodyDefault,
218+
)
219+
220+
Row(
221+
verticalAlignment = Alignment.CenterVertically,
222+
horizontalArrangement = Arrangement.spacedBy(StreamTokens.spacingXs),
223+
) {
224+
val user = answer.user?.takeIf { showAvatar }
225+
if (user != null) {
226+
ChatTheme.componentFactory.UserAvatar(
227+
params = UserAvatarParams(
228+
modifier = Modifier.size(AvatarSize.ExtraSmall),
229+
user = user,
230+
showIndicator = false,
231+
showBorder = false,
232+
),
233+
)
234+
235+
Text(
236+
modifier = Modifier.weight(1f),
237+
text = user.name,
238+
color = colors.chatTextUsername,
239+
style = typography.captionDefault,
240+
)
241+
}
229242

230243
Text(
231-
modifier = Modifier.weight(1f),
232-
text = user.name,
233-
color = colors.chatTextUsername,
244+
text = ChatTheme.dateFormatter.formatDate(answer.createdAt),
245+
color = colors.textTertiary,
234246
style = typography.captionDefault,
235247
)
236248
}
237-
238-
Text(
239-
text = ChatTheme.dateFormatter.formatDate(answer.createdAt),
240-
color = colors.textTertiary,
241-
style = typography.captionDefault,
242-
)
243249
}
244250

245251
if (showUpdateButton) {

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ import androidx.compose.foundation.layout.fillMaxWidth
2323
import androidx.compose.foundation.layout.padding
2424
import androidx.compose.material3.Text
2525
import androidx.compose.runtime.Composable
26+
import androidx.compose.runtime.LaunchedEffect
2627
import androidx.compose.ui.Alignment
2728
import androidx.compose.ui.Modifier
29+
import androidx.compose.ui.platform.LocalView
2830
import androidx.compose.ui.res.painterResource
2931
import androidx.compose.ui.res.stringResource
32+
import androidx.compose.ui.semantics.heading
33+
import androidx.compose.ui.semantics.semantics
3034
import androidx.compose.ui.text.style.TextOverflow
3135
import androidx.compose.ui.tooling.preview.Preview
3236
import io.getstream.chat.android.compose.R
@@ -40,6 +44,9 @@ public fun PollDialogHeader(
4044
onBackPressed: () -> Unit,
4145
trailingContent: @Composable () -> Unit = {},
4246
) {
47+
val view = LocalView.current
48+
LaunchedEffect(title) { view.announceForAccessibility(title) }
49+
4350
Row(
4451
modifier = Modifier
4552
.fillMaxWidth()
@@ -52,7 +59,9 @@ public fun PollDialogHeader(
5259
)
5360

5461
Text(
55-
modifier = Modifier.weight(1f),
62+
modifier = Modifier
63+
.weight(1f)
64+
.semantics { heading() },
5665
text = title,
5766
style = ChatTheme.typography.headingMedium,
5867
maxLines = 1,

0 commit comments

Comments
 (0)