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`.
This commit is contained in:
Marco Romano 2023-09-15 20:24:33 +02:00 committed by GitHub
commit 8f519b614f
41 changed files with 144 additions and 126 deletions

View file

@ -21,24 +21,21 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.RadioButtonUnchecked import androidx.compose.material.icons.filled.RadioButtonUnchecked
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconToggleButton
import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor
@ -47,41 +44,33 @@ import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonPlurals import io.element.android.libraries.ui.strings.CommonPlurals
@Composable @Composable
fun PollAnswerView( internal fun PollAnswerView(
answerItem: PollAnswerItem, answerItem: PollAnswerItem,
onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Row( Row(
modifier modifier = modifier.fillMaxWidth(),
.fillMaxWidth()
.selectable(
selected = answerItem.isSelected,
enabled = answerItem.isEnabled,
onClick = onClick,
role = Role.RadioButton,
)
) { ) {
IconToggleButton( Icon(
modifier = Modifier.size(22.dp), imageVector = if (answerItem.isSelected) {
checked = answerItem.isSelected, Icons.Default.CheckCircle
enabled = answerItem.isEnabled, } else {
colors = IconButtonDefaults.iconToggleButtonColors( Icons.Default.RadioButtonUnchecked
contentColor = ElementTheme.colors.iconSecondary, },
checkedContentColor = ElementTheme.colors.iconPrimary, contentDescription = null,
disabledContentColor = ElementTheme.colors.iconDisabled, modifier = Modifier
), .padding(0.5.dp)
onCheckedChange = { onClick() }, .size(22.dp),
) { tint = if (answerItem.isEnabled) {
Icon( if (answerItem.isSelected) {
imageVector = if (answerItem.isSelected) { ElementTheme.colors.iconPrimary
Icons.Default.CheckCircle
} else { } else {
Icons.Default.RadioButtonUnchecked ElementTheme.colors.iconSecondary
}, }
contentDescription = null, } else {
) ElementTheme.colors.iconDisabled
} },
)
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
Column { Column {
Row { Row {
@ -119,65 +108,58 @@ fun PollAnswerView(
} }
} }
@Preview @DayNightPreviews
@Composable @Composable
internal fun PollAnswerDisclosedNotSelectedPreview() = ElementThemedPreview { internal fun PollAnswerDisclosedNotSelectedPreview() = ElementPreview {
PollAnswerView( PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false), answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false),
onClick = { },
) )
} }
@Preview @DayNightPreviews
@Composable @Composable
internal fun PollAnswerDisclosedSelectedPreview() = ElementThemedPreview { internal fun PollAnswerDisclosedSelectedPreview() = ElementPreview {
PollAnswerView( PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true), answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true),
onClick = { }
) )
} }
@Preview @DayNightPreviews
@Composable @Composable
internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementThemedPreview { internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementPreview {
PollAnswerView( PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = false, isSelected = false), answerItem = aPollAnswerItem(isDisclosed = false, isSelected = false),
onClick = { },
) )
} }
@Preview @DayNightPreviews
@Composable @Composable
internal fun PollAnswerUndisclosedSelectedPreview() = ElementThemedPreview { internal fun PollAnswerUndisclosedSelectedPreview() = ElementPreview {
PollAnswerView( PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = false, isSelected = true), answerItem = aPollAnswerItem(isDisclosed = false, isSelected = true),
onClick = { }
) )
} }
@Preview @DayNightPreviews
@Composable @Composable
internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementThemedPreview { internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementPreview {
PollAnswerView( PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false, isEnabled = false, isWinner = true), answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false, isEnabled = false, isWinner = true),
onClick = { }
) )
} }
@Preview @DayNightPreviews
@Composable @Composable
internal fun PollAnswerEndedWinnerSelectedPreview() = ElementThemedPreview { internal fun PollAnswerEndedWinnerSelectedPreview() = ElementPreview {
PollAnswerView( PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = true), answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = true),
onClick = { }
) )
} }
@Preview @DayNightPreviews
@Composable @Composable
internal fun PollAnswerEndedSelectedPreview() = ElementThemedPreview { internal fun PollAnswerEndedSelectedPreview() = ElementPreview {
PollAnswerView( PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = false), answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = false),
onClick = { }
) )
} }

View file

@ -23,11 +23,14 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.VectorIcons import io.element.android.libraries.designsystem.VectorIcons
import io.element.android.libraries.designsystem.preview.DayNightPreviews import io.element.android.libraries.designsystem.preview.DayNightPreviews
@ -56,24 +59,24 @@ fun PollContentView(
} }
Column( Column(
modifier = modifier modifier = modifier.fillMaxWidth(),
.selectableGroup()
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
PollTitle(title = question, isPollEnded = isPollEnded) PollTitle(title = question, isPollEnded = isPollEnded)
PollAnswers(answerItems = answerItems, onAnswerSelected = ::onAnswerSelected) PollAnswers(answerItems = answerItems, onAnswerSelected = ::onAnswerSelected)
when { if (isPollEnded || pollKind == PollKind.Disclosed) {
isPollEnded || pollKind == PollKind.Disclosed -> DisclosedPollBottomNotice(answerItems) val votesCount = remember(answerItems) { answerItems.sumOf { it.votesCount } }
pollKind == PollKind.Undisclosed -> UndisclosedPollBottomNotice() DisclosedPollBottomNotice(votesCount = votesCount)
} else {
UndisclosedPollBottomNotice()
} }
} }
} }
@Composable @Composable
internal fun PollTitle( private fun PollTitle(
title: String, title: String,
isPollEnded: Boolean, isPollEnded: Boolean,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@ -85,13 +88,13 @@ internal fun PollTitle(
if (isPollEnded) { if (isPollEnded) {
Icon( Icon(
resourceId = VectorIcons.PollEnd, resourceId = VectorIcons.PollEnd,
contentDescription = null, contentDescription = stringResource(id = CommonStrings.a11y_poll_end),
modifier = Modifier.size(22.dp) modifier = Modifier.size(22.dp)
) )
} else { } else {
Icon( Icon(
resourceId = VectorIcons.Poll, resourceId = VectorIcons.Poll,
contentDescription = null, contentDescription = stringResource(id = CommonStrings.a11y_poll),
modifier = Modifier.size(22.dp) modifier = Modifier.size(22.dp)
) )
} }
@ -103,27 +106,35 @@ internal fun PollTitle(
} }
@Composable @Composable
internal fun PollAnswers( private fun PollAnswers(
answerItems: ImmutableList<PollAnswerItem>, answerItems: ImmutableList<PollAnswerItem>,
onAnswerSelected: (PollAnswer) -> Unit, onAnswerSelected: (PollAnswer) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column(
answerItems.forEach { answerItem -> modifier = modifier.selectableGroup(),
PollAnswerView( verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier, ) {
answerItem = answerItem, answerItems.forEach {
onClick = { onAnswerSelected(answerItem.answer) } PollAnswerView(
) answerItem = it,
modifier = Modifier
.selectable(
selected = it.isSelected,
enabled = it.isEnabled,
onClick = { onAnswerSelected(it.answer) },
role = Role.RadioButton,
),
)
}
} }
} }
@Composable @Composable
internal fun ColumnScope.DisclosedPollBottomNotice( private fun ColumnScope.DisclosedPollBottomNotice(
answerItems: ImmutableList<PollAnswerItem>, votesCount: Int,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val votesCount = answerItems.sumOf { it.votesCount }
Text( Text(
modifier = modifier.align(Alignment.End), modifier = modifier.align(Alignment.End),
style = ElementTheme.typography.fontBodyXsRegular, style = ElementTheme.typography.fontBodyXsRegular,
@ -133,7 +144,9 @@ internal fun ColumnScope.DisclosedPollBottomNotice(
} }
@Composable @Composable
fun ColumnScope.UndisclosedPollBottomNotice(modifier: Modifier = Modifier) { private fun ColumnScope.UndisclosedPollBottomNotice(
modifier: Modifier = Modifier
) {
Text( Text(
modifier = modifier modifier = modifier
.align(Alignment.Start) .align(Alignment.Start)

View file

@ -3,6 +3,8 @@
<string name="a11y_hide_password">"Hide password"</string> <string name="a11y_hide_password">"Hide password"</string>
<string name="a11y_notifications_mentions_only">"Mentions only"</string> <string name="a11y_notifications_mentions_only">"Mentions only"</string>
<string name="a11y_notifications_muted">"Muted"</string> <string name="a11y_notifications_muted">"Muted"</string>
<string name="a11y_poll">"Poll"</string>
<string name="a11y_poll_end">"Ended poll"</string>
<string name="a11y_send_files">"Send files"</string> <string name="a11y_send_files">"Send files"</string>
<string name="a11y_show_password">"Show password"</string> <string name="a11y_show_password">"Show password"</string>
<string name="a11y_user_menu">"User menu"</string> <string name="a11y_user_menu">"User menu"</string>

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:801f832469346524fdce0b5ad8654c3405daf0f21ae0601c62dc7f148b576ce8 oid sha256:188c362ebd8bc32a47b66a080331db7643cd97714f2d4e952d7bec8c11520dcd
size 49074 size 49026

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:944eb2ef8abf2e8f1715d063020767940c80b40690aae1129f69d02ddafac9d0 oid sha256:9de6ab591cb02f6545218a2606d031f4f93c82e71e99523eacaf77ffa78fadb1
size 51029 size 50940

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:adf6f3f79b9d8f62172171dd8a172bff1958bc4698df4586bf83c73fe4c6c6f3 oid sha256:b4b00894025844927932e790a1738c85cfbb61e61a81dfbbbd7e342f38f40b99
size 46198 size 46061

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:38183d1d69e36c2570259c987be079d09ecfc0a0cba7508fc43d33e95c574121 oid sha256:bd88ed3aeb9a20f148e914c4a5d4554220a1fecd8e8a8fe87400de78ff4bf248
size 48368 size 48237

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:54a4fe8e7f968bf487168a517baa0d9c9f6fc5b2354f4eff45f56de77b043f8f oid sha256:5490f2501c6ef257f926fc2f4bd9d94ef4e6e3017d4da290e2199eaeaa2ac5b5
size 56535 size 56571

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:045a09f485856b01e1084a91fefca0ccd7f9e7ef7470a22a4db0a60e95fb8667
size 23119

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:31a8f8e06cc101bcdccb973f3a003238cc093342f04613975cb9512072b051c2
size 21619

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9f97a2f591619aa2dd914f79a2bdfabea3e4e8239e6d80f29400e2c02c6ae21d
size 39250

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1abb2b1c635b5a6666ce7de0ffbe10c753d32c6f0f2e95e8cf17d481bfa5228b
size 23024

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:609279d1f2e25b86b8ae5a598bb6602a43a6714fa8e98a4625c349470d9d7e85
size 21345

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:03b4bfab1cbbbedd9219423834f3bdf47f40e20c0da791be2c892cb2af86cf7d
size 38694

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08550f84b603d0c0424d63e7aef09856a048b7a862288ac43911673019ae7a5d
size 23075

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7e6000321200788598744d82d8b11c9f2d6eb509bf8c9c0bf362c10d1b27b9ae
size 21474

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1672ff3a807b387b1f780920111c19484261b36884f1f68d72e108c90a4104d2
size 38948

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:013bfb3d3bd78d8db9f0ac51b11a41c2b82daaa28f87cdfd866260d3fb40bf0d
size 23125

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7c039ab6649ff160b540256dacba1037b15c5907d9994a041cbab3be0ee5cacd
size 21283

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:43da602c904210df0112a1af831cd2a2f7a7c5109d26605efad464a3c5b58995
size 38864

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a99ff857afe6478e579c96b984e1abdbf981638e6964a7d2c94b53153aff078d
size 23135

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d97ba3288f5fcdaaed917a994df9273d3e82d5a6e0c7978044dbaf03152e3269
size 21270

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bf806b2490fc8a1df278bde94535cb1b5d38f66531df85d6fd1251e6671d9f56
size 38756

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:05af414578ab61e5b24b6e0a4895e0b52c3566625fd47f00b32f38b416dbe7fa
size 21498

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9ccde06f5825c37c1c58ef0d9aee44f7b65425988386a97ba744360d8c22e470
size 19563

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eeaa924482fa2fc29079f7f0d4bddacb96567a2133808bed8b49be2ad7afe80f
size 36481

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ab9d7637c2c64d39beaa96244e348f39fc9b42fcebf54016fd27b4fd8e0a8c69
size 21352

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:16ea9da926c330f56bde968e90f8df1bc015bf8d55853edc21edfaf3f793adfe
size 19266

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:de340de9f0f07a44c9d2c1af196abc059d32f62fc857214cdf2c1d1e1951d267
size 35915

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:801f832469346524fdce0b5ad8654c3405daf0f21ae0601c62dc7f148b576ce8
size 49074

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:188c362ebd8bc32a47b66a080331db7643cd97714f2d4e952d7bec8c11520dcd
size 49026

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:adf6f3f79b9d8f62172171dd8a172bff1958bc4698df4586bf83c73fe4c6c6f3
size 46198

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b4b00894025844927932e790a1738c85cfbb61e61a81dfbbbd7e342f38f40b99
size 46061

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7d0674f2bac8e4912bb1faec069be0e86e3b796f61e6217494cfe42cce89dca2
size 49143

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:88230f28f7761d8f76bc5a50dbc4ba08a694eaf0d64207257f5196c741fb7b52
size 49078

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f54d112dc90c4e2402eeab5fe8e649ffc397e208c395a19f37889038206c8179
size 45927

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a6e31f773b884608499081ab3c7a27297edfbf8af76f502d9076b4753956e2db
size 45929

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4fe5ffa68f8ea13ae4343a4fd915c58c1ff9157433248ac93ffa8ab6d8ecbbdf
size 47223

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cc814fc52a37c0724ac5a45a0337af25a2371ed8c4a0d4ce4f899bfb0a6c2ef5
size 47138

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:137bce4fc589d2275239c84ebea5a3fc07f90a31a8dbb0a5efb830a9433aa437
size 43650

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f5f8f51b498725d57c6086741ff07123c504c81c8c2bfafd483130afc2559b35
size 43471