diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index 30cb8c5006..c537c89b6d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Brush import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.isTraversalGroup import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -31,6 +32,7 @@ import io.element.android.features.messages.impl.timeline.components.layout.Cont import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent @@ -137,6 +139,8 @@ internal fun TimelineItemRow( } else { timelineItem.safeSenderName } + // For Polls, allow the answers to be traversed by Talkback + isTraversalGroup = timelineItem.content is TimelineItemPollContent } // Custom clickable that applies over the whole item for accessibility .then( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollViewTest.kt index b89a3830be..3935d6d615 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollViewTest.kt @@ -47,7 +47,10 @@ class TimelineItemPollViewTest { ) } val answer = content.answerItems[answerIndex].answer - rule.onNode(hasText(answer.text)).performClick() + rule.onNode( + matcher = hasText(answer.text), + useUnmergedTree = true, + ).performClick() eventsRecorder.assertSingle(TimelineEvents.SelectPollAnswer(content.eventId!!, answer.id)) } diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerView.kt index 6243bd38d9..59d13fed1e 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerView.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerView.kt @@ -20,9 +20,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.poll.api.R import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon @@ -32,14 +36,42 @@ import io.element.android.libraries.designsystem.theme.progressIndicatorTrackCol import io.element.android.libraries.designsystem.toEnabledColor import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.ui.strings.CommonPlurals +import io.element.android.libraries.ui.strings.CommonStrings @Composable internal fun PollAnswerView( answerItem: PollAnswerItem, modifier: Modifier = Modifier, ) { + val nbVotesText = pluralStringResource( + id = CommonPlurals.common_poll_votes_count, + count = answerItem.votesCount, + answerItem.votesCount, + ) + val a11yText = buildString { + val sentenceDelimiter = stringResource(CommonStrings.common_sentence_delimiter) + append(answerItem.answer.text.removeSuffix(".")) + if (answerItem.showVotes) { + append(sentenceDelimiter) + append(nbVotesText) + if (answerItem.votesCount != 0) { + append(sentenceDelimiter) + (answerItem.percentage * 100).toInt().let { percent -> + append(pluralStringResource(R.plurals.a11y_polls_percent_of_total, percent, percent)) + } + } + if (answerItem.isWinner) { + append(sentenceDelimiter) + append(stringResource(R.string.a11y_polls_winning_answer)) + } + } + } Row( - modifier = modifier.fillMaxWidth(), + modifier = modifier + .fillMaxWidth() + .clearAndSetSemantics { + contentDescription = a11yText + }, ) { Icon( imageVector = if (answerItem.isSelected) { @@ -70,11 +102,6 @@ internal fun PollAnswerView( style = if (answerItem.isWinner) ElementTheme.typography.fontBodyLgMedium else ElementTheme.typography.fontBodyLgRegular, ) if (answerItem.showVotes) { - val text = pluralStringResource( - id = CommonPlurals.common_poll_votes_count, - count = answerItem.votesCount, - answerItem.votesCount - ) Row( modifier = Modifier.align(Alignment.Bottom), verticalAlignment = Alignment.CenterVertically, @@ -87,13 +114,13 @@ internal fun PollAnswerView( ) Spacer(modifier = Modifier.width(2.dp)) Text( - text = text, + text = nbVotesText, style = ElementTheme.typography.fontBodySmMedium, color = ElementTheme.colors.textPrimary, ) } else { Text( - text = text, + text = nbVotesText, style = ElementTheme.typography.fontBodySmRegular, color = ElementTheme.colors.textSecondary, ) diff --git a/features/poll/api/src/main/res/values/localazy.xml b/features/poll/api/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..2d1142194c --- /dev/null +++ b/features/poll/api/src/main/res/values/localazy.xml @@ -0,0 +1,8 @@ + + + + "%1$d percent of total votes" + "%1$d percents of total votes" + + "This is the winning answer" + diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt index f8384279bf..038fb397f3 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt @@ -143,7 +143,7 @@ fun CreatePollView( trailingContent = ListItemContent.Custom { Icon( imageVector = CompoundIcons.Delete(), - contentDescription = null, + contentDescription = stringResource(R.string.screen_create_poll_delete_option_a11y, answer.text), modifier = Modifier.clickable(answer.canDelete) { state.eventSink(CreatePollEvents.RemoveAnswer(index)) }, diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryView.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryView.kt index d9d639e8a8..1101276577 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryView.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryView.kt @@ -29,6 +29,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -178,7 +180,9 @@ private fun PollHistoryList( if (pollHistoryItems.isEmpty()) { item { Column( - modifier = Modifier.fillParentMaxSize().padding(bottom = 24.dp), + modifier = Modifier + .fillParentMaxSize() + .padding(bottom = 24.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -191,7 +195,9 @@ private fun PollHistoryList( text = emptyStringResource, style = ElementTheme.typography.fontBodyLgRegular, color = ElementTheme.colors.textSecondary, - modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp, horizontal = 16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 24.dp, horizontal = 16.dp), textAlign = TextAlign.Center, ) @@ -227,7 +233,10 @@ private fun PollHistoryItemRow( modifier: Modifier = Modifier, ) { Surface( - modifier = modifier, + modifier = modifier.semantics(mergeDescendants = true) { + // Allow the answers to be traversed by Talkback + isTraversalGroup = true + }, border = BorderStroke(1.dp, ElementTheme.colors.borderInteractiveSecondary), shape = RoundedCornerShape(size = 12.dp) ) { diff --git a/features/poll/impl/src/main/res/values/localazy.xml b/features/poll/impl/src/main/res/values/localazy.xml index 7a7a15ea3c..6019d908b4 100644 --- a/features/poll/impl/src/main/res/values/localazy.xml +++ b/features/poll/impl/src/main/res/values/localazy.xml @@ -5,6 +5,7 @@ "Hide votes" "Option %1$d" "Your changes have not been saved. Are you sure you want to go back?" + "Delete option %1$s" "Question or topic" "What is the poll about?" "Create Poll" diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryViewTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryViewTest.kt index 3da8d50d49..54df066bbe 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryViewTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryViewTest.kt @@ -131,7 +131,10 @@ class PollHistoryViewTest { rule.setPollHistoryViewView( state = state, ) - rule.onNodeWithText(answer.text).performClick() + rule.onNodeWithText( + text = answer.text, + useUnmergedTree = true, + ).performClick() eventsRecorder.assertSingle( PollHistoryEvents.SelectPollAnswer(eventId, answer.id) ) diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index d2cbee9a19..21004580f1 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -259,6 +259,7 @@ Reason: %1$s." "Sending…" "Sending failed" "Sent" + ". " "Server not supported" "Server URL" "Settings" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index b299c39ef7..5c70f3fe50 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -242,6 +242,12 @@ "screen_polls_history_.*" ] }, + { + "name" : ":features:poll:api", + "includeRegex" : [ + "a11y\\.polls\\..*" + ] + }, { "name" : ":features:securebackup:impl", "includeRegex" : [