Improve accessibility of the timeline (#4579)

* Make whole messages selectable and readable as a single unit when possible.
* Make most UI components not clickable when talkback is enabled.
* Make voice messages work with talkback too.
* Read grouped state events even if the events are collapsed.
* Move image and video item actions to the timeline item.
* Improve accessibility in the message context menu too
* Fix a11y issue on add attachment button.
* Add `contentDescription` to file icon so it's read aloud

---------

Co-authored-by: Benoit Marty <benoit@matrix.org>
This commit is contained in:
Jorge Martin Espinosa 2025-04-15 17:28:29 +02:00 committed by GitHub
parent f9c00ac99d
commit 740c28eda9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 226 additions and 70 deletions

View file

@ -19,13 +19,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.ui.strings.CommonStrings
/**
* Button with colored background.
@ -35,6 +33,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun IconColorButton(
onClick: () -> Unit,
imageVector: ImageVector,
contentDescription: String?,
modifier: Modifier = Modifier,
buttonSize: ButtonSize = ButtonSize.Large,
iconColorButtonStyle: IconColorButtonStyle = IconColorButtonStyle.Primary,
@ -55,7 +54,7 @@ fun IconColorButton(
.background(bgColor)
.padding(buttonSize.toContainerPadding()),
imageVector = imageVector,
contentDescription = stringResource(CommonStrings.action_close),
contentDescription = contentDescription,
tint = ElementTheme.colors.iconOnSolidPrimary
)
}
@ -101,6 +100,7 @@ internal fun IconColorButtonPreview() = ElementPreview {
IconColorButton(
onClick = {},
imageVector = CompoundIcons.Close(),
contentDescription = null,
buttonSize = size,
iconColorButtonStyle = style,
)

View file

@ -24,7 +24,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.traversalIndex
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@ -58,7 +60,7 @@ fun InReplyToView(
senderId = inReplyTo.senderId,
senderProfile = inReplyTo.senderProfile,
metadata = inReplyTo.metadata(hideImage),
modifier = modifier
modifier = modifier,
)
}
is InReplyToDetails.Error ->
@ -96,13 +98,18 @@ private fun ReplyToReadyContent(
Spacer(modifier = Modifier.width(8.dp))
}
val a11InReplyToText = stringResource(CommonStrings.common_in_reply_to, senderProfile.getDisambiguatedDisplayName(senderId))
Column(verticalArrangement = Arrangement.SpaceBetween) {
Column(
modifier = Modifier.semantics(mergeDescendants = false) { isTraversalGroup = true },
verticalArrangement = Arrangement.SpaceBetween
) {
SenderName(
senderId = senderId,
senderProfile = senderProfile,
senderNameMode = SenderNameMode.Reply,
modifier = Modifier.semantics {
contentDescription = a11InReplyToText
isTraversalGroup = true
traversalIndex = 1f
},
)
ReplyToContentText(metadata)
@ -169,6 +176,10 @@ private fun ReplyToContentText(metadata: InReplyToMetadata?) {
else -> FontStyle.Normal
}
Row(
modifier = Modifier.semantics(mergeDescendants = false) {
isTraversalGroup = true
traversalIndex = -1f
},
verticalAlignment = Alignment.CenterVertically,
) {
if (iconResourceId != null) {

View file

@ -148,6 +148,7 @@ fun TextComposer(
IconColorButton(
onClick = onAddAttachment,
imageVector = CompoundIcons.Plus(),
contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment),
)
}
}
@ -292,6 +293,7 @@ fun TextComposer(
IconColorButton(
onClick = onDismissTextFormatting,
imageVector = CompoundIcons.Close(),
contentDescription = stringResource(CommonStrings.action_close),
)
},
textFormatting = textFormattingOptions,

View file

@ -0,0 +1,20 @@
/*
* Copyright 2025 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.ui.utils.time
import android.view.accessibility.AccessibilityManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
@Composable
fun isTalkbackActive(): Boolean {
val context = LocalContext.current
val accessibilityManager = remember { context.getSystemService(AccessibilityManager::class.java) }
return accessibilityManager.isTouchExplorationEnabled
}