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 d6dac9a236
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.RadioButtonUnchecked
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.runtime.Composable
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.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
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.IconToggleButton
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.progressIndicatorTrackColor
@ -47,41 +44,33 @@ import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonPlurals
@Composable
fun PollAnswerView(
internal fun PollAnswerView(
answerItem: PollAnswerItem,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier
.fillMaxWidth()
.selectable(
selected = answerItem.isSelected,
enabled = answerItem.isEnabled,
onClick = onClick,
role = Role.RadioButton,
)
modifier = modifier.fillMaxWidth(),
) {
IconToggleButton(
modifier = Modifier.size(22.dp),
checked = answerItem.isSelected,
enabled = answerItem.isEnabled,
colors = IconButtonDefaults.iconToggleButtonColors(
contentColor = ElementTheme.colors.iconSecondary,
checkedContentColor = ElementTheme.colors.iconPrimary,
disabledContentColor = ElementTheme.colors.iconDisabled,
),
onCheckedChange = { onClick() },
) {
Icon(
imageVector = if (answerItem.isSelected) {
Icons.Default.CheckCircle
Icon(
imageVector = if (answerItem.isSelected) {
Icons.Default.CheckCircle
} else {
Icons.Default.RadioButtonUnchecked
},
contentDescription = null,
modifier = Modifier
.padding(0.5.dp)
.size(22.dp),
tint = if (answerItem.isEnabled) {
if (answerItem.isSelected) {
ElementTheme.colors.iconPrimary
} else {
Icons.Default.RadioButtonUnchecked
},
contentDescription = null,
)
}
ElementTheme.colors.iconSecondary
}
} else {
ElementTheme.colors.iconDisabled
},
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Row {
@ -119,65 +108,58 @@ fun PollAnswerView(
}
}
@Preview
@DayNightPreviews
@Composable
internal fun PollAnswerDisclosedNotSelectedPreview() = ElementThemedPreview {
internal fun PollAnswerDisclosedNotSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false),
onClick = { },
)
}
@Preview
@DayNightPreviews
@Composable
internal fun PollAnswerDisclosedSelectedPreview() = ElementThemedPreview {
internal fun PollAnswerDisclosedSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true),
onClick = { }
)
}
@Preview
@DayNightPreviews
@Composable
internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementThemedPreview {
internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = false, isSelected = false),
onClick = { },
)
}
@Preview
@DayNightPreviews
@Composable
internal fun PollAnswerUndisclosedSelectedPreview() = ElementThemedPreview {
internal fun PollAnswerUndisclosedSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = false, isSelected = true),
onClick = { }
)
}
@Preview
@DayNightPreviews
@Composable
internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementThemedPreview {
internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false, isEnabled = false, isWinner = true),
onClick = { }
)
}
@Preview
@DayNightPreviews
@Composable
internal fun PollAnswerEndedWinnerSelectedPreview() = ElementThemedPreview {
internal fun PollAnswerEndedWinnerSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = true),
onClick = { }
)
}
@Preview
@DayNightPreviews
@Composable
internal fun PollAnswerEndedSelectedPreview() = ElementThemedPreview {
internal fun PollAnswerEndedSelectedPreview() = ElementPreview {
PollAnswerView(
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.VectorIcons
import io.element.android.libraries.designsystem.preview.DayNightPreviews
@ -56,24 +59,24 @@ fun PollContentView(
}
Column(
modifier = modifier
.selectableGroup()
.fillMaxWidth(),
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
PollTitle(title = question, isPollEnded = isPollEnded)
PollAnswers(answerItems = answerItems, onAnswerSelected = ::onAnswerSelected)
when {
isPollEnded || pollKind == PollKind.Disclosed -> DisclosedPollBottomNotice(answerItems)
pollKind == PollKind.Undisclosed -> UndisclosedPollBottomNotice()
if (isPollEnded || pollKind == PollKind.Disclosed) {
val votesCount = remember(answerItems) { answerItems.sumOf { it.votesCount } }
DisclosedPollBottomNotice(votesCount = votesCount)
} else {
UndisclosedPollBottomNotice()
}
}
}
@Composable
internal fun PollTitle(
private fun PollTitle(
title: String,
isPollEnded: Boolean,
modifier: Modifier = Modifier
@ -85,13 +88,13 @@ internal fun PollTitle(
if (isPollEnded) {
Icon(
resourceId = VectorIcons.PollEnd,
contentDescription = null,
contentDescription = stringResource(id = CommonStrings.a11y_poll_end),
modifier = Modifier.size(22.dp)
)
} else {
Icon(
resourceId = VectorIcons.Poll,
contentDescription = null,
contentDescription = stringResource(id = CommonStrings.a11y_poll),
modifier = Modifier.size(22.dp)
)
}
@ -103,27 +106,35 @@ internal fun PollTitle(
}
@Composable
internal fun PollAnswers(
private fun PollAnswers(
answerItems: ImmutableList<PollAnswerItem>,
onAnswerSelected: (PollAnswer) -> Unit,
modifier: Modifier = Modifier,
) {
answerItems.forEach { answerItem ->
PollAnswerView(
modifier = modifier,
answerItem = answerItem,
onClick = { onAnswerSelected(answerItem.answer) }
)
Column(
modifier = modifier.selectableGroup(),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
answerItems.forEach {
PollAnswerView(
answerItem = it,
modifier = Modifier
.selectable(
selected = it.isSelected,
enabled = it.isEnabled,
onClick = { onAnswerSelected(it.answer) },
role = Role.RadioButton,
),
)
}
}
}
@Composable
internal fun ColumnScope.DisclosedPollBottomNotice(
answerItems: ImmutableList<PollAnswerItem>,
private fun ColumnScope.DisclosedPollBottomNotice(
votesCount: Int,
modifier: Modifier = Modifier
) {
val votesCount = answerItems.sumOf { it.votesCount }
Text(
modifier = modifier.align(Alignment.End),
style = ElementTheme.typography.fontBodyXsRegular,
@ -133,7 +144,9 @@ internal fun ColumnScope.DisclosedPollBottomNotice(
}
@Composable
fun ColumnScope.UndisclosedPollBottomNotice(modifier: Modifier = Modifier) {
private fun ColumnScope.UndisclosedPollBottomNotice(
modifier: Modifier = Modifier
) {
Text(
modifier = modifier
.align(Alignment.Start)

View file

@ -3,6 +3,8 @@
<string name="a11y_hide_password">"Hide password"</string>
<string name="a11y_notifications_mentions_only">"Mentions only"</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_show_password">"Show password"</string>
<string name="a11y_user_menu">"User menu"</string>

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:54a4fe8e7f968bf487168a517baa0d9c9f6fc5b2354f4eff45f56de77b043f8f
size 56535
oid sha256:5490f2501c6ef257f926fc2f4bd9d94ef4e6e3017d4da290e2199eaeaa2ac5b5
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