Skip to content

Commit d6dac9a

Browse files
author
Marco Romano
authored
Make PollContentView a11y friendly #1345
Improves a bit how screen readers read polls in the timeline. - Adds a few `contentDescription` so that talkback reads “poll” or “ended poll” before the poll question. - Changes the compose structure of the answers so that they are properly scanned by the screen reader. This meant getting rid of the `IconToggleButton` which was made redundant by the use of the `selectable`.
2 parents 0bdb5d0 + 626ee7f commit d6dac9a

File tree

41 files changed

+144
-126
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+144
-126
lines changed

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

Lines changed: 38 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,21 @@ import androidx.compose.foundation.layout.Row
2121
import androidx.compose.foundation.layout.Spacer
2222
import androidx.compose.foundation.layout.fillMaxWidth
2323
import androidx.compose.foundation.layout.height
24+
import androidx.compose.foundation.layout.padding
2425
import androidx.compose.foundation.layout.size
2526
import androidx.compose.foundation.layout.width
26-
import androidx.compose.foundation.selection.selectable
2727
import androidx.compose.material.icons.Icons
2828
import androidx.compose.material.icons.filled.CheckCircle
2929
import androidx.compose.material.icons.filled.RadioButtonUnchecked
30-
import androidx.compose.material3.IconButtonDefaults
3130
import androidx.compose.runtime.Composable
3231
import androidx.compose.ui.Alignment
3332
import androidx.compose.ui.Modifier
3433
import androidx.compose.ui.graphics.StrokeCap
3534
import androidx.compose.ui.res.pluralStringResource
36-
import androidx.compose.ui.semantics.Role
37-
import androidx.compose.ui.tooling.preview.Preview
3835
import androidx.compose.ui.unit.dp
39-
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
36+
import io.element.android.libraries.designsystem.preview.DayNightPreviews
37+
import io.element.android.libraries.designsystem.preview.ElementPreview
4038
import io.element.android.libraries.designsystem.theme.components.Icon
41-
import io.element.android.libraries.designsystem.theme.components.IconToggleButton
4239
import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
4340
import io.element.android.libraries.designsystem.theme.components.Text
4441
import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor
@@ -47,41 +44,33 @@ import io.element.android.libraries.theme.ElementTheme
4744
import io.element.android.libraries.ui.strings.CommonPlurals
4845

4946
@Composable
50-
fun PollAnswerView(
47+
internal fun PollAnswerView(
5148
answerItem: PollAnswerItem,
52-
onClick: () -> Unit,
5349
modifier: Modifier = Modifier,
5450
) {
5551
Row(
56-
modifier
57-
.fillMaxWidth()
58-
.selectable(
59-
selected = answerItem.isSelected,
60-
enabled = answerItem.isEnabled,
61-
onClick = onClick,
62-
role = Role.RadioButton,
63-
)
52+
modifier = modifier.fillMaxWidth(),
6453
) {
65-
IconToggleButton(
66-
modifier = Modifier.size(22.dp),
67-
checked = answerItem.isSelected,
68-
enabled = answerItem.isEnabled,
69-
colors = IconButtonDefaults.iconToggleButtonColors(
70-
contentColor = ElementTheme.colors.iconSecondary,
71-
checkedContentColor = ElementTheme.colors.iconPrimary,
72-
disabledContentColor = ElementTheme.colors.iconDisabled,
73-
),
74-
onCheckedChange = { onClick() },
75-
) {
76-
Icon(
77-
imageVector = if (answerItem.isSelected) {
78-
Icons.Default.CheckCircle
54+
Icon(
55+
imageVector = if (answerItem.isSelected) {
56+
Icons.Default.CheckCircle
57+
} else {
58+
Icons.Default.RadioButtonUnchecked
59+
},
60+
contentDescription = null,
61+
modifier = Modifier
62+
.padding(0.5.dp)
63+
.size(22.dp),
64+
tint = if (answerItem.isEnabled) {
65+
if (answerItem.isSelected) {
66+
ElementTheme.colors.iconPrimary
7967
} else {
80-
Icons.Default.RadioButtonUnchecked
81-
},
82-
contentDescription = null,
83-
)
84-
}
68+
ElementTheme.colors.iconSecondary
69+
}
70+
} else {
71+
ElementTheme.colors.iconDisabled
72+
},
73+
)
8574
Spacer(modifier = Modifier.width(12.dp))
8675
Column {
8776
Row {
@@ -119,65 +108,58 @@ fun PollAnswerView(
119108
}
120109
}
121110

122-
@Preview
111+
@DayNightPreviews
123112
@Composable
124-
internal fun PollAnswerDisclosedNotSelectedPreview() = ElementThemedPreview {
113+
internal fun PollAnswerDisclosedNotSelectedPreview() = ElementPreview {
125114
PollAnswerView(
126115
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false),
127-
onClick = { },
128116
)
129117
}
130118

131-
@Preview
119+
@DayNightPreviews
132120
@Composable
133-
internal fun PollAnswerDisclosedSelectedPreview() = ElementThemedPreview {
121+
internal fun PollAnswerDisclosedSelectedPreview() = ElementPreview {
134122
PollAnswerView(
135123
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true),
136-
onClick = { }
137124
)
138125
}
139126

140-
@Preview
127+
@DayNightPreviews
141128
@Composable
142-
internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementThemedPreview {
129+
internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementPreview {
143130
PollAnswerView(
144131
answerItem = aPollAnswerItem(isDisclosed = false, isSelected = false),
145-
onClick = { },
146132
)
147133
}
148134

149-
@Preview
135+
@DayNightPreviews
150136
@Composable
151-
internal fun PollAnswerUndisclosedSelectedPreview() = ElementThemedPreview {
137+
internal fun PollAnswerUndisclosedSelectedPreview() = ElementPreview {
152138
PollAnswerView(
153139
answerItem = aPollAnswerItem(isDisclosed = false, isSelected = true),
154-
onClick = { }
155140
)
156141
}
157142

158-
@Preview
143+
@DayNightPreviews
159144
@Composable
160-
internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementThemedPreview {
145+
internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementPreview {
161146
PollAnswerView(
162147
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false, isEnabled = false, isWinner = true),
163-
onClick = { }
164148
)
165149
}
166150

167-
@Preview
151+
@DayNightPreviews
168152
@Composable
169-
internal fun PollAnswerEndedWinnerSelectedPreview() = ElementThemedPreview {
153+
internal fun PollAnswerEndedWinnerSelectedPreview() = ElementPreview {
170154
PollAnswerView(
171155
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = true),
172-
onClick = { }
173156
)
174157
}
175158

176-
@Preview
159+
@DayNightPreviews
177160
@Composable
178-
internal fun PollAnswerEndedSelectedPreview() = ElementThemedPreview {
161+
internal fun PollAnswerEndedSelectedPreview() = ElementPreview {
179162
PollAnswerView(
180163
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = false),
181-
onClick = { }
182164
)
183165
}

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

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@ import androidx.compose.foundation.layout.Row
2323
import androidx.compose.foundation.layout.fillMaxWidth
2424
import androidx.compose.foundation.layout.padding
2525
import androidx.compose.foundation.layout.size
26+
import androidx.compose.foundation.selection.selectable
2627
import androidx.compose.foundation.selection.selectableGroup
2728
import androidx.compose.runtime.Composable
29+
import androidx.compose.runtime.remember
2830
import androidx.compose.ui.Alignment
2931
import androidx.compose.ui.Modifier
3032
import androidx.compose.ui.res.stringResource
33+
import androidx.compose.ui.semantics.Role
3134
import androidx.compose.ui.unit.dp
3235
import io.element.android.libraries.designsystem.VectorIcons
3336
import io.element.android.libraries.designsystem.preview.DayNightPreviews
@@ -56,24 +59,24 @@ fun PollContentView(
5659
}
5760

5861
Column(
59-
modifier = modifier
60-
.selectableGroup()
61-
.fillMaxWidth(),
62+
modifier = modifier.fillMaxWidth(),
6263
verticalArrangement = Arrangement.spacedBy(16.dp),
6364
) {
6465
PollTitle(title = question, isPollEnded = isPollEnded)
6566

6667
PollAnswers(answerItems = answerItems, onAnswerSelected = ::onAnswerSelected)
6768

68-
when {
69-
isPollEnded || pollKind == PollKind.Disclosed -> DisclosedPollBottomNotice(answerItems)
70-
pollKind == PollKind.Undisclosed -> UndisclosedPollBottomNotice()
69+
if (isPollEnded || pollKind == PollKind.Disclosed) {
70+
val votesCount = remember(answerItems) { answerItems.sumOf { it.votesCount } }
71+
DisclosedPollBottomNotice(votesCount = votesCount)
72+
} else {
73+
UndisclosedPollBottomNotice()
7174
}
7275
}
7376
}
7477

7578
@Composable
76-
internal fun PollTitle(
79+
private fun PollTitle(
7780
title: String,
7881
isPollEnded: Boolean,
7982
modifier: Modifier = Modifier
@@ -85,13 +88,13 @@ internal fun PollTitle(
8588
if (isPollEnded) {
8689
Icon(
8790
resourceId = VectorIcons.PollEnd,
88-
contentDescription = null,
91+
contentDescription = stringResource(id = CommonStrings.a11y_poll_end),
8992
modifier = Modifier.size(22.dp)
9093
)
9194
} else {
9295
Icon(
9396
resourceId = VectorIcons.Poll,
94-
contentDescription = null,
97+
contentDescription = stringResource(id = CommonStrings.a11y_poll),
9598
modifier = Modifier.size(22.dp)
9699
)
97100
}
@@ -103,27 +106,35 @@ internal fun PollTitle(
103106
}
104107

105108
@Composable
106-
internal fun PollAnswers(
109+
private fun PollAnswers(
107110
answerItems: ImmutableList<PollAnswerItem>,
108111
onAnswerSelected: (PollAnswer) -> Unit,
109112
modifier: Modifier = Modifier,
110113
) {
111-
112-
answerItems.forEach { answerItem ->
113-
PollAnswerView(
114-
modifier = modifier,
115-
answerItem = answerItem,
116-
onClick = { onAnswerSelected(answerItem.answer) }
117-
)
114+
Column(
115+
modifier = modifier.selectableGroup(),
116+
verticalArrangement = Arrangement.spacedBy(16.dp),
117+
) {
118+
answerItems.forEach {
119+
PollAnswerView(
120+
answerItem = it,
121+
modifier = Modifier
122+
.selectable(
123+
selected = it.isSelected,
124+
enabled = it.isEnabled,
125+
onClick = { onAnswerSelected(it.answer) },
126+
role = Role.RadioButton,
127+
),
128+
)
129+
}
118130
}
119131
}
120132

121133
@Composable
122-
internal fun ColumnScope.DisclosedPollBottomNotice(
123-
answerItems: ImmutableList<PollAnswerItem>,
134+
private fun ColumnScope.DisclosedPollBottomNotice(
135+
votesCount: Int,
124136
modifier: Modifier = Modifier
125137
) {
126-
val votesCount = answerItems.sumOf { it.votesCount }
127138
Text(
128139
modifier = modifier.align(Alignment.End),
129140
style = ElementTheme.typography.fontBodyXsRegular,
@@ -133,7 +144,9 @@ internal fun ColumnScope.DisclosedPollBottomNotice(
133144
}
134145

135146
@Composable
136-
fun ColumnScope.UndisclosedPollBottomNotice(modifier: Modifier = Modifier) {
147+
private fun ColumnScope.UndisclosedPollBottomNotice(
148+
modifier: Modifier = Modifier
149+
) {
137150
Text(
138151
modifier = modifier
139152
.align(Alignment.Start)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
<string name="a11y_hide_password">"Hide password"</string>
44
<string name="a11y_notifications_mentions_only">"Mentions only"</string>
55
<string name="a11y_notifications_muted">"Muted"</string>
6+
<string name="a11y_poll">"Poll"</string>
7+
<string name="a11y_poll_end">"Ended poll"</string>
68
<string name="a11y_send_files">"Send files"</string>
79
<string name="a11y_show_password">"Show password"</string>
810
<string name="a11y_user_menu">"User menu"</string>
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)