From 00a1e3c1d0184dccc6714e6ce2944beea0069978 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 9 Jul 2025 16:09:17 +0200 Subject: [PATCH 1/9] [a11y] Fix talkback saying twice "Report room" --- .../android/features/home/impl/roomlist/RoomListContextMenu.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt index 27883e88d5..2a6cfee502 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt @@ -176,7 +176,6 @@ private fun RoomListModalBottomSheetContent( leadingContent = ListItemContent.Icon( iconSource = IconSource.Vector( CompoundIcons.ChatProblem(), - contentDescription = stringResource(CommonStrings.action_report_room), ) ), style = ListItemStyle.Destructive, From 035aaf5aa2dbcb77613278a4d7e64e744e41bcda Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 9 Jul 2025 16:20:47 +0200 Subject: [PATCH 2/9] Sync strings. --- features/poll/api/src/main/res/values/localazy.xml | 1 + libraries/ui-strings/src/main/res/values/localazy.xml | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/features/poll/api/src/main/res/values/localazy.xml b/features/poll/api/src/main/res/values/localazy.xml index 2d1142194c..ebba470b6a 100644 --- a/features/poll/api/src/main/res/values/localazy.xml +++ b/features/poll/api/src/main/res/values/localazy.xml @@ -4,5 +4,6 @@ "%1$d percent of total votes" "%1$d percents of total votes" + "Will remove previous selection" "This is the winning answer" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 270e862300..826dd2fe55 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -7,11 +7,15 @@ "%1$d digit entered" "%1$d digits entered" + "Edit avatar" "Hide password" "Join call" "Jump to bottom" "Mentions only" "Muted" + "New mentions" + "New messages" + "Ongoing call" "Other user\'s avatar" "Page %1$d" "Pause" @@ -35,6 +39,7 @@ "Send files" "Show password" "Start a call" + "Time limited action required" "User avatar" "User menu" "View avatar" From 1f3a017c73ec9ed59fb6c9842f2fa841551f313d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 9 Jul 2025 16:20:59 +0200 Subject: [PATCH 3/9] [a11y] Add click label on edit avatar component https://github.com/element-hq/customer-success/issues/579 --- .../android/libraries/matrix/ui/components/EditableAvatarView.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt index 3b8b6831ca..ad06985ab7 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt @@ -62,6 +62,7 @@ fun EditableAvatarView( modifier = Modifier .clickable( interactionSource = remember { MutableInteractionSource() }, + onClickLabel = stringResource(CommonStrings.a11y_edit_avatar), onClick = onAvatarClick, indication = ripple(bounded = false), ) From 62c2738de09cbc584d80ea17720574b1f7679911 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 9 Jul 2025 16:47:23 +0200 Subject: [PATCH 4/9] [a11y] Make SelectedUser more accessible by grouping the text and the action to remove. https://github.com/element-hq/customer-success/issues/566 --- .../matrix/ui/components/SelectedUser.kt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt index 31cc724838..ad36ef9ced 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt @@ -23,6 +23,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds 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.TextOverflow import androidx.compose.ui.unit.dp @@ -48,9 +51,21 @@ fun SelectedUser( onUserRemove: (MatrixUser) -> Unit, modifier: Modifier = Modifier, ) { + val actionRemove = stringResource(id = CommonStrings.action_remove) Box( modifier = modifier .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( horizontalAlignment = Alignment.CenterHorizontally, @@ -83,7 +98,7 @@ fun SelectedUser( ) { Icon( imageVector = CompoundIcons.Close(), - contentDescription = stringResource(id = CommonStrings.action_remove), + contentDescription = null, tint = ElementTheme.colors.iconOnSolidPrimary, modifier = Modifier.padding(2.dp) ) From 803a5292ba389ecc7bd533131ff2241695ea566d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 9 Jul 2025 17:03:50 +0200 Subject: [PATCH 5/9] [a11y] Improve accessibility of picto next to the timestamp. https://github.com/element-hq/customer-success/issues/572 --- .../components/TimelineEventTimestampView.kt | 29 ++++++++++--------- .../timeline/components/TimelineItemRow.kt | 5 +++- .../src/main/res/values/localazy.xml | 1 + 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt index 644cbf2310..d8af3c1736 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt @@ -18,8 +18,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.unit.dp import io.element.android.compound.theme.ElementTheme @@ -44,15 +42,15 @@ fun TimelineEventTimestampView( modifier: Modifier = Modifier, ) { val formattedTime = event.sentTime - val hasError = event.localSendState is LocalEventSendState.Failed + val hasError = event.failedToSend val hasEncryptionCritical = event.messageShield?.isCritical.orFalse() val isMessageEdited = event.content.isEdited() val isMessageRedacted = event.content.isRedacted() val tint = if (hasError || hasEncryptionCritical && !isMessageRedacted) ElementTheme.colors.textCriticalPrimary else ElementTheme.colors.textSecondary Row( modifier = Modifier - .padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing)) - .then(modifier), + .padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing)) + .then(modifier), verticalAlignment = Alignment.CenterVertically, ) { if (isMessageEdited) { @@ -76,11 +74,13 @@ fun TimelineEventTimestampView( contentDescription = stringResource(id = CommonStrings.common_sending_failed), tint = tint, modifier = Modifier - .size(15.dp, 18.dp) - .clickable(isVerifiedUserSendFailure) { - eventSink(TimelineEvents.ComputeVerifiedUserSendFailure(event)) - } - .semantics { hideFromAccessibility() } + .size(15.dp, 18.dp) + .clickable( + enabled = isVerifiedUserSendFailure, + onClickLabel = stringResource(CommonStrings.action_open_context_menu), + ) { + eventSink(TimelineEvents.ComputeVerifiedUserSendFailure(event)) + } ) } @@ -89,13 +89,14 @@ fun TimelineEventTimestampView( Spacer(modifier = Modifier.width(2.dp)) Icon( imageVector = shield.toIcon(), - contentDescription = shield.toText(), + contentDescription = stringResource(id = CommonStrings.a11y_encryption_details), modifier = Modifier .size(15.dp) - .clickable { + .clickable( + onClickLabel = stringResource(CommonStrings.a11y_view_details), + ) { eventSink(TimelineEvents.ShowShieldDialog(shield)) - } - .semantics { hideFromAccessibility() }, + }, tint = shield.toIconColor(), ) Spacer(modifier = Modifier.width(4.dp)) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index 9628fb2bb4..59ec63d842 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -140,7 +140,10 @@ internal fun TimelineItemRow( timelineItem.safeSenderName } // 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 .then( diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 826dd2fe55..362b2310d4 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -8,6 +8,7 @@ "%1$d digits entered" "Edit avatar" + "Encryption details" "Hide password" "Join call" "Jump to bottom" From 366d040d01209f8ac7457002161bbdc401960a77 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 10 Jul 2025 08:30:39 +0200 Subject: [PATCH 6/9] Remove dead code. --- .../androidutils/accessibility/ContextExt.kt | 29 ------------------- 1 file changed, 29 deletions(-) delete mode 100644 libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/accessibility/ContextExt.kt diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/accessibility/ContextExt.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/accessibility/ContextExt.kt deleted file mode 100644 index 02250297b3..0000000000 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/accessibility/ContextExt.kt +++ /dev/null @@ -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() - ?: return false - - return accessibilityManager.let { - it.isEnabled && it.isTouchExplorationEnabled - } -} From 1790d289c3a0dd386348b551e87be574aee89b1d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 10 Jul 2025 10:04:21 +0200 Subject: [PATCH 7/9] Quality --- .../android/libraries/matrix/ui/components/SelectedUser.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt index ad36ef9ced..cfd378d23c 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt @@ -62,7 +62,10 @@ fun SelectedUser( // when talkback is not enabled onClick( label = actionRemove, - action = { onUserRemove(matrixUser); true } + action = { + onUserRemove(matrixUser) + true + } ) } } From fa9268175ea353a4d667a629a72990095f756e64 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 10 Jul 2025 10:36:57 +0200 Subject: [PATCH 8/9] Fix test --- .../android/features/messages/impl/timeline/TimelineViewTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index c4a2351990..73969a2860 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -116,7 +116,7 @@ class TimelineViewTest { 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() eventsRecorder.assertList( listOf( From 00a00ecbe483ca8c569ae53164369d5c2f800492 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 10 Jul 2025 11:40:38 +0200 Subject: [PATCH 9/9] Fix test --- .../changeroles/ChangeRolesViewTest.kt | 14 ++++++++++---- .../libraries/matrix/ui/components/SelectedUser.kt | 3 ++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesViewTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesViewTest.kt index 4bb0253e6a..6f2179d4d4 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesViewTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/rolesandpermissions/changeroles/ChangeRolesViewTest.kt @@ -225,7 +225,10 @@ class ChangeRolesViewTest { ) // Unselect the user from the row list val contentDescription = rule.activity.getString(CommonStrings.action_remove) - rule.onNodeWithContentDescription(contentDescription).performClick() + rule.onNodeWithContentDescription( + label = contentDescription, + useUnmergedTree = true, + ).performClick() eventsRecorder.assertList( listOf( ChangeRolesEvent.QueryChanged(""), @@ -248,7 +251,7 @@ class ChangeRolesViewTest { rule.setChangeRolesContent( state = state, ) - // Select the user from the row list + // Select the user from the user list rule.onNodeWithText("Carol").performClick() eventsRecorder.assertList( listOf( @@ -271,8 +274,11 @@ class ChangeRolesViewTest { rule.setChangeRolesContent( state = state, ) - // Select the user from the rom list - rule.onAllNodesWithText("Bob")[1].performClick() + // Unselect the user from the user list + rule.onAllNodesWithText( + text = "Bob", + useUnmergedTree = true, + )[1].performClick() eventsRecorder.assertList( listOf( ChangeRolesEvent.QueryChanged(""), diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt index cfd378d23c..f3c3c634e6 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt @@ -101,7 +101,8 @@ fun SelectedUser( ) { Icon( imageVector = CompoundIcons.Close(), - contentDescription = null, + // Note: keep the context description for the test + contentDescription = stringResource(id = CommonStrings.action_remove), tint = ElementTheme.colors.iconOnSolidPrimary, modifier = Modifier.padding(2.dp) )