Merge pull request #4875 from element-hq/feature/bma/a11yPolls
[a11y] Improve screen reader on polls
This commit is contained in:
commit
1a8f38e29a
10 changed files with 76 additions and 14 deletions
|
|
@ -20,6 +20,7 @@ import androidx.compose.ui.geometry.Size
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.isTraversalGroup
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -31,6 +32,7 @@ import io.element.android.features.messages.impl.timeline.components.layout.Cont
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
|
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent
|
||||||
|
|
@ -137,6 +139,8 @@ internal fun TimelineItemRow(
|
||||||
} else {
|
} else {
|
||||||
timelineItem.safeSenderName
|
timelineItem.safeSenderName
|
||||||
}
|
}
|
||||||
|
// For Polls, allow the answers to be traversed by Talkback
|
||||||
|
isTraversalGroup = timelineItem.content is TimelineItemPollContent
|
||||||
}
|
}
|
||||||
// Custom clickable that applies over the whole item for accessibility
|
// Custom clickable that applies over the whole item for accessibility
|
||||||
.then(
|
.then(
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,10 @@ class TimelineItemPollViewTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val answer = content.answerItems[answerIndex].answer
|
val answer = content.answerItems[answerIndex].answer
|
||||||
rule.onNode(hasText(answer.text)).performClick()
|
rule.onNode(
|
||||||
|
matcher = hasText(answer.text),
|
||||||
|
useUnmergedTree = true,
|
||||||
|
).performClick()
|
||||||
eventsRecorder.assertSingle(TimelineEvents.SelectPollAnswer(content.eventId!!, answer.id))
|
eventsRecorder.assertSingle(TimelineEvents.SelectPollAnswer(content.eventId!!, answer.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,13 @@ 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.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
import io.element.android.features.poll.api.R
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
|
@ -32,14 +36,42 @@ import io.element.android.libraries.designsystem.theme.progressIndicatorTrackCol
|
||||||
import io.element.android.libraries.designsystem.toEnabledColor
|
import io.element.android.libraries.designsystem.toEnabledColor
|
||||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
import io.element.android.libraries.ui.strings.CommonPlurals
|
import io.element.android.libraries.ui.strings.CommonPlurals
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PollAnswerView(
|
internal fun PollAnswerView(
|
||||||
answerItem: PollAnswerItem,
|
answerItem: PollAnswerItem,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val nbVotesText = pluralStringResource(
|
||||||
|
id = CommonPlurals.common_poll_votes_count,
|
||||||
|
count = answerItem.votesCount,
|
||||||
|
answerItem.votesCount,
|
||||||
|
)
|
||||||
|
val a11yText = buildString {
|
||||||
|
val sentenceDelimiter = stringResource(CommonStrings.common_sentence_delimiter)
|
||||||
|
append(answerItem.answer.text.removeSuffix("."))
|
||||||
|
if (answerItem.showVotes) {
|
||||||
|
append(sentenceDelimiter)
|
||||||
|
append(nbVotesText)
|
||||||
|
if (answerItem.votesCount != 0) {
|
||||||
|
append(sentenceDelimiter)
|
||||||
|
(answerItem.percentage * 100).toInt().let { percent ->
|
||||||
|
append(pluralStringResource(R.plurals.a11y_polls_percent_of_total, percent, percent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (answerItem.isWinner) {
|
||||||
|
append(sentenceDelimiter)
|
||||||
|
append(stringResource(R.string.a11y_polls_winning_answer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clearAndSetSemantics {
|
||||||
|
contentDescription = a11yText
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (answerItem.isSelected) {
|
imageVector = if (answerItem.isSelected) {
|
||||||
|
|
@ -70,11 +102,6 @@ internal fun PollAnswerView(
|
||||||
style = if (answerItem.isWinner) ElementTheme.typography.fontBodyLgMedium else ElementTheme.typography.fontBodyLgRegular,
|
style = if (answerItem.isWinner) ElementTheme.typography.fontBodyLgMedium else ElementTheme.typography.fontBodyLgRegular,
|
||||||
)
|
)
|
||||||
if (answerItem.showVotes) {
|
if (answerItem.showVotes) {
|
||||||
val text = pluralStringResource(
|
|
||||||
id = CommonPlurals.common_poll_votes_count,
|
|
||||||
count = answerItem.votesCount,
|
|
||||||
answerItem.votesCount
|
|
||||||
)
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.align(Alignment.Bottom),
|
modifier = Modifier.align(Alignment.Bottom),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|
@ -87,13 +114,13 @@ internal fun PollAnswerView(
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(2.dp))
|
Spacer(modifier = Modifier.width(2.dp))
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = nbVotesText,
|
||||||
style = ElementTheme.typography.fontBodySmMedium,
|
style = ElementTheme.typography.fontBodySmMedium,
|
||||||
color = ElementTheme.colors.textPrimary,
|
color = ElementTheme.colors.textPrimary,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = nbVotesText,
|
||||||
style = ElementTheme.typography.fontBodySmRegular,
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
color = ElementTheme.colors.textSecondary,
|
color = ElementTheme.colors.textSecondary,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
8
features/poll/api/src/main/res/values/localazy.xml
Normal file
8
features/poll/api/src/main/res/values/localazy.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<plurals name="a11y_polls_percent_of_total">
|
||||||
|
<item quantity="one">"%1$d percent of total votes"</item>
|
||||||
|
<item quantity="other">"%1$d percents of total votes"</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="a11y_polls_winning_answer">"This is the winning answer"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -143,7 +143,7 @@ fun CreatePollView(
|
||||||
trailingContent = ListItemContent.Custom {
|
trailingContent = ListItemContent.Custom {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.Delete(),
|
imageVector = CompoundIcons.Delete(),
|
||||||
contentDescription = null,
|
contentDescription = stringResource(R.string.screen_create_poll_delete_option_a11y, answer.text),
|
||||||
modifier = Modifier.clickable(answer.canDelete) {
|
modifier = Modifier.clickable(answer.canDelete) {
|
||||||
state.eventSink(CreatePollEvents.RemoveAnswer(index))
|
state.eventSink(CreatePollEvents.RemoveAnswer(index))
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
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.isTraversalGroup
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -178,7 +180,9 @@ private fun PollHistoryList(
|
||||||
if (pollHistoryItems.isEmpty()) {
|
if (pollHistoryItems.isEmpty()) {
|
||||||
item {
|
item {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillParentMaxSize().padding(bottom = 24.dp),
|
modifier = Modifier
|
||||||
|
.fillParentMaxSize()
|
||||||
|
.padding(bottom = 24.dp),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
|
|
@ -191,7 +195,9 @@ private fun PollHistoryList(
|
||||||
text = emptyStringResource,
|
text = emptyStringResource,
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
color = ElementTheme.colors.textSecondary,
|
color = ElementTheme.colors.textSecondary,
|
||||||
modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp, horizontal = 16.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 24.dp, horizontal = 16.dp),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -227,7 +233,10 @@ private fun PollHistoryItemRow(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier,
|
modifier = modifier.semantics(mergeDescendants = true) {
|
||||||
|
// Allow the answers to be traversed by Talkback
|
||||||
|
isTraversalGroup = true
|
||||||
|
},
|
||||||
border = BorderStroke(1.dp, ElementTheme.colors.borderInteractiveSecondary),
|
border = BorderStroke(1.dp, ElementTheme.colors.borderInteractiveSecondary),
|
||||||
shape = RoundedCornerShape(size = 12.dp)
|
shape = RoundedCornerShape(size = 12.dp)
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
<string name="screen_create_poll_anonymous_headline">"Hide votes"</string>
|
<string name="screen_create_poll_anonymous_headline">"Hide votes"</string>
|
||||||
<string name="screen_create_poll_answer_hint">"Option %1$d"</string>
|
<string name="screen_create_poll_answer_hint">"Option %1$d"</string>
|
||||||
<string name="screen_create_poll_cancel_confirmation_content_android">"Your changes have not been saved. Are you sure you want to go back?"</string>
|
<string name="screen_create_poll_cancel_confirmation_content_android">"Your changes have not been saved. Are you sure you want to go back?"</string>
|
||||||
|
<string name="screen_create_poll_delete_option_a11y">"Delete option %1$s"</string>
|
||||||
<string name="screen_create_poll_question_desc">"Question or topic"</string>
|
<string name="screen_create_poll_question_desc">"Question or topic"</string>
|
||||||
<string name="screen_create_poll_question_hint">"What is the poll about?"</string>
|
<string name="screen_create_poll_question_hint">"What is the poll about?"</string>
|
||||||
<string name="screen_create_poll_title">"Create Poll"</string>
|
<string name="screen_create_poll_title">"Create Poll"</string>
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,10 @@ class PollHistoryViewTest {
|
||||||
rule.setPollHistoryViewView(
|
rule.setPollHistoryViewView(
|
||||||
state = state,
|
state = state,
|
||||||
)
|
)
|
||||||
rule.onNodeWithText(answer.text).performClick()
|
rule.onNodeWithText(
|
||||||
|
text = answer.text,
|
||||||
|
useUnmergedTree = true,
|
||||||
|
).performClick()
|
||||||
eventsRecorder.assertSingle(
|
eventsRecorder.assertSingle(
|
||||||
PollHistoryEvents.SelectPollAnswer(eventId, answer.id)
|
PollHistoryEvents.SelectPollAnswer(eventId, answer.id)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,7 @@ Reason: %1$s."</string>
|
||||||
<string name="common_sending">"Sending…"</string>
|
<string name="common_sending">"Sending…"</string>
|
||||||
<string name="common_sending_failed">"Sending failed"</string>
|
<string name="common_sending_failed">"Sending failed"</string>
|
||||||
<string name="common_sent">"Sent"</string>
|
<string name="common_sent">"Sent"</string>
|
||||||
|
<string name="common_sentence_delimiter">". "</string>
|
||||||
<string name="common_server_not_supported">"Server not supported"</string>
|
<string name="common_server_not_supported">"Server not supported"</string>
|
||||||
<string name="common_server_url">"Server URL"</string>
|
<string name="common_server_url">"Server URL"</string>
|
||||||
<string name="common_settings">"Settings"</string>
|
<string name="common_settings">"Settings"</string>
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,12 @@
|
||||||
"screen_polls_history_.*"
|
"screen_polls_history_.*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name" : ":features:poll:api",
|
||||||
|
"includeRegex" : [
|
||||||
|
"a11y\\.polls\\..*"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name" : ":features:securebackup:impl",
|
"name" : ":features:securebackup:impl",
|
||||||
"includeRegex" : [
|
"includeRegex" : [
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue