From 4fb73497f1d53d8b45e0cfc02f6e52b0fdc7483b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Jun 2025 15:05:42 +0200 Subject: [PATCH 1/5] A11Y: improve accessibility on event reactions. --- .../components/MessagesReactionButton.kt | 59 ++++++++++++++++++- .../impl/src/main/res/values/localazy.xml | 11 +++- .../messages/impl/MessagesViewTest.kt | 10 +++- .../src/main/res/values/localazy.xml | 1 + 4 files changed, 77 insertions(+), 4 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt index 46133b69a7..30f48d7662 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt @@ -28,7 +28,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.onClick import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -47,6 +51,7 @@ import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.ui.media.MediaRequestData +import io.element.android.libraries.ui.strings.CommonStrings @Composable @Suppress("ModifierClickableOrder") // This is needed to display the right ripple shape @@ -68,6 +73,47 @@ fun MessagesReactionButton( buttonColor } + val a11yText = when (content) { + is MessagesReactionsButtonContent.Icon -> stringResource(id = R.string.screen_room_timeline_add_reaction) + is MessagesReactionsButtonContent.Text -> content.text + is MessagesReactionsButtonContent.Reaction -> { + val reaction = if (content.reaction.key.startsWith("mxc://")) { + stringResource(CommonStrings.common_an_image) + } else { + content.reaction.key + } + if (content.isHighlighted) { + if (content.reaction.count == 1) { + stringResource(R.string.screen_room_timeline_reaction_you_a11y, reaction) + } else { + pluralStringResource( + R.plurals.screen_room_timeline_reaction_including_you_a11y, + content.reaction.count - 1, + content.reaction.count - 1, + reaction, + ) + } + } else { + pluralStringResource( + R.plurals.screen_room_timeline_reaction_a11y, + content.reaction.count, + content.reaction.count, + reaction, + ) + } + } + } + + val a11yClickLabel = if (content is MessagesReactionsButtonContent.Reaction) { + if (content.isHighlighted) { + stringResource(id = CommonStrings.a11y_remove_reaction_with, content.reaction.key) + } else { + stringResource(id = CommonStrings.a11y_react_with, content.reaction.key) + } + } else { + "" + } + Surface( modifier = modifier .background(Color.Transparent) @@ -86,7 +132,18 @@ fun MessagesReactionButton( // Inner border, to highlight when selected .border(BorderStroke(1.dp, borderColor), RoundedCornerShape(corner = CornerSize(12.dp))) .background(buttonColor, RoundedCornerShape(corner = CornerSize(12.dp))) - .padding(vertical = 4.dp, horizontal = 10.dp), + .padding(vertical = 4.dp, horizontal = 10.dp) + .clearAndSetSemantics { + contentDescription = a11yText + if (content is MessagesReactionsButtonContent.Reaction) { + onClick( + label = a11yClickLabel + ) { + onClick() + true + } + } + }, color = buttonColor ) { when (content) { diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index 73d266f799..704ba6ed05 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -28,13 +28,22 @@ "Everyone" "Send again" "Your message failed to send" - "Add emoji" + "Add a reaction" "This is the beginning of %1$s." "This is the beginning of this conversation." "Unsupported call. Ask if the caller can use the new Element X app." "Show less" "Message copied" "You do not have permission to post to this room" + + "%1$d member reacted with %2$s" + "%1$d members reacted with %2$s" + + + "You and %1$d member reacted with %2$s" + "You and %1$d members reacted with %2$s" + + "You reacted with %1$s" "Show less" "Show more" "New" diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index 4f6ac91890..d433614537 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -391,7 +391,10 @@ class MessagesViewTest { rule.setMessagesView( state = state, ) - rule.onAllNodesWithText("👍️").onFirst().performClick() + rule.onAllNodesWithText( + text = "👍️", + useUnmergedTree = true, + ).onFirst().performClick() eventsRecorder.assertSingle(MessagesEvents.ToggleReaction("👍️", timelineItem.eventOrTransactionId)) } @@ -411,7 +414,10 @@ class MessagesViewTest { rule.setMessagesView( state = state, ) - rule.onAllNodesWithText("👍️").onFirst().performTouchInput { longClick() } + rule.onAllNodesWithText( + text = "👍️", + useUnmergedTree = true, + ).onFirst().performTouchInput { longClick() } eventsRecorder.assertSingle(ReactionSummaryEvents.ShowReactionSummary(timelineItem.eventId!!, timelineItem.reactionsState.reactions, "👍️")) } diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 21004580f1..1a2052eb51 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -144,6 +144,7 @@ "Acceptable use policy" "Adding caption" "Advanced settings" + "an image" "Analytics" "Appearance" "Audio" From ca00f71e74772258a4a04a76c7b8ac5e641bf3e9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Jun 2025 14:18:52 +0200 Subject: [PATCH 2/5] a11y: improve reaction behavior on message action list. --- .../messages/impl/actionlist/ActionListView.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt index f849effb1a..c3ab8c91d5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt @@ -39,7 +39,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.clearAndSetSemantics -import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.traversalIndex import androidx.compose.ui.text.style.TextAlign @@ -438,7 +438,7 @@ private fun EmojiButton( } else { Color.Transparent } - val description = if (isHighlighted) { + val a11yClickLabel = if (isHighlighted) { stringResource(id = CommonStrings.a11y_remove_reaction_with, emoji) } else { stringResource(id = CommonStrings.a11y_react_with, emoji) @@ -454,7 +454,12 @@ private fun EmojiButton( interactionSource = remember { MutableInteractionSource() } ) .semantics { - contentDescription = description + onClick( + label = a11yClickLabel, + ) { + onClick(emoji) + true + } }, contentAlignment = Alignment.Center ) { From ace1ec9d14fc4e0eb236dc0f011583bc8956cdf2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Jun 2025 14:25:45 +0200 Subject: [PATCH 3/5] a11y: Extract method to avoid code duplication --- .../impl/actionlist/ActionListView.kt | 10 +++---- .../messages/impl/timeline/a11y/Reactions.kt | 26 +++++++++++++++++++ .../components/MessagesReactionButton.kt | 10 +++---- .../components/customreaction/EmojiItem.kt | 12 ++++----- 4 files changed, 41 insertions(+), 17 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/a11y/Reactions.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt index c3ab8c91d5..a16f1efa19 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt @@ -53,6 +53,7 @@ import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUser import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure.ChangedIdentity import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure.None import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure.UnsignedDevice +import io.element.android.features.messages.impl.timeline.a11y.a11yReactionAction import io.element.android.features.messages.impl.timeline.components.MessageShieldView import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent @@ -438,11 +439,10 @@ private fun EmojiButton( } else { Color.Transparent } - val a11yClickLabel = if (isHighlighted) { - stringResource(id = CommonStrings.a11y_remove_reaction_with, emoji) - } else { - stringResource(id = CommonStrings.a11y_react_with, emoji) - } + val a11yClickLabel = a11yReactionAction( + emoji = emoji, + userAlreadyReacted = isHighlighted, + ) Box( modifier = modifier .size(48.dp) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/a11y/Reactions.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/a11y/Reactions.kt new file mode 100644 index 0000000000..e9fed6a6de --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/a11y/Reactions.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.messages.impl.timeline.a11y + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.res.stringResource +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +@ReadOnlyComposable +fun a11yReactionAction( + emoji: String, + userAlreadyReacted: Boolean = false, +): String { + return if (userAlreadyReacted) { + stringResource(id = CommonStrings.a11y_remove_reaction_with, emoji) + } else { + stringResource(id = CommonStrings.a11y_react_with, emoji) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt index 30f48d7662..85402254ad 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import io.element.android.compound.theme.ElementTheme import io.element.android.features.messages.impl.R +import io.element.android.features.messages.impl.timeline.a11y.a11yReactionAction import io.element.android.features.messages.impl.timeline.model.AggregatedReaction import io.element.android.features.messages.impl.timeline.model.AggregatedReactionProvider import io.element.android.features.messages.impl.timeline.model.aTimelineItemReactions @@ -105,11 +106,10 @@ fun MessagesReactionButton( } val a11yClickLabel = if (content is MessagesReactionsButtonContent.Reaction) { - if (content.isHighlighted) { - stringResource(id = CommonStrings.a11y_remove_reaction_with, content.reaction.key) - } else { - stringResource(id = CommonStrings.a11y_react_with, content.reaction.key) - } + a11yReactionAction( + emoji = content.reaction.key, + userAlreadyReacted = content.isHighlighted + ) } else { "" } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiItem.kt index a27de9c324..b6ad695aa8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiItem.kt @@ -22,7 +22,6 @@ import androidx.compose.runtime.remember 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.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.unit.TextUnit @@ -30,11 +29,11 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.compound.theme.ElementTheme import io.element.android.emojibasebindings.Emoji +import io.element.android.features.messages.impl.timeline.a11y.a11yReactionAction import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toDp import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.ui.strings.CommonStrings @Composable fun EmojiItem( @@ -49,11 +48,10 @@ fun EmojiItem( } else { Color.Transparent } - val description = if (isSelected) { - stringResource(id = CommonStrings.a11y_remove_reaction_with, item.unicode) - } else { - stringResource(id = CommonStrings.a11y_react_with, item.unicode) - } + val description = a11yReactionAction( + emoji = item.unicode, + userAlreadyReacted = isSelected, + ) Box( modifier = modifier .sizeIn(minWidth = 40.dp, minHeight = 40.dp) From 2dd31d1b3a47663f2f146134f5664bfe839ba93b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Jun 2025 14:29:00 +0200 Subject: [PATCH 4/5] a11y: Extract method a11yReactionDetails to use it at other places --- .../messages/impl/timeline/a11y/Reactions.kt | 37 ++++++++++++++++++- .../components/MessagesReactionButton.kt | 32 +++------------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/a11y/Reactions.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/a11y/Reactions.kt index e9fed6a6de..9da0168b38 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/a11y/Reactions.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/a11y/Reactions.kt @@ -9,14 +9,16 @@ package io.element.android.features.messages.impl.timeline.a11y import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import io.element.android.features.messages.impl.R import io.element.android.libraries.ui.strings.CommonStrings @Composable @ReadOnlyComposable fun a11yReactionAction( emoji: String, - userAlreadyReacted: Boolean = false, + userAlreadyReacted: Boolean, ): String { return if (userAlreadyReacted) { stringResource(id = CommonStrings.a11y_remove_reaction_with, emoji) @@ -24,3 +26,36 @@ fun a11yReactionAction( stringResource(id = CommonStrings.a11y_react_with, emoji) } } + +@Composable +@ReadOnlyComposable +fun a11yReactionDetails( + emoji: String, + userAlreadyReacted: Boolean, + reactionCount: Int, +): String { + val reaction = if (emoji.startsWith("mxc://")) { + stringResource(CommonStrings.common_an_image) + } else { + emoji + } + return if (userAlreadyReacted) { + if (reactionCount == 1) { + stringResource(R.string.screen_room_timeline_reaction_you_a11y, reaction) + } else { + pluralStringResource( + R.plurals.screen_room_timeline_reaction_including_you_a11y, + reactionCount - 1, + reactionCount - 1, + reaction, + ) + } + } else { + pluralStringResource( + R.plurals.screen_room_timeline_reaction_a11y, + reactionCount, + reactionCount, + reaction, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt index 85402254ad..b0026fb449 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription @@ -40,6 +39,7 @@ import coil3.compose.AsyncImage import io.element.android.compound.theme.ElementTheme import io.element.android.features.messages.impl.R import io.element.android.features.messages.impl.timeline.a11y.a11yReactionAction +import io.element.android.features.messages.impl.timeline.a11y.a11yReactionDetails import io.element.android.features.messages.impl.timeline.model.AggregatedReaction import io.element.android.features.messages.impl.timeline.model.AggregatedReactionProvider import io.element.android.features.messages.impl.timeline.model.aTimelineItemReactions @@ -52,7 +52,6 @@ import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.ui.media.MediaRequestData -import io.element.android.libraries.ui.strings.CommonStrings @Composable @Suppress("ModifierClickableOrder") // This is needed to display the right ripple shape @@ -78,30 +77,11 @@ fun MessagesReactionButton( is MessagesReactionsButtonContent.Icon -> stringResource(id = R.string.screen_room_timeline_add_reaction) is MessagesReactionsButtonContent.Text -> content.text is MessagesReactionsButtonContent.Reaction -> { - val reaction = if (content.reaction.key.startsWith("mxc://")) { - stringResource(CommonStrings.common_an_image) - } else { - content.reaction.key - } - if (content.isHighlighted) { - if (content.reaction.count == 1) { - stringResource(R.string.screen_room_timeline_reaction_you_a11y, reaction) - } else { - pluralStringResource( - R.plurals.screen_room_timeline_reaction_including_you_a11y, - content.reaction.count - 1, - content.reaction.count - 1, - reaction, - ) - } - } else { - pluralStringResource( - R.plurals.screen_room_timeline_reaction_a11y, - content.reaction.count, - content.reaction.count, - reaction, - ) - } + a11yReactionDetails( + emoji = content.reaction.key, + userAlreadyReacted = content.isHighlighted, + reactionCount = content.reaction.count, + ) } } From 37425b192e4de869000a440ce5c98467df880ae1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Jun 2025 14:40:03 +0200 Subject: [PATCH 5/5] a11y: Improve accessibility on ReactionSummaryView --- .../reactionsummary/ReactionSummaryView.kt | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt index b1aa726628..c14d792252 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt @@ -27,6 +27,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api @@ -45,12 +46,17 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import io.element.android.compound.theme.ElementTheme +import io.element.android.features.messages.impl.timeline.a11y.a11yReactionDetails import io.element.android.features.messages.impl.timeline.components.REACTION_IMAGE_ASPECT_RATIO import io.element.android.features.messages.impl.timeline.model.AggregatedReaction import io.element.android.libraries.designsystem.components.avatar.Avatar @@ -140,9 +146,7 @@ private fun ReactionSummaryViewContent( HorizontalPager(state = pagerState) { page -> LazyColumn(modifier = Modifier.fillMaxHeight()) { items(summary.reactions[page].senders) { sender -> - val user = sender.user ?: MatrixUser(userId = sender.senderId) - SenderRow( avatarData = user.getAvatarData(AvatarSize.UserListItem), name = user.displayName ?: user.userId.value, @@ -166,21 +170,32 @@ private fun AggregatedReactionButton( } else { Color.Transparent } - val textColor = if (isHighlighted) { MaterialTheme.colorScheme.inversePrimary } else { ElementTheme.colors.textPrimary } - val roundedCornerShape = RoundedCornerShape(corner = CornerSize(percent = 50)) + val a11yText = a11yReactionDetails( + emoji = reaction.key, + userAlreadyReacted = reaction.isHighlighted, + reactionCount = reaction.count, + ) Surface( modifier = Modifier .background(buttonColor, roundedCornerShape) .clip(roundedCornerShape) .clickable(onClick = onClick) - .padding(vertical = 8.dp, horizontal = 12.dp), - color = buttonColor + .padding(vertical = 8.dp, horizontal = 12.dp) + .selectable( + selected = isHighlighted, + role = Role.Tab, + onClick = onClick, + ) + .clearAndSetSemantics { + contentDescription = a11yText + }, + color = buttonColor, ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -230,7 +245,8 @@ private fun SenderRow( modifier = Modifier .fillMaxWidth() .heightIn(min = 56.dp) - .padding(start = 16.dp, top = 4.dp, end = 16.dp, bottom = 4.dp), + .padding(start = 16.dp, top = 4.dp, end = 16.dp, bottom = 4.dp) + .semantics(mergeDescendants = true) {}, verticalAlignment = Alignment.CenterVertically ) { Avatar(avatarData)