mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-04 20:01:55 +00:00
Merge branch 'develop' into fix/a11y-reading-order
This commit is contained in:
+6
-2
@@ -33,6 +33,8 @@ import androidx.compose.ui.focus.focusRequester
|
|||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
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.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
@@ -112,10 +114,12 @@ private fun RoomListSearchContent(
|
|||||||
title = {
|
title = {
|
||||||
// The stateSaver will keep the selection state when returning to this UI
|
// The stateSaver will keep the selection state when returning to this UI
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val searchLabel = stringResource(CommonStrings.action_search)
|
||||||
FilledTextField(
|
FilledTextField(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester)
|
||||||
|
.semantics { contentDescription = searchLabel },
|
||||||
state = state.query,
|
state = state.query,
|
||||||
lineLimits = TextFieldLineLimits.SingleLine,
|
lineLimits = TextFieldLineLimits.SingleLine,
|
||||||
colors = TextFieldDefaults.colors(
|
colors = TextFieldDefaults.colors(
|
||||||
@@ -132,7 +136,7 @@ private fun RoomListSearchContent(
|
|||||||
IconButton(onClick = { state.eventSink(RoomListSearchEvent.ClearQuery) }) {
|
IconButton(onClick = { state.eventSink(RoomListSearchEvent.ClearQuery) }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.Close(),
|
imageVector = CompoundIcons.Close(),
|
||||||
contentDescription = stringResource(CommonStrings.action_cancel)
|
contentDescription = stringResource(CommonStrings.a11y_clear_search_field)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-9
@@ -25,9 +25,10 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.hideFromAccessibility
|
import androidx.compose.ui.semantics.role
|
||||||
import androidx.compose.ui.semantics.testTag
|
import androidx.compose.ui.semantics.testTag
|
||||||
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
|
||||||
@@ -57,18 +58,12 @@ fun TimelineItemReadReceiptView(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (state.receipts.isNotEmpty()) {
|
if (state.receipts.isNotEmpty()) {
|
||||||
ReadReceiptsRow(
|
ReadReceiptsRow(modifier = modifier) {
|
||||||
modifier = modifier.clearAndSetSemantics {
|
|
||||||
hideFromAccessibility()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
ReadReceiptsAvatars(
|
ReadReceiptsAvatars(
|
||||||
receipts = state.receipts,
|
receipts = state.receipts,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(4.dp))
|
.clip(RoundedCornerShape(4.dp))
|
||||||
.clickable {
|
.clickable(onClick = onReadReceiptsClick)
|
||||||
onReadReceiptsClick()
|
|
||||||
}
|
|
||||||
.padding(2.dp)
|
.padding(2.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -140,6 +135,7 @@ private fun ReadReceiptsAvatars(
|
|||||||
.clearAndSetSemantics {
|
.clearAndSetSemantics {
|
||||||
testTag = TestTags.messageReadReceipts.value
|
testTag = TestTags.messageReadReceipts.value
|
||||||
contentDescription = receiptDescription
|
contentDescription = receiptDescription
|
||||||
|
role = Role.Button
|
||||||
},
|
},
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp - avatarStrokeSize),
|
horizontalArrangement = Arrangement.spacedBy(4.dp - avatarStrokeSize),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|||||||
+1
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
+3
@@ -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(
|
||||||
|
|||||||
+8
-1
@@ -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 = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -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(
|
||||||
|
|||||||
+2
@@ -93,6 +93,7 @@ fun MarkdownTextInput(
|
|||||||
setText(text)
|
setText(text)
|
||||||
setHint(placeholder)
|
setHint(placeholder)
|
||||||
setHintTextColor(ColorStateList.valueOf(placeholderColor.toArgb()))
|
setHintTextColor(ColorStateList.valueOf(placeholderColor.toArgb()))
|
||||||
|
contentDescription = placeholder
|
||||||
inputType = InputType.TYPE_CLASS_TEXT or
|
inputType = InputType.TYPE_CLASS_TEXT or
|
||||||
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or
|
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or
|
||||||
InputType.TYPE_TEXT_FLAG_MULTI_LINE or
|
InputType.TYPE_TEXT_FLAG_MULTI_LINE or
|
||||||
@@ -129,6 +130,7 @@ fun MarkdownTextInput(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
update = { editText ->
|
update = { editText ->
|
||||||
|
editText.contentDescription = placeholder
|
||||||
editText.applyStyleInCompose(richTextEditorStyle)
|
editText.applyStyleInCompose(richTextEditorStyle)
|
||||||
val text = state.text.value()
|
val text = state.text.value()
|
||||||
mentionSpanUpdater.updateMentionSpans(text)
|
mentionSpanUpdater.updateMentionSpans(text)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<string name="a11y_add_reaction">"Add reaction: %1$s"</string>
|
<string name="a11y_add_reaction">"Add reaction: %1$s"</string>
|
||||||
<string name="a11y_address">"Address"</string>
|
<string name="a11y_address">"Address"</string>
|
||||||
<string name="a11y_avatar">"Avatar"</string>
|
<string name="a11y_avatar">"Avatar"</string>
|
||||||
|
<string name="a11y_clear_search_field">"Clear search field"</string>
|
||||||
<string name="a11y_collapse_message_text_field">"Minimise message text field"</string>
|
<string name="a11y_collapse_message_text_field">"Minimise message text field"</string>
|
||||||
<string name="a11y_create_poll_votes_allowed_decrease">"Decrease votes allowed per person"</string>
|
<string name="a11y_create_poll_votes_allowed_decrease">"Decrease votes allowed per person"</string>
|
||||||
<string name="a11y_create_poll_votes_allowed_increase">"Increase votes allowed per person"</string>
|
<string name="a11y_create_poll_votes_allowed_increase">"Increase votes allowed per person"</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user