Merge pull request #7137 from element-hq/fix/a11y-video-preview

Add content description to video preview for screen readers
This commit is contained in:
Benoit Marty
2026-07-02 15:29:38 +02:00
committed by GitHub
5 changed files with 25 additions and 1 deletions
@@ -40,6 +40,7 @@ class DefaultLocalMediaRenderer(
localMediaViewState = localMediaViewState, localMediaViewState = localMediaViewState,
textFileViewer = textFileViewer, textFileViewer = textFileViewer,
audioFocus = audioFocus, audioFocus = audioFocus,
forPreview = true,
onClick = {}, onClick = {},
onOpenWith = null, onOpenWith = null,
) )
@@ -33,6 +33,7 @@ fun LocalMediaView(
onClick: () -> Unit, onClick: () -> Unit,
onOpenWith: (() -> Unit)?, onOpenWith: (() -> Unit)?,
textFileViewer: TextFileViewer, textFileViewer: TextFileViewer,
forPreview: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isDisplayed: Boolean = true, isDisplayed: Boolean = true,
isUserSelected: Boolean = false, isUserSelected: Boolean = false,
@@ -45,6 +46,7 @@ fun LocalMediaView(
localMediaViewState = localMediaViewState, localMediaViewState = localMediaViewState,
localMedia = localMedia, localMedia = localMedia,
modifier = modifier, modifier = modifier,
forPreview = forPreview,
onClick = onClick, onClick = onClick,
) )
mimeType.isMimeTypeVideo() -> MediaVideoView( mimeType.isMimeTypeVideo() -> MediaVideoView(
@@ -54,6 +56,7 @@ fun LocalMediaView(
localMedia = localMedia, localMedia = localMedia,
autoplay = isUserSelected, autoplay = isUserSelected,
audioFocus = audioFocus, audioFocus = audioFocus,
forPreview = forPreview,
modifier = modifier, modifier = modifier,
) )
mimeType == MimeTypes.PlainText -> TextFileView( mimeType == MimeTypes.PlainText -> TextFileView(
@@ -30,6 +30,7 @@ import me.saket.telephoto.zoomable.rememberZoomableImageState
fun MediaImageView( fun MediaImageView(
localMediaViewState: LocalMediaViewState, localMediaViewState: LocalMediaViewState,
localMedia: LocalMedia?, localMedia: LocalMedia?,
forPreview: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@@ -42,11 +43,16 @@ fun MediaImageView(
} else { } else {
val zoomableImageState = rememberZoomableImageState(localMediaViewState.zoomableState) val zoomableImageState = rememberZoomableImageState(localMediaViewState.zoomableState)
localMediaViewState.isReady = zoomableImageState.isImageDisplayed localMediaViewState.isReady = zoomableImageState.isImageDisplayed
val imageDescription = if (forPreview) {
stringResource(CommonStrings.a11y_photo_preview)
} else {
stringResource(CommonStrings.common_image)
}
ZoomableAsyncImage( ZoomableAsyncImage(
modifier = modifier, modifier = modifier,
state = zoomableImageState, state = zoomableImageState,
model = localMedia?.uri, model = localMedia?.uri,
contentDescription = stringResource(id = CommonStrings.common_image), contentDescription = imageDescription,
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
onClick = { onClick() } onClick = { onClick() }
) )
@@ -60,6 +66,7 @@ internal fun MediaImageViewPreview() = ElementPreview {
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
localMediaViewState = rememberLocalMediaViewState(), localMediaViewState = rememberLocalMediaViewState(),
localMedia = null, localMedia = null,
forPreview = false,
onClick = {}, onClick = {},
) )
} }
@@ -30,6 +30,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.media3.common.MediaItem 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.seekToEnsurePlaying
import io.element.android.libraries.mediaviewer.impl.local.player.togglePlay import io.element.android.libraries.mediaviewer.impl.local.player.togglePlay
import io.element.android.libraries.mediaviewer.impl.local.rememberLocalMediaViewState 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 io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import me.saket.telephoto.zoomable.zoomable import me.saket.telephoto.zoomable.zoomable
@@ -75,6 +77,7 @@ fun MediaVideoView(
localMedia: LocalMedia?, localMedia: LocalMedia?,
autoplay: Boolean, autoplay: Boolean,
audioFocus: AudioFocus?, audioFocus: AudioFocus?,
forPreview: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val exoPlayer = rememberExoPlayer(forAudioOnly = false) val exoPlayer = rememberExoPlayer(forAudioOnly = false)
@@ -86,6 +89,7 @@ fun MediaVideoView(
localMedia = localMedia, localMedia = localMedia,
autoplay = autoplay, autoplay = autoplay,
audioFocus = audioFocus, audioFocus = audioFocus,
forPreview = forPreview,
modifier = modifier, modifier = modifier,
) )
} }
@@ -100,6 +104,7 @@ private fun ExoPlayerMediaVideoView(
localMedia: LocalMedia?, localMedia: LocalMedia?,
autoplay: Boolean, autoplay: Boolean,
audioFocus: AudioFocus?, audioFocus: AudioFocus?,
forPreview: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
var mediaPlayerControllerState: MediaPlayerControllerState by remember { var mediaPlayerControllerState: MediaPlayerControllerState by remember {
@@ -218,6 +223,11 @@ private fun ExoPlayerMediaVideoView(
text = "A Video Player will render here", text = "A Video Player will render here",
) )
} else { } else {
val videoDescription = if (forPreview) {
stringResource(CommonStrings.a11y_video_preview)
} else {
stringResource(CommonStrings.common_video)
}
AndroidView( AndroidView(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -236,6 +246,7 @@ private fun ExoPlayerMediaVideoView(
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
useController = false useController = false
contentDescription = videoDescription
} }
}, },
onRelease = { playerView -> onRelease = { playerView ->
@@ -355,5 +366,6 @@ internal fun MediaVideoViewPreview() = ElementPreview {
localMedia = null, localMedia = null,
audioFocus = null, audioFocus = null,
autoplay = false, autoplay = false,
forPreview = false,
) )
} }
@@ -396,6 +396,7 @@ private fun MediaViewerPage(
onOpenWith = onOpenWith, onOpenWith = onOpenWith,
isUserSelected = isUserSelected, isUserSelected = isUserSelected,
audioFocus = audioFocus, audioFocus = audioFocus,
forPreview = false,
) )
if (showThumbnail) { if (showThumbnail) {
ThumbnailView( ThumbnailView(