Merge pull request #6650 from element-hq/feature/bma/a11yFixes

[a11y] Fix a set of issues
This commit is contained in:
Benoit Marty 2026-05-04 11:50:15 +02:00 committed by GitHub
commit abc540b379
44 changed files with 271 additions and 169 deletions

View file

@ -19,6 +19,9 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
@ -90,7 +93,11 @@ fun ChooseSelfVerificationModeView(
Text(
modifier = Modifier
.clickable(onClick = onLearnMore)
.padding(vertical = 4.dp, horizontal = 16.dp),
.padding(vertical = 4.dp, horizontal = 16.dp)
.semantics {
// Note: there is no Role.Link, so we use Role.Button for better accessibility support
role = Role.Button
},
text = stringResource(CommonStrings.action_learn_more),
style = ElementTheme.typography.fontBodyLgMedium
)

View file

@ -237,6 +237,7 @@ private fun SpaceFilterButton(
else -> Unit
}
}
val isSelected = spaceFiltersState is SpaceFiltersState.Selected
IconButton(
onClick = ::onClick,
@ -320,7 +321,15 @@ private fun AccountIcon(
Avatar(
avatarData = avatarData,
avatarType = AvatarType.User,
contentDescription = if (isCurrentAccount) stringResource(CommonStrings.common_settings) else null,
contentDescription = if (isCurrentAccount) {
if (showAvatarIndicator) {
stringResource(CommonStrings.a11y_settings_with_required_action)
} else {
stringResource(CommonStrings.common_settings)
}
} else {
null
},
)
if (showAvatarIndicator) {
RedIndicatorAtom(

View file

@ -143,6 +143,7 @@ class MessagesFlowNode(
val mediaInfo: MediaInfo,
val mediaSource: MediaSource,
val thumbnailSource: MediaSource?,
val canUseOverlay: Boolean,
) : NavTarget
@Parcelize
@ -227,10 +228,11 @@ class MessagesFlowNode(
callback.navigateToRoomDetails()
}
override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean {
override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean {
return processEventClick(
timelineMode = timelineMode,
event = event,
canUseOverlay = canUseOverlay,
)
}
@ -320,7 +322,11 @@ class MessagesFlowNode(
)
val callback = object : MediaViewerEntryPoint.Callback {
override fun onDone() {
overlay.hide()
if (navTarget.canUseOverlay) {
overlay.hide()
} else {
backstack.pop()
}
}
override fun viewInTimeline(eventId: EventId) {
@ -414,10 +420,11 @@ class MessagesFlowNode(
}
NavTarget.PinnedMessagesList -> {
val callback = object : PinnedMessagesListNode.Callback {
override fun handleEventClick(event: TimelineItem.Event) {
override fun handleEventClick(event: TimelineItem.Event, canUseOverlay: Boolean) {
processEventClick(
timelineMode = Timeline.Mode.PinnedEvents,
event = event,
canUseOverlay = canUseOverlay,
)
}
@ -456,10 +463,11 @@ class MessagesFlowNode(
focusedEventId = navTarget.focusedEventId,
)
val callback = object : ThreadedMessagesNode.Callback {
override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean {
override fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean {
return processEventClick(
timelineMode = timelineMode,
event = event,
canUseOverlay = canUseOverlay,
)
}
@ -547,6 +555,7 @@ class MessagesFlowNode(
private fun processEventClick(
timelineMode: Timeline.Mode,
event: TimelineItem.Event,
canUseOverlay: Boolean,
): Boolean {
val navTarget = when (event.content) {
is TimelineItemImageContent -> {
@ -556,6 +565,7 @@ class MessagesFlowNode(
content = event.content,
mediaSource = event.content.mediaSource,
thumbnailSource = event.content.thumbnailSource,
canUseOverlay = canUseOverlay,
)
}
is TimelineItemVideoContent -> {
@ -565,6 +575,7 @@ class MessagesFlowNode(
content = event.content,
mediaSource = event.content.mediaSource,
thumbnailSource = event.content.thumbnailSource,
canUseOverlay = canUseOverlay,
)
}
is TimelineItemFileContent -> {
@ -574,6 +585,7 @@ class MessagesFlowNode(
content = event.content,
mediaSource = event.content.mediaSource,
thumbnailSource = event.content.thumbnailSource,
canUseOverlay = canUseOverlay,
)
}
is TimelineItemAudioContent -> {
@ -583,6 +595,7 @@ class MessagesFlowNode(
content = event.content,
mediaSource = event.content.mediaSource,
thumbnailSource = null,
canUseOverlay = canUseOverlay,
)
}
is TimelineItemLocationContent -> {
@ -603,7 +616,11 @@ class MessagesFlowNode(
}
return when (navTarget) {
is NavTarget.MediaViewer -> {
overlay.show(navTarget)
if (canUseOverlay) {
overlay.show(navTarget)
} else {
backstack.push(navTarget)
}
true
}
is NavTarget.LocationViewer -> {
@ -620,6 +637,7 @@ class MessagesFlowNode(
content: TimelineItemEventContentWithAttachment,
mediaSource: MediaSource,
thumbnailSource: MediaSource?,
canUseOverlay: Boolean,
): NavTarget {
return NavTarget.MediaViewer(
mode = mode,
@ -647,6 +665,7 @@ class MessagesFlowNode(
),
mediaSource = mediaSource,
thumbnailSource = thumbnailSource,
canUseOverlay = canUseOverlay,
)
}

View file

@ -68,6 +68,8 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.mediaplayer.api.MediaPlayer
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.a11y.hasExternalKeyboard
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.LoadMessagesUi
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.finishLongRunningTransaction
@ -115,7 +117,7 @@ class MessagesNode(
)
interface Callback : Plugin {
fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean
fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean
fun navigateToPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?)
fun navigateToRoomMemberDetails(userId: UserId)
fun handlePermalinkClick(data: PermalinkData)
@ -247,6 +249,7 @@ class MessagesNode(
override fun View(modifier: Modifier) {
val activity = requireNotNull(LocalActivity.current)
val isDark = ElementTheme.isLightTheme.not()
val canUseOverlay = !isTalkbackActive() && !hasExternalKeyboard()
CompositionLocalProvider(
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
) {
@ -268,11 +271,11 @@ class MessagesNode(
onRoomDetailsClick = callback::navigateToRoomDetails,
onEventContentClick = { isLive, event ->
if (isLive) {
callback.handleEventClick(timelineController.mainTimelineMode(), event)
callback.handleEventClick(timelineController.mainTimelineMode(), event, canUseOverlay)
} else {
val detachedTimelineMode = timelineController.detachedTimelineMode()
if (detachedTimelineMode != null) {
callback.handleEventClick(detachedTimelineMode, event)
callback.handleEventClick(detachedTimelineMode, event, canUseOverlay)
} else {
false
}

View file

@ -38,6 +38,8 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.a11y.hasExternalKeyboard
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
@ContributesNode(RoomScope::class)
@AssistedInject
@ -50,7 +52,7 @@ class PinnedMessagesListNode(
private val permalinkParser: PermalinkParser,
) : Node(buildContext, plugins = plugins), PinnedMessagesListNavigator {
interface Callback : Plugin {
fun handleEventClick(event: TimelineItem.Event)
fun handleEventClick(event: TimelineItem.Event, canUseOverlay: Boolean)
fun navigateToRoomMemberDetails(userId: UserId)
fun viewInTimeline(eventId: EventId)
fun handlePermalinkClick(data: PermalinkData.RoomLink)
@ -103,6 +105,7 @@ class PinnedMessagesListNode(
@Composable
override fun View(modifier: Modifier) {
val canUseOverlay = !isTalkbackActive() && !hasExternalKeyboard()
CompositionLocalProvider(
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
) {
@ -113,7 +116,9 @@ class PinnedMessagesListNode(
PinnedMessagesListView(
state = state,
onBackClick = ::navigateUp,
onEventClick = callback::handleEventClick,
onEventClick = {
callback.handleEventClick(it, canUseOverlay)
},
onUserDataClick = { callback.navigateToRoomMemberDetails(it.userId) },
onLinkClick = { link -> onLinkClick(context, link.url) },
onLinkLongClick = {

View file

@ -68,6 +68,8 @@ import io.element.android.libraries.matrix.api.room.alias.matches
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.mediaplayer.api.MediaPlayer
import io.element.android.libraries.ui.utils.a11y.hasExternalKeyboard
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.collections.immutable.ImmutableList
@ -124,7 +126,7 @@ class ThreadedMessagesNode(
}
interface Callback : Plugin {
fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event): Boolean
fun handleEventClick(timelineMode: Timeline.Mode, event: TimelineItem.Event, canUseOverlay: Boolean): Boolean
fun navigateToPreviewAttachments(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?)
fun navigateToRoomMemberDetails(userId: UserId)
fun handlePermalinkClick(data: PermalinkData)
@ -252,6 +254,7 @@ class ThreadedMessagesNode(
override fun View(modifier: Modifier) {
val activity = requireNotNull(LocalActivity.current)
val isDark = ElementTheme.isLightTheme.not()
val canUseOverlay = !isTalkbackActive() && !hasExternalKeyboard()
CompositionLocalProvider(
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
) {
@ -271,11 +274,11 @@ class ThreadedMessagesNode(
onEventContentClick = { isLive, event ->
timelineController?.let { controller ->
if (isLive) {
callback.handleEventClick(controller.mainTimelineMode(), event)
callback.handleEventClick(controller.mainTimelineMode(), event, canUseOverlay)
} else {
val detachedTimelineMode = controller.detachedTimelineMode()
if (detachedTimelineMode != null) {
callback.handleEventClick(detachedTimelineMode, event)
callback.handleEventClick(detachedTimelineMode, event, canUseOverlay)
} else {
false
}

View file

@ -77,7 +77,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
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.time.isTalkbackActive
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.wysiwyg.link.Link
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest

View file

@ -47,8 +47,8 @@ import io.element.android.libraries.designsystem.theme.messageFromMeBackground
import io.element.android.libraries.designsystem.theme.messageFromOtherBackground
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.libraries.ui.utils.graphics.drawInLayer
import io.element.android.libraries.ui.utils.time.isTalkbackActive
private val BUBBLE_RADIUS = 12.dp
private val avatarRadius = AvatarSize.TimelineSender.dp / 2

View file

@ -120,7 +120,7 @@ import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonPlurals
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.time.isTalkbackActive
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.wysiwyg.link.Link
import kotlinx.coroutines.launch
import kotlin.math.abs

View file

@ -34,7 +34,7 @@ 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.time.isTalkbackActive
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.wysiwyg.link.Link
@Composable

View file

@ -47,7 +47,7 @@ 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.strings.CommonStrings
import io.element.android.libraries.ui.utils.time.isTalkbackActive
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.wysiwyg.link.Link
import kotlin.time.DurationUnit

View file

@ -54,7 +54,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.time.isTalkbackActive
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.wysiwyg.compose.EditorStyledText
import io.element.android.wysiwyg.link.Link

View file

@ -64,7 +64,7 @@ import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_WIDTH
import io.element.android.libraries.matrix.ui.media.MediaRequestData
import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.time.isTalkbackActive
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.wysiwyg.compose.EditorStyledText
import io.element.android.wysiwyg.link.Link

View file

@ -52,7 +52,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.time.isTalkbackActive
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.libraries.voiceplayer.api.VoiceMessageEvent
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
import io.element.android.libraries.voiceplayer.api.VoiceMessageStateProvider

View file

@ -24,8 +24,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.focused
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@ -227,7 +229,11 @@ private fun ContentInitial(
Text(
modifier = Modifier
.clickable { onLearnMoreClick() }
.padding(vertical = 4.dp, horizontal = 16.dp),
.padding(vertical = 4.dp, horizontal = 16.dp)
.semantics {
// Note: there is no Role.Link, so we use Role.Button for better accessibility support
role = Role.Button
},
text = stringResource(CommonStrings.action_learn_more),
style = ElementTheme.typography.fontBodyLgMedium
)