From 79f1423a30f365418d36273741a57d02f0a041c5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jul 2026 17:49:32 +0200 Subject: [PATCH 1/5] Fix accessibility: message composer label and 'Seen by' button (#6390, #6393) - MarkdownTextInput: set contentDescription on MarkdownEditText so TalkBack announces a persistent label even when the field has content (fixes #6390) - TimelineItemReadReceiptView: remove hideFromAccessibility() so the 'Seen by' button is exposed to TalkBack; use semantics (not clearAndSetSemantics) to preserve click action, and add Role.Button (fixes #6393) Co-Authored-By: Claude Sonnet 4.6 --- .../receipt/TimelineItemReadReceiptView.kt | 18 +++++++----------- .../components/markdown/MarkdownTextInput.kt | 2 ++ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt index 037d9ea45d..071594c6ac 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt @@ -25,9 +25,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.hideFromAccessibility +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -57,18 +58,12 @@ fun TimelineItemReadReceiptView( modifier: Modifier = Modifier, ) { if (state.receipts.isNotEmpty()) { - ReadReceiptsRow( - modifier = modifier.clearAndSetSemantics { - hideFromAccessibility() - } - ) { + ReadReceiptsRow(modifier = modifier) { ReadReceiptsAvatars( receipts = state.receipts, modifier = Modifier .clip(RoundedCornerShape(4.dp)) - .clickable { - onReadReceiptsClick() - } + .clickable { onReadReceiptsClick() } .padding(2.dp) ) } @@ -137,9 +132,10 @@ private fun ReadReceiptsAvatars( val receiptDescription = computeReceiptDescription(receipts) Row( modifier = modifier - .clearAndSetSemantics { + .semantics { testTag = TestTags.messageReadReceipts.value contentDescription = receiptDescription + role = Role.Button }, horizontalArrangement = Arrangement.spacedBy(4.dp - avatarStrokeSize), verticalAlignment = Alignment.CenterVertically, diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownTextInput.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownTextInput.kt index bd6944d603..064ef03106 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownTextInput.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/markdown/MarkdownTextInput.kt @@ -93,6 +93,7 @@ fun MarkdownTextInput( setText(text) setHint(placeholder) setHintTextColor(ColorStateList.valueOf(placeholderColor.toArgb())) + contentDescription = placeholder inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or InputType.TYPE_TEXT_FLAG_MULTI_LINE or @@ -129,6 +130,7 @@ fun MarkdownTextInput( } }, update = { editText -> + editText.contentDescription = placeholder editText.applyStyleInCompose(richTextEditorStyle) val text = state.text.value() mentionSpanUpdater.updateMentionSpans(text) From 01c8064a16d8afa266f81e1e2024d05545cdb22d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 1 Jul 2026 17:52:56 +0200 Subject: [PATCH 2/5] Fix accessibility: add content description to video preview (#6382) Set contentDescription on the ExoPlayer PlayerView so TalkBack announces the video preview to screen reader users instead of treating it as decorative/invisible content. Co-Authored-By: Claude Sonnet 4.6 --- .../libraries/mediaviewer/impl/local/video/MediaVideoView.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt index 21279a598e..b360e01cd8 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.stringResource import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.media3.common.MediaItem @@ -60,6 +61,7 @@ import io.element.android.libraries.mediaviewer.impl.local.player.rememberExoPla import io.element.android.libraries.mediaviewer.impl.local.player.seekToEnsurePlaying import io.element.android.libraries.mediaviewer.impl.local.player.togglePlay import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState +import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.utils.a11y.isTalkbackActive import kotlinx.coroutines.delay import me.saket.telephoto.zoomable.zoomable @@ -218,6 +220,7 @@ private fun ExoPlayerMediaVideoView( text = "A Video Player will render here", ) } else { + val videoDescription = stringResource(CommonStrings.a11y_video_preview) AndroidView( modifier = Modifier .fillMaxSize() @@ -236,6 +239,7 @@ private fun ExoPlayerMediaVideoView( resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) useController = false + contentDescription = videoDescription } }, onRelease = { playerView -> From c1dfd6d6ae75841be44804f140b89a3f7de725d5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jul 2026 11:10:14 +0200 Subject: [PATCH 3/5] A11Y: Improve talkback for video and image / preview not preview. --- .../impl/local/DefaultLocalMediaRenderer.kt | 1 + .../libraries/mediaviewer/impl/local/LocalMediaView.kt | 3 +++ .../mediaviewer/impl/local/image/MediaImageView.kt | 9 ++++++++- .../mediaviewer/impl/local/video/MediaVideoView.kt | 10 +++++++++- .../mediaviewer/impl/viewer/MediaViewerView.kt | 1 + 5 files changed, 22 insertions(+), 2 deletions(-) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/DefaultLocalMediaRenderer.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/DefaultLocalMediaRenderer.kt index 319aebe33e..f7564aec31 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/DefaultLocalMediaRenderer.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/DefaultLocalMediaRenderer.kt @@ -40,6 +40,7 @@ class DefaultLocalMediaRenderer( localMediaViewState = localMediaViewState, textFileViewer = textFileViewer, audioFocus = audioFocus, + forPreview = true, onClick = {}, onOpenWith = null, ) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaView.kt index c19b1a24c0..01b44f1246 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/LocalMediaView.kt @@ -33,6 +33,7 @@ fun LocalMediaView( onClick: () -> Unit, onOpenWith: (() -> Unit)?, textFileViewer: TextFileViewer, + forPreview: Boolean, modifier: Modifier = Modifier, isDisplayed: Boolean = true, isUserSelected: Boolean = false, @@ -45,6 +46,7 @@ fun LocalMediaView( localMediaViewState = localMediaViewState, localMedia = localMedia, modifier = modifier, + forPreview = forPreview, onClick = onClick, ) mimeType.isMimeTypeVideo() -> MediaVideoView( @@ -54,6 +56,7 @@ fun LocalMediaView( localMedia = localMedia, autoplay = isUserSelected, audioFocus = audioFocus, + forPreview = forPreview, modifier = modifier, ) mimeType == MimeTypes.PlainText -> TextFileView( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/image/MediaImageView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/image/MediaImageView.kt index 4c73dd215d..c935b17b85 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/image/MediaImageView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/image/MediaImageView.kt @@ -30,6 +30,7 @@ import me.saket.telephoto.zoomable.rememberZoomableImageState fun MediaImageView( localMediaViewState: LocalMediaViewState, localMedia: LocalMedia?, + forPreview: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -42,11 +43,16 @@ fun MediaImageView( } else { val zoomableImageState = rememberZoomableImageState(localMediaViewState.zoomableState) localMediaViewState.isReady = zoomableImageState.isImageDisplayed + val imageDescription = if (forPreview) { + stringResource(CommonStrings.a11y_photo_preview) + } else { + stringResource(CommonStrings.common_image) + } ZoomableAsyncImage( modifier = modifier, state = zoomableImageState, model = localMedia?.uri, - contentDescription = stringResource(id = CommonStrings.common_image), + contentDescription = imageDescription, contentScale = ContentScale.Fit, onClick = { onClick() } ) @@ -60,6 +66,7 @@ internal fun MediaImageViewPreview() = ElementPreview { modifier = Modifier.fillMaxSize(), localMediaViewState = rememberLocalMediaViewState(), localMedia = null, + forPreview = false, onClick = {}, ) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt index b360e01cd8..d6484cd5a4 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt @@ -77,6 +77,7 @@ fun MediaVideoView( localMedia: LocalMedia?, autoplay: Boolean, audioFocus: AudioFocus?, + forPreview: Boolean, modifier: Modifier = Modifier, ) { val exoPlayer = rememberExoPlayer(forAudioOnly = false) @@ -88,6 +89,7 @@ fun MediaVideoView( localMedia = localMedia, autoplay = autoplay, audioFocus = audioFocus, + forPreview = forPreview, modifier = modifier, ) } @@ -102,6 +104,7 @@ private fun ExoPlayerMediaVideoView( localMedia: LocalMedia?, autoplay: Boolean, audioFocus: AudioFocus?, + forPreview: Boolean, modifier: Modifier = Modifier, ) { var mediaPlayerControllerState: MediaPlayerControllerState by remember { @@ -220,7 +223,11 @@ private fun ExoPlayerMediaVideoView( text = "A Video Player will render here", ) } else { - val videoDescription = stringResource(CommonStrings.a11y_video_preview) + val videoDescription = if (forPreview) { + stringResource(CommonStrings.a11y_video_preview) + } else { + stringResource(CommonStrings.common_video) + } AndroidView( modifier = Modifier .fillMaxSize() @@ -359,5 +366,6 @@ internal fun MediaVideoViewPreview() = ElementPreview { localMedia = null, audioFocus = null, autoplay = false, + forPreview = false, ) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt index c4d442338a..c981132b48 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt @@ -396,6 +396,7 @@ private fun MediaViewerPage( onOpenWith = onOpenWith, isUserSelected = isUserSelected, audioFocus = audioFocus, + forPreview = false, ) if (showThumbnail) { ThumbnailView( From 57ed76bd0334ef19c93b14ff5cc1ccddc096b99a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jul 2026 12:26:49 +0200 Subject: [PATCH 4/5] A11Y: Do not read the `+x` Text. --- .../components/receipt/TimelineItemReadReceiptView.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt index 071594c6ac..3ccf32d422 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt @@ -26,9 +26,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -63,7 +63,7 @@ fun TimelineItemReadReceiptView( receipts = state.receipts, modifier = Modifier .clip(RoundedCornerShape(4.dp)) - .clickable { onReadReceiptsClick() } + .clickable(onClick = onReadReceiptsClick) .padding(2.dp) ) } @@ -132,7 +132,7 @@ private fun ReadReceiptsAvatars( val receiptDescription = computeReceiptDescription(receipts) Row( modifier = modifier - .semantics { + .clearAndSetSemantics { testTag = TestTags.messageReadReceipts.value contentDescription = receiptDescription role = Role.Button From a3fbbed8f4e602f6caf55742e1e6daeb1e11a79c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jul 2026 13:16:01 +0200 Subject: [PATCH 5/5] A11Y: Improve accessibility of search field. Closes #6389 --- .../features/home/impl/search/RoomListSearchView.kt | 8 ++++++-- libraries/ui-strings/src/main/res/values/localazy.xml | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt index 942e924820..f67338ef21 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt @@ -33,6 +33,8 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.generated.CompoundIcons @@ -112,10 +114,12 @@ private fun RoomListSearchContent( title = { // The stateSaver will keep the selection state when returning to this UI val focusRequester = remember { FocusRequester() } + val searchLabel = stringResource(CommonStrings.action_search) FilledTextField( modifier = Modifier .fillMaxWidth() - .focusRequester(focusRequester), + .focusRequester(focusRequester) + .semantics { contentDescription = searchLabel }, state = state.query, lineLimits = TextFieldLineLimits.SingleLine, colors = TextFieldDefaults.colors( @@ -132,7 +136,7 @@ private fun RoomListSearchContent( IconButton(onClick = { state.eventSink(RoomListSearchEvent.ClearQuery) }) { Icon( imageVector = CompoundIcons.Close(), - contentDescription = stringResource(CommonStrings.action_cancel) + contentDescription = stringResource(CommonStrings.a11y_clear_search_field) ) } } diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index f477af40d8..4885c38ef3 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -4,6 +4,7 @@ "Add reaction: %1$s" "Address" "Avatar" + "Clear search field" "Minimise message text field" "Decrease votes allowed per person" "Increase votes allowed per person"