Merge pull request #7138 from element-hq/fix/a11y-reading-order

Fix timeline reading order distorted when TalkBack is active
This commit is contained in:
Benoit Marty
2026-07-07 09:42:00 +02:00
committed by GitHub
5 changed files with 28 additions and 31 deletions
@@ -103,7 +103,6 @@ import io.element.android.libraries.testtags.TestTag
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.wysiwyg.link.Link
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.delay
@@ -165,9 +164,6 @@ fun TimelineView(
val context = LocalContext.current
val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard)
val view = LocalView.current
// Disable reverse layout when TalkBack is enabled to avoid incorrect ordering issues seen in the current Compose UI version
val useReverseLayout = !isTalkbackActive()
fun inReplyToClick(eventId: EventId) {
state.eventSink(TimelineEvent.FocusOnEvent(eventId))
}
@@ -195,7 +191,7 @@ fun TimelineView(
.nestedScroll(nestedScrollConnection)
.testTag(TestTags.timeline),
state = lazyListState,
reverseLayout = useReverseLayout,
reverseLayout = true,
contentPadding = PaddingValues(top = 64.dp, bottom = 8.dp),
) {
items(
@@ -255,14 +251,12 @@ fun TimelineView(
onFocusOnEvent = ::onFocusOnEvent,
)
if (useReverseLayout) {
FloatingDateBadgeOverlay(
lazyListState = lazyListState,
timelineItems = state.timelineItems,
isLive = state.isLive,
topOffset = floatingDateTopOffset,
)
}
FloatingDateBadgeOverlay(
lazyListState = lazyListState,
timelineItems = state.timelineItems,
isLive = state.isLive,
topOffset = floatingDateTopOffset,
)
}
}
@@ -27,6 +27,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.hideFromAccessibility
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
@@ -102,7 +104,10 @@ internal fun BoxScope.FloatingDateBadgeOverlay(
visible = showBadge,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = 8.dp + topOffset),
.padding(top = 8.dp + topOffset)
.clearAndSetSemantics {
hideFromAccessibility()
},
enter = fadeIn(animationSpec = tween(150)),
exit = fadeOut(animationSpec = tween(300)),
) {
@@ -34,7 +34,6 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.wysiwyg.link.Link
@Composable
@@ -160,13 +159,7 @@ private fun TimelineItemGroupedEventsRowContent(
)
if (isExpanded) {
Column {
timelineItem.events.let {
if (isTalkbackActive()) {
it.reversed()
} else {
it
}
}.forEach { subGroupEvent ->
timelineItem.events.forEach { subGroupEvent ->
TimelineItemRow(
timelineMode = timelineMode,
timelineItem = subGroupEvent,
@@ -61,10 +61,7 @@ fun TimelineItemReadReceiptView(
ReadReceiptsRow(modifier = modifier) {
ReadReceiptsAvatars(
receipts = state.receipts,
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.clickable(onClick = onReadReceiptsClick)
.padding(2.dp)
onClick = onReadReceiptsClick,
)
}
} else {
@@ -124,7 +121,8 @@ private fun ReadReceiptsRow(
@Composable
private fun ReadReceiptsAvatars(
receipts: ImmutableList<ReadReceiptData>,
modifier: Modifier = Modifier
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val avatarSize = AvatarSize.TimelineReadReceipt.dp
val avatarStrokeSize = 1.dp
@@ -132,6 +130,12 @@ private fun ReadReceiptsAvatars(
val receiptDescription = computeReceiptDescription(receipts)
Row(
modifier = modifier
.clip(RoundedCornerShape(4.dp))
.clickable(
onClick = onClick,
onClickLabel = stringResource(id = CommonStrings.a11y_read_receipts_tap_to_show_all_android),
)
.padding(2.dp)
.clearAndSetSemantics {
testTag = TestTags.messageReadReceipts.value
contentDescription = receiptDescription
@@ -44,13 +44,14 @@
<string name="a11y_qr_code">"QR Code"</string>
<string name="a11y_react_with">"React with %1$s"</string>
<string name="a11y_react_with_other_emojis">"React with other emojis"</string>
<string name="a11y_read_receipts_multiple">"Read by %1$s and %2$s"</string>
<string name="a11y_read_receipts_multiple">"Previous message read by %1$s and %2$s"</string>
<plurals name="a11y_read_receipts_multiple_with_others">
<item quantity="one">"Read by %1$s and %2$d other"</item>
<item quantity="other">"Read by %1$s and %2$d others"</item>
<item quantity="one">"Previous message read by %1$s and %2$d other"</item>
<item quantity="other">"Previous message read by %1$s and %2$d others"</item>
</plurals>
<string name="a11y_read_receipts_single">"Read by %1$s"</string>
<string name="a11y_read_receipts_single">"Previous message read by %1$s"</string>
<string name="a11y_read_receipts_tap_to_show_all">"Tap to show all"</string>
<string name="a11y_read_receipts_tap_to_show_all_android">"show all read receipts"</string>
<string name="a11y_remove_reaction">"Remove reaction: %1$s"</string>
<string name="a11y_remove_reaction_with">"Remove reaction with %1$s"</string>
<string name="a11y_room_avatar">"Room avatar"</string>