Skip to content

Commit 0b72bdc

Browse files
authored
Merge pull request #4875 from element-hq/feature/bma/a11yPolls
[a11y] Improve screen reader on polls
2 parents bb51cab + 2cab05e commit 0b72bdc

File tree

10 files changed

+76
-14
lines changed

10 files changed

+76
-14
lines changed

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import androidx.compose.ui.geometry.Size
2020
import androidx.compose.ui.graphics.Brush
2121
import androidx.compose.ui.res.stringResource
2222
import androidx.compose.ui.semantics.contentDescription
23+
import androidx.compose.ui.semantics.isTraversalGroup
2324
import androidx.compose.ui.semantics.semantics
2425
import androidx.compose.ui.unit.Dp
2526
import androidx.compose.ui.unit.dp
@@ -31,6 +32,7 @@ import io.element.android.features.messages.impl.timeline.components.layout.Cont
3132
import io.element.android.features.messages.impl.timeline.model.TimelineItem
3233
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
3334
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
35+
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
3436
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
3537
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
3638
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
@@ -137,6 +139,8 @@ internal fun TimelineItemRow(
137139
} else {
138140
timelineItem.safeSenderName
139141
}
142+
// For Polls, allow the answers to be traversed by Talkback
143+
isTraversalGroup = timelineItem.content is TimelineItemPollContent
140144
}
141145
// Custom clickable that applies over the whole item for accessibility
142146
.then(

features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollViewTest.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ class TimelineItemPollViewTest {
4747
)
4848
}
4949
val answer = content.answerItems[answerIndex].answer
50-
rule.onNode(hasText(answer.text)).performClick()
50+
rule.onNode(
51+
matcher = hasText(answer.text),
52+
useUnmergedTree = true,
53+
).performClick()
5154
eventsRecorder.assertSingle(TimelineEvents.SelectPollAnswer(content.eventId!!, answer.id))
5255
}
5356

features/poll/api/src/main/kotlin/io/element/android/features/poll/api/pollcontent/PollAnswerView.kt

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@ import androidx.compose.ui.Alignment
2020
import androidx.compose.ui.Modifier
2121
import androidx.compose.ui.graphics.StrokeCap
2222
import androidx.compose.ui.res.pluralStringResource
23+
import androidx.compose.ui.res.stringResource
24+
import androidx.compose.ui.semantics.clearAndSetSemantics
25+
import androidx.compose.ui.semantics.contentDescription
2326
import androidx.compose.ui.unit.dp
2427
import io.element.android.compound.theme.ElementTheme
2528
import io.element.android.compound.tokens.generated.CompoundIcons
29+
import io.element.android.features.poll.api.R
2630
import io.element.android.libraries.designsystem.preview.ElementPreview
2731
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
2832
import io.element.android.libraries.designsystem.theme.components.Icon
@@ -32,14 +36,42 @@ import io.element.android.libraries.designsystem.theme.progressIndicatorTrackCol
3236
import io.element.android.libraries.designsystem.toEnabledColor
3337
import io.element.android.libraries.designsystem.utils.CommonDrawables
3438
import io.element.android.libraries.ui.strings.CommonPlurals
39+
import io.element.android.libraries.ui.strings.CommonStrings
3540

3641
@Composable
3742
internal fun PollAnswerView(
3843
answerItem: PollAnswerItem,
3944
modifier: Modifier = Modifier,
4045
) {
46+
val nbVotesText = pluralStringResource(
47+
id = CommonPlurals.common_poll_votes_count,
48+
count = answerItem.votesCount,
49+
answerItem.votesCount,
50+
)
51+
val a11yText = buildString {
52+
val sentenceDelimiter = stringResource(CommonStrings.common_sentence_delimiter)
53+
append(answerItem.answer.text.removeSuffix("."))
54+
if (answerItem.showVotes) {
55+
append(sentenceDelimiter)
56+
append(nbVotesText)
57+
if (answerItem.votesCount != 0) {
58+
append(sentenceDelimiter)
59+
(answerItem.percentage * 100).toInt().let { percent ->
60+
append(pluralStringResource(R.plurals.a11y_polls_percent_of_total, percent, percent))
61+
}
62+
}
63+
if (answerItem.isWinner) {
64+
append(sentenceDelimiter)
65+
append(stringResource(R.string.a11y_polls_winning_answer))
66+
}
67+
}
68+
}
4169
Row(
42-
modifier = modifier.fillMaxWidth(),
70+
modifier = modifier
71+
.fillMaxWidth()
72+
.clearAndSetSemantics {
73+
contentDescription = a11yText
74+
},
4375
) {
4476
Icon(
4577
imageVector = if (answerItem.isSelected) {
@@ -70,11 +102,6 @@ internal fun PollAnswerView(
70102
style = if (answerItem.isWinner) ElementTheme.typography.fontBodyLgMedium else ElementTheme.typography.fontBodyLgRegular,
71103
)
72104
if (answerItem.showVotes) {
73-
val text = pluralStringResource(
74-
id = CommonPlurals.common_poll_votes_count,
75-
count = answerItem.votesCount,
76-
answerItem.votesCount
77-
)
78105
Row(
79106
modifier = Modifier.align(Alignment.Bottom),
80107
verticalAlignment = Alignment.CenterVertically,
@@ -87,13 +114,13 @@ internal fun PollAnswerView(
87114
)
88115
Spacer(modifier = Modifier.width(2.dp))
89116
Text(
90-
text = text,
117+
text = nbVotesText,
91118
style = ElementTheme.typography.fontBodySmMedium,
92119
color = ElementTheme.colors.textPrimary,
93120
)
94121
} else {
95122
Text(
96-
text = text,
123+
text = nbVotesText,
97124
style = ElementTheme.typography.fontBodySmRegular,
98125
color = ElementTheme.colors.textSecondary,
99126
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
3+
<plurals name="a11y_polls_percent_of_total">
4+
<item quantity="one">"%1$d percent of total votes"</item>
5+
<item quantity="other">"%1$d percents of total votes"</item>
6+
</plurals>
7+
<string name="a11y_polls_winning_answer">"This is the winning answer"</string>
8+
</resources>

features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ fun CreatePollView(
143143
trailingContent = ListItemContent.Custom {
144144
Icon(
145145
imageVector = CompoundIcons.Delete(),
146-
contentDescription = null,
146+
contentDescription = stringResource(R.string.screen_create_poll_delete_option_a11y, answer.text),
147147
modifier = Modifier.clickable(answer.canDelete) {
148148
state.eventSink(CreatePollEvents.RemoveAnswer(index))
149149
},

features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryView.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import androidx.compose.runtime.LaunchedEffect
2929
import androidx.compose.ui.Alignment
3030
import androidx.compose.ui.Modifier
3131
import androidx.compose.ui.res.stringResource
32+
import androidx.compose.ui.semantics.isTraversalGroup
33+
import androidx.compose.ui.semantics.semantics
3234
import androidx.compose.ui.text.style.TextAlign
3335
import androidx.compose.ui.tooling.preview.PreviewParameter
3436
import androidx.compose.ui.unit.dp
@@ -178,7 +180,9 @@ private fun PollHistoryList(
178180
if (pollHistoryItems.isEmpty()) {
179181
item {
180182
Column(
181-
modifier = Modifier.fillParentMaxSize().padding(bottom = 24.dp),
183+
modifier = Modifier
184+
.fillParentMaxSize()
185+
.padding(bottom = 24.dp),
182186
verticalArrangement = Arrangement.Center,
183187
horizontalAlignment = Alignment.CenterHorizontally,
184188
) {
@@ -191,7 +195,9 @@ private fun PollHistoryList(
191195
text = emptyStringResource,
192196
style = ElementTheme.typography.fontBodyLgRegular,
193197
color = ElementTheme.colors.textSecondary,
194-
modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp, horizontal = 16.dp),
198+
modifier = Modifier
199+
.fillMaxWidth()
200+
.padding(vertical = 24.dp, horizontal = 16.dp),
195201
textAlign = TextAlign.Center,
196202
)
197203

@@ -227,7 +233,10 @@ private fun PollHistoryItemRow(
227233
modifier: Modifier = Modifier,
228234
) {
229235
Surface(
230-
modifier = modifier,
236+
modifier = modifier.semantics(mergeDescendants = true) {
237+
// Allow the answers to be traversed by Talkback
238+
isTraversalGroup = true
239+
},
231240
border = BorderStroke(1.dp, ElementTheme.colors.borderInteractiveSecondary),
232241
shape = RoundedCornerShape(size = 12.dp)
233242
) {

features/poll/impl/src/main/res/values/localazy.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<string name="screen_create_poll_anonymous_headline">"Hide votes"</string>
66
<string name="screen_create_poll_answer_hint">"Option %1$d"</string>
77
<string name="screen_create_poll_cancel_confirmation_content_android">"Your changes have not been saved. Are you sure you want to go back?"</string>
8+
<string name="screen_create_poll_delete_option_a11y">"Delete option %1$s"</string>
89
<string name="screen_create_poll_question_desc">"Question or topic"</string>
910
<string name="screen_create_poll_question_hint">"What is the poll about?"</string>
1011
<string name="screen_create_poll_title">"Create Poll"</string>

features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryViewTest.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,10 @@ class PollHistoryViewTest {
131131
rule.setPollHistoryViewView(
132132
state = state,
133133
)
134-
rule.onNodeWithText(answer.text).performClick()
134+
rule.onNodeWithText(
135+
text = answer.text,
136+
useUnmergedTree = true,
137+
).performClick()
135138
eventsRecorder.assertSingle(
136139
PollHistoryEvents.SelectPollAnswer(eventId, answer.id)
137140
)

libraries/ui-strings/src/main/res/values/localazy.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ Reason: %1$s."</string>
259259
<string name="common_sending">"Sending…"</string>
260260
<string name="common_sending_failed">"Sending failed"</string>
261261
<string name="common_sent">"Sent"</string>
262+
<string name="common_sentence_delimiter">". "</string>
262263
<string name="common_server_not_supported">"Server not supported"</string>
263264
<string name="common_server_url">"Server URL"</string>
264265
<string name="common_settings">"Settings"</string>

tools/localazy/config.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,12 @@
242242
"screen_polls_history_.*"
243243
]
244244
},
245+
{
246+
"name" : ":features:poll:api",
247+
"includeRegex" : [
248+
"a11y\\.polls\\..*"
249+
]
250+
},
245251
{
246252
"name" : ":features:securebackup:impl",
247253
"includeRegex" : [

0 commit comments

Comments
 (0)