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:
parent
5c14badb5b
commit
44a9da8fbf
3 changed files with 74 additions and 77 deletions
|
|
@ -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 = { }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue