[a11y] Ensure that the focus is not lost when the send button state change.

This commit is contained in:
Benoit Marty
2025-07-03 17:19:43 +02:00
parent a84f387f4d
commit 24f08e8d20
4 changed files with 67 additions and 17 deletions

View File

@@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -39,7 +40,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.hideFromAccessibility
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -280,6 +285,13 @@ fun TextComposer(
else -> sendButton
}
val endButtonA11y = endButtonA11y(
composerMode = composerMode,
voiceMessageState = voiceMessageState,
enableVoiceMessages = enableVoiceMessages,
canSendMessage = canSendMessage,
)
val voiceRecording = @Composable {
when (voiceMessageState) {
is VoiceMessageState.Preview ->
@@ -323,6 +335,7 @@ fun TextComposer(
)
},
textFormatting = textFormattingOptions,
endButtonA11y = endButtonA11y,
sendButton = sendButton,
)
} else {
@@ -334,6 +347,7 @@ fun TextComposer(
composerOptionsButton = composerOptionsButton,
textInput = textInput,
endButton = sendOrRecordButton,
endButtonA11y = endButtonA11y,
voiceRecording = voiceRecording,
voiceDeleteButton = voiceDeleteButton,
)
@@ -359,6 +373,40 @@ fun TextComposer(
}
}
@ReadOnlyComposable
@Composable
private fun endButtonA11y(
composerMode: MessageComposerMode,
voiceMessageState: VoiceMessageState,
enableVoiceMessages: Boolean,
canSendMessage: Boolean,
): (SemanticsPropertyReceiver) -> Unit {
val a11ySendButtonDescription = stringResource(
id = when {
enableVoiceMessages && !canSendMessage ->
when (voiceMessageState) {
VoiceMessageState.Idle,
is VoiceMessageState.Recording -> if (voiceMessageState is VoiceMessageState.Recording) {
CommonStrings.a11y_voice_message_stop_recording
} else {
CommonStrings.a11y_voice_message_record
}
is VoiceMessageState.Preview -> when (voiceMessageState.isSending) {
true -> CommonStrings.common_sending
false -> CommonStrings.action_send_voice_message
}
}
composerMode.isEditing -> CommonStrings.action_send_edited_message
else -> CommonStrings.action_send_message
}
)
val endButtonA11y: (SemanticsPropertyReceiver.() -> Unit) = {
contentDescription = a11ySendButtonDescription
onClick(null, null)
}
return endButtonA11y
}
@Composable
private fun StandardLayout(
voiceMessageState: VoiceMessageState,
@@ -369,6 +417,7 @@ private fun StandardLayout(
voiceRecording: @Composable () -> Unit,
voiceDeleteButton: @Composable () -> Unit,
endButton: @Composable () -> Unit,
endButtonA11y: (SemanticsPropertyReceiver.() -> Unit),
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
@@ -416,7 +465,8 @@ private fun StandardLayout(
Box(
Modifier
.padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp)
.size(48.dp),
.size(48.dp)
.clearAndSetSemantics(endButtonA11y),
contentAlignment = Alignment.Center,
) {
endButton()
@@ -454,6 +504,7 @@ private fun TextFormattingLayout(
dismissTextFormattingButton: @Composable () -> Unit,
textFormatting: @Composable () -> Unit,
sendButton: @Composable () -> Unit,
endButtonA11y: (SemanticsPropertyReceiver.() -> Unit),
modifier: Modifier = Modifier
) {
Column(
@@ -485,10 +536,12 @@ private fun TextFormattingLayout(
textFormatting()
}
Box(
modifier = Modifier.padding(
start = 14.dp,
end = 6.dp
)
modifier = Modifier
.padding(
start = 14.dp,
end = 6.dp,
)
.clearAndSetSemantics(endButtonA11y)
) {
sendButton()
}

View File

@@ -21,7 +21,6 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.LinearGradientShader
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
@@ -32,7 +31,6 @@ import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.ui.strings.CommonStrings
/**
* Send button for the message composer.
@@ -60,10 +58,6 @@ internal fun SendButton(
composerMode.isEditing -> 0.dp
else -> 2.dp
}
val contentDescription = when {
composerMode.isEditing -> stringResource(CommonStrings.action_edit)
else -> stringResource(CommonStrings.action_send)
}
Box(
modifier = Modifier
.clip(CircleShape)
@@ -81,7 +75,8 @@ internal fun SendButton(
.padding(start = iconStartPadding)
.align(Alignment.Center),
imageVector = iconVector,
contentDescription = contentDescription,
// Note: accessibility is managed in TextComposer.
contentDescription = null,
tint = if (canSendMessage) {
if (ElementTheme.colors.isLight) {
ElementTheme.colors.iconOnSolidPrimary

View File

@@ -16,7 +16,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
@@ -26,7 +25,6 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun VoiceMessageRecorderButton(
@@ -70,7 +68,8 @@ private fun StartButton(
Icon(
modifier = Modifier.size(24.dp),
imageVector = CompoundIcons.MicOn(),
contentDescription = stringResource(CommonStrings.a11y_voice_message_record),
// Note: accessibility is managed in TextComposer.
contentDescription = null,
tint = ElementTheme.colors.iconSecondary,
)
}
@@ -95,7 +94,8 @@ private fun StopButton(
Icon(
modifier = Modifier.size(24.dp),
resourceId = CommonDrawables.ic_stop,
contentDescription = stringResource(CommonStrings.a11y_voice_message_stop_recording),
// Note: accessibility is managed in TextComposer.
contentDescription = null,
tint = ElementTheme.colors.iconOnSolidPrimary,
)
}

View File

@@ -12,7 +12,7 @@
<string name="a11y_jump_to_bottom">"Jump to bottom"</string>
<string name="a11y_notifications_mentions_only">"Mentions only"</string>
<string name="a11y_notifications_muted">"Muted"</string>
<string name="a11y_other_user_avatar">"Other user avatar"</string>
<string name="a11y_other_user_avatar">"Other user\'s avatar"</string>
<string name="a11y_page_n">"Page %1$d"</string>
<string name="a11y_pause">"Pause"</string>
<string name="a11y_paused_voice_message">"Voice message, duration: %1$s, current position: %2$s"</string>
@@ -125,7 +125,9 @@
<string name="action_save">"Save"</string>
<string name="action_search">"Search"</string>
<string name="action_send">"Send"</string>
<string name="action_send_edited_message">"Send edited message"</string>
<string name="action_send_message">"Send message"</string>
<string name="action_send_voice_message">"Send voice message"</string>
<string name="action_share">"Share"</string>
<string name="action_share_link">"Share link"</string>
<string name="action_show">"Show"</string>