Make PollContentView a11y friendly

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-14 13:45:01 +02:00
parent 5c14badb5b
commit 44a9da8fbf
3 changed files with 74 additions and 77 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>