Merge pull request #5007 from element-hq/feature/bma/a11y/reportRoom
[a11y] Fix several issues around accessibility
This commit is contained in:
commit
f39304d9d2
10 changed files with 57 additions and 50 deletions
|
|
@ -176,7 +176,6 @@ private fun RoomListModalBottomSheetContent(
|
||||||
leadingContent = ListItemContent.Icon(
|
leadingContent = ListItemContent.Icon(
|
||||||
iconSource = IconSource.Vector(
|
iconSource = IconSource.Vector(
|
||||||
CompoundIcons.ChatProblem(),
|
CompoundIcons.ChatProblem(),
|
||||||
contentDescription = stringResource(CommonStrings.action_report_room),
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
style = ListItemStyle.Destructive,
|
style = ListItemStyle.Destructive,
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,6 @@ 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.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.hideFromAccessibility
|
|
||||||
import androidx.compose.ui.semantics.semantics
|
|
||||||
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
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
|
@ -44,15 +42,15 @@ fun TimelineEventTimestampView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val formattedTime = event.sentTime
|
val formattedTime = event.sentTime
|
||||||
val hasError = event.localSendState is LocalEventSendState.Failed
|
val hasError = event.failedToSend
|
||||||
val hasEncryptionCritical = event.messageShield?.isCritical.orFalse()
|
val hasEncryptionCritical = event.messageShield?.isCritical.orFalse()
|
||||||
val isMessageEdited = event.content.isEdited()
|
val isMessageEdited = event.content.isEdited()
|
||||||
val isMessageRedacted = event.content.isRedacted()
|
val isMessageRedacted = event.content.isRedacted()
|
||||||
val tint = if (hasError || hasEncryptionCritical && !isMessageRedacted) ElementTheme.colors.textCriticalPrimary else ElementTheme.colors.textSecondary
|
val tint = if (hasError || hasEncryptionCritical && !isMessageRedacted) ElementTheme.colors.textCriticalPrimary else ElementTheme.colors.textSecondary
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing))
|
.padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing))
|
||||||
.then(modifier),
|
.then(modifier),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
if (isMessageEdited) {
|
if (isMessageEdited) {
|
||||||
|
|
@ -76,11 +74,13 @@ fun TimelineEventTimestampView(
|
||||||
contentDescription = stringResource(id = CommonStrings.common_sending_failed),
|
contentDescription = stringResource(id = CommonStrings.common_sending_failed),
|
||||||
tint = tint,
|
tint = tint,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(15.dp, 18.dp)
|
.size(15.dp, 18.dp)
|
||||||
.clickable(isVerifiedUserSendFailure) {
|
.clickable(
|
||||||
eventSink(TimelineEvents.ComputeVerifiedUserSendFailure(event))
|
enabled = isVerifiedUserSendFailure,
|
||||||
}
|
onClickLabel = stringResource(CommonStrings.action_open_context_menu),
|
||||||
.semantics { hideFromAccessibility() }
|
) {
|
||||||
|
eventSink(TimelineEvents.ComputeVerifiedUserSendFailure(event))
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,13 +89,14 @@ fun TimelineEventTimestampView(
|
||||||
Spacer(modifier = Modifier.width(2.dp))
|
Spacer(modifier = Modifier.width(2.dp))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = shield.toIcon(),
|
imageVector = shield.toIcon(),
|
||||||
contentDescription = shield.toText(),
|
contentDescription = stringResource(id = CommonStrings.a11y_encryption_details),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(15.dp)
|
.size(15.dp)
|
||||||
.clickable {
|
.clickable(
|
||||||
|
onClickLabel = stringResource(CommonStrings.a11y_view_details),
|
||||||
|
) {
|
||||||
eventSink(TimelineEvents.ShowShieldDialog(shield))
|
eventSink(TimelineEvents.ShowShieldDialog(shield))
|
||||||
}
|
},
|
||||||
.semantics { hideFromAccessibility() },
|
|
||||||
tint = shield.toIconColor(),
|
tint = shield.toIconColor(),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,10 @@ internal fun TimelineItemRow(
|
||||||
timelineItem.safeSenderName
|
timelineItem.safeSenderName
|
||||||
}
|
}
|
||||||
// For Polls, allow the answers to be traversed by Talkback
|
// For Polls, allow the answers to be traversed by Talkback
|
||||||
isTraversalGroup = timelineItem.content is TimelineItemPollContent
|
isTraversalGroup = timelineItem.content is TimelineItemPollContent ||
|
||||||
|
timelineItem.failedToSend ||
|
||||||
|
timelineItem.messageShield != null
|
||||||
|
// TODO Also set to true when the event has link(s)
|
||||||
}
|
}
|
||||||
// Custom clickable that applies over the whole item for accessibility
|
// Custom clickable that applies over the whole item for accessibility
|
||||||
.then(
|
.then(
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ class TimelineViewTest {
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val contentDescription = rule.activity.getString(CommonStrings.event_shield_reason_unverified_identity)
|
val contentDescription = rule.activity.getString(CommonStrings.a11y_encryption_details)
|
||||||
rule.onNodeWithContentDescription(contentDescription).performClick()
|
rule.onNodeWithContentDescription(contentDescription).performClick()
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
listOf(
|
listOf(
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,6 @@
|
||||||
<item quantity="one">"%1$d percent of total votes"</item>
|
<item quantity="one">"%1$d percent of total votes"</item>
|
||||||
<item quantity="other">"%1$d percents of total votes"</item>
|
<item quantity="other">"%1$d percents of total votes"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="a11y_polls_will_remove_selection">"Will remove previous selection"</string>
|
||||||
<string name="a11y_polls_winning_answer">"This is the winning answer"</string>
|
<string name="a11y_polls_winning_answer">"This is the winning answer"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,10 @@ class ChangeRolesViewTest {
|
||||||
)
|
)
|
||||||
// Unselect the user from the row list
|
// Unselect the user from the row list
|
||||||
val contentDescription = rule.activity.getString(CommonStrings.action_remove)
|
val contentDescription = rule.activity.getString(CommonStrings.action_remove)
|
||||||
rule.onNodeWithContentDescription(contentDescription).performClick()
|
rule.onNodeWithContentDescription(
|
||||||
|
label = contentDescription,
|
||||||
|
useUnmergedTree = true,
|
||||||
|
).performClick()
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
listOf(
|
listOf(
|
||||||
ChangeRolesEvent.QueryChanged(""),
|
ChangeRolesEvent.QueryChanged(""),
|
||||||
|
|
@ -248,7 +251,7 @@ class ChangeRolesViewTest {
|
||||||
rule.setChangeRolesContent(
|
rule.setChangeRolesContent(
|
||||||
state = state,
|
state = state,
|
||||||
)
|
)
|
||||||
// Select the user from the row list
|
// Select the user from the user list
|
||||||
rule.onNodeWithText("Carol").performClick()
|
rule.onNodeWithText("Carol").performClick()
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
listOf(
|
listOf(
|
||||||
|
|
@ -271,8 +274,11 @@ class ChangeRolesViewTest {
|
||||||
rule.setChangeRolesContent(
|
rule.setChangeRolesContent(
|
||||||
state = state,
|
state = state,
|
||||||
)
|
)
|
||||||
// Select the user from the rom list
|
// Unselect the user from the user list
|
||||||
rule.onAllNodesWithText("Bob")[1].performClick()
|
rule.onAllNodesWithText(
|
||||||
|
text = "Bob",
|
||||||
|
useUnmergedTree = true,
|
||||||
|
)[1].performClick()
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
listOf(
|
listOf(
|
||||||
ChangeRolesEvent.QueryChanged(""),
|
ChangeRolesEvent.QueryChanged(""),
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.androidutils.accessibility
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.accessibility.AccessibilityManager
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether a screen reader is enabled.
|
|
||||||
*
|
|
||||||
* Avoid changing UI or app behavior based on the state of accessibility.
|
|
||||||
* See [AccessibilityManager.isTouchExplorationEnabled] for more details.
|
|
||||||
*
|
|
||||||
* @return true if the screen reader is enabled.
|
|
||||||
*/
|
|
||||||
fun Context.isScreenReaderEnabled(): Boolean {
|
|
||||||
val accessibilityManager = getSystemService<AccessibilityManager>()
|
|
||||||
?: return false
|
|
||||||
|
|
||||||
return accessibilityManager.let {
|
|
||||||
it.isEnabled && it.isTouchExplorationEnabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -62,6 +62,7 @@ fun EditableAvatarView(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClickLabel = stringResource(CommonStrings.a11y_edit_avatar),
|
||||||
onClick = onAvatarClick,
|
onClick = onAvatarClick,
|
||||||
indication = ripple(bounded = false),
|
indication = ripple(bounded = false),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.onClick
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -48,9 +51,24 @@ fun SelectedUser(
|
||||||
onUserRemove: (MatrixUser) -> Unit,
|
onUserRemove: (MatrixUser) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val actionRemove = stringResource(id = CommonStrings.action_remove)
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.width(AvatarSize.SelectedUser.dp)
|
.width(AvatarSize.SelectedUser.dp)
|
||||||
|
.clearAndSetSemantics {
|
||||||
|
contentDescription = matrixUser.getBestName()
|
||||||
|
if (canRemove) {
|
||||||
|
// Note: this does not set the click effect to the whole Box
|
||||||
|
// when talkback is not enabled
|
||||||
|
onClick(
|
||||||
|
label = actionRemove,
|
||||||
|
action = {
|
||||||
|
onUserRemove(matrixUser)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
|
@ -83,6 +101,7 @@ fun SelectedUser(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.Close(),
|
imageVector = CompoundIcons.Close(),
|
||||||
|
// Note: keep the context description for the test
|
||||||
contentDescription = stringResource(id = CommonStrings.action_remove),
|
contentDescription = stringResource(id = CommonStrings.action_remove),
|
||||||
tint = ElementTheme.colors.iconOnSolidPrimary,
|
tint = ElementTheme.colors.iconOnSolidPrimary,
|
||||||
modifier = Modifier.padding(2.dp)
|
modifier = Modifier.padding(2.dp)
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,16 @@
|
||||||
<item quantity="one">"%1$d digit entered"</item>
|
<item quantity="one">"%1$d digit entered"</item>
|
||||||
<item quantity="other">"%1$d digits entered"</item>
|
<item quantity="other">"%1$d digits entered"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="a11y_edit_avatar">"Edit avatar"</string>
|
||||||
|
<string name="a11y_encryption_details">"Encryption details"</string>
|
||||||
<string name="a11y_hide_password">"Hide password"</string>
|
<string name="a11y_hide_password">"Hide password"</string>
|
||||||
<string name="a11y_join_call">"Join call"</string>
|
<string name="a11y_join_call">"Join call"</string>
|
||||||
<string name="a11y_jump_to_bottom">"Jump to bottom"</string>
|
<string name="a11y_jump_to_bottom">"Jump to bottom"</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_notifications_new_mentions">"New mentions"</string>
|
||||||
|
<string name="a11y_notifications_new_messages">"New messages"</string>
|
||||||
|
<string name="a11y_notifications_ongoing_call">"Ongoing call"</string>
|
||||||
<string name="a11y_other_user_avatar">"Other user\'s avatar"</string>
|
<string name="a11y_other_user_avatar">"Other user\'s avatar"</string>
|
||||||
<string name="a11y_page_n">"Page %1$d"</string>
|
<string name="a11y_page_n">"Page %1$d"</string>
|
||||||
<string name="a11y_pause">"Pause"</string>
|
<string name="a11y_pause">"Pause"</string>
|
||||||
|
|
@ -35,6 +40,7 @@
|
||||||
<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_start_call">"Start a call"</string>
|
<string name="a11y_start_call">"Start a call"</string>
|
||||||
|
<string name="a11y_time_limited_action_required">"Time limited action required"</string>
|
||||||
<string name="a11y_user_avatar">"User avatar"</string>
|
<string name="a11y_user_avatar">"User avatar"</string>
|
||||||
<string name="a11y_user_menu">"User menu"</string>
|
<string name="a11y_user_menu">"User menu"</string>
|
||||||
<string name="a11y_view_avatar">"View avatar"</string>
|
<string name="a11y_view_avatar">"View avatar"</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue