A11Y: ensure a11y focus is not lost and reset to the back button when the user use the keyboard to focus the send button and press the space bar to perform a click.
This commit is contained in:
@@ -39,6 +39,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
|
||||
@@ -61,6 +63,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
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.IconColorButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
@@ -123,9 +126,6 @@ fun TextComposer(
|
||||
is TextEditorState.Markdown -> state.state.text.value()
|
||||
is TextEditorState.Rich -> state.richTextEditorState.messageMarkdown
|
||||
}
|
||||
val onSendClick = {
|
||||
onSendMessage()
|
||||
}
|
||||
|
||||
val onPlayVoiceMessageClick = {
|
||||
onVoicePlayerEvent(VoiceMessagePlayerEvent.Play)
|
||||
@@ -238,20 +238,17 @@ fun TextComposer(
|
||||
val sendButton = @Composable {
|
||||
SendButton(
|
||||
canSendMessage = canSendMessage,
|
||||
onClick = onSendClick,
|
||||
composerMode = composerMode,
|
||||
)
|
||||
}
|
||||
val recordVoiceButton = @Composable {
|
||||
VoiceMessageRecorderButton(
|
||||
isRecording = voiceMessageState is VoiceMessageState.Recording,
|
||||
onEvent = onVoiceRecorderEvent,
|
||||
)
|
||||
}
|
||||
val sendVoiceButton = @Composable {
|
||||
SendButton(
|
||||
canSendMessage = voiceMessageState is VoiceMessageState.Preview,
|
||||
onClick = onSendVoiceMessage,
|
||||
composerMode = composerMode,
|
||||
)
|
||||
}
|
||||
@@ -265,6 +262,39 @@ fun TextComposer(
|
||||
@Composable { TextFormatting(state = it.richTextEditorState) }
|
||||
}
|
||||
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
|
||||
fun performHapticFeedback() {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
|
||||
fun endButtonClickStandard() = when {
|
||||
!canSendMessage ->
|
||||
when (voiceMessageState) {
|
||||
VoiceMessageState.Idle -> {
|
||||
performHapticFeedback()
|
||||
onVoiceRecorderEvent.invoke(VoiceMessageRecorderEvent.Start)
|
||||
}
|
||||
is VoiceMessageState.Recording -> {
|
||||
performHapticFeedback()
|
||||
onVoiceRecorderEvent.invoke(VoiceMessageRecorderEvent.Stop)
|
||||
}
|
||||
is VoiceMessageState.Preview -> when (voiceMessageState.isSending) {
|
||||
true -> {
|
||||
// No op
|
||||
}
|
||||
false -> onSendVoiceMessage()
|
||||
}
|
||||
}
|
||||
else -> onSendMessage()
|
||||
}
|
||||
|
||||
fun endButtonClickFormatting() {
|
||||
if (canSendMessage) {
|
||||
onSendMessage()
|
||||
}
|
||||
}
|
||||
|
||||
val sendOrRecordButton = when {
|
||||
!canSendMessage ->
|
||||
when (voiceMessageState) {
|
||||
@@ -330,8 +360,9 @@ fun TextComposer(
|
||||
)
|
||||
},
|
||||
textFormatting = textFormattingOptions,
|
||||
endButtonA11y = endButtonA11y,
|
||||
sendButton = sendButton,
|
||||
endButtonClick = ::endButtonClickFormatting,
|
||||
endButtonA11y = endButtonA11y,
|
||||
)
|
||||
} else {
|
||||
StandardLayout(
|
||||
@@ -341,6 +372,7 @@ fun TextComposer(
|
||||
composerOptionsButton = composerOptionsButton,
|
||||
textInput = textInput,
|
||||
endButton = sendOrRecordButton,
|
||||
endButtonClick = ::endButtonClickStandard,
|
||||
endButtonA11y = endButtonA11y,
|
||||
voiceRecording = voiceRecording,
|
||||
voiceDeleteButton = voiceDeleteButton,
|
||||
@@ -409,6 +441,7 @@ private fun StandardLayout(
|
||||
voiceRecording: @Composable () -> Unit,
|
||||
voiceDeleteButton: @Composable () -> Unit,
|
||||
endButton: @Composable () -> Unit,
|
||||
endButtonClick: () -> Unit,
|
||||
endButtonA11y: (SemanticsPropertyReceiver.() -> Unit),
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -454,12 +487,13 @@ private fun StandardLayout(
|
||||
textInput()
|
||||
}
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
// To avoid loosing keyboard focus, the IconButton has to be defined here and has to be always enabled.
|
||||
IconButton(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp)
|
||||
.size(48.dp)
|
||||
.clearAndSetSemantics(endButtonA11y),
|
||||
contentAlignment = Alignment.Center,
|
||||
onClick = endButtonClick,
|
||||
) {
|
||||
endButton()
|
||||
}
|
||||
@@ -496,6 +530,7 @@ private fun TextFormattingLayout(
|
||||
dismissTextFormattingButton: @Composable () -> Unit,
|
||||
textFormatting: @Composable () -> Unit,
|
||||
sendButton: @Composable () -> Unit,
|
||||
endButtonClick: () -> Unit,
|
||||
endButtonA11y: (SemanticsPropertyReceiver.() -> Unit),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -527,13 +562,16 @@ private fun TextFormattingLayout(
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
textFormatting()
|
||||
}
|
||||
Box(
|
||||
// To avoid loosing keyboard focus, the IconButton has to be defined here and has to be always enabled.
|
||||
IconButton(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = 14.dp,
|
||||
end = 6.dp,
|
||||
)
|
||||
.clearAndSetSemantics(endButtonA11y)
|
||||
.size(48.dp)
|
||||
.clearAndSetSemantics(endButtonA11y),
|
||||
onClick = endButtonClick,
|
||||
) {
|
||||
sendButton()
|
||||
}
|
||||
|
||||
@@ -41,48 +41,40 @@ import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
@Composable
|
||||
internal fun SendButton(
|
||||
canSendMessage: Boolean,
|
||||
onClick: () -> Unit,
|
||||
composerMode: MessageComposerMode,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
IconButton(
|
||||
val iconVector = when {
|
||||
composerMode.isEditing -> CompoundIcons.Check()
|
||||
else -> CompoundIcons.SendSolid()
|
||||
}
|
||||
val iconStartPadding = when {
|
||||
composerMode.isEditing -> 0.dp
|
||||
else -> 2.dp
|
||||
}
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(48.dp),
|
||||
onClick = onClick,
|
||||
enabled = canSendMessage,
|
||||
.clip(CircleShape)
|
||||
.size(36.dp)
|
||||
.buttonBackgroundModifier(canSendMessage)
|
||||
) {
|
||||
val iconVector = when {
|
||||
composerMode.isEditing -> CompoundIcons.Check()
|
||||
else -> CompoundIcons.SendSolid()
|
||||
}
|
||||
val iconStartPadding = when {
|
||||
composerMode.isEditing -> 0.dp
|
||||
else -> 2.dp
|
||||
}
|
||||
Box(
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.size(36.dp)
|
||||
.buttonBackgroundModifier(canSendMessage)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.padding(start = iconStartPadding)
|
||||
.align(Alignment.Center),
|
||||
imageVector = iconVector,
|
||||
// Note: accessibility is managed in TextComposer.
|
||||
contentDescription = null,
|
||||
tint = if (canSendMessage) {
|
||||
if (ElementTheme.colors.isLight) {
|
||||
ElementTheme.colors.iconOnSolidPrimary
|
||||
} else {
|
||||
ElementTheme.colors.iconPrimary
|
||||
}
|
||||
.padding(start = iconStartPadding)
|
||||
.align(Alignment.Center),
|
||||
imageVector = iconVector,
|
||||
// Note: accessibility is managed in TextComposer.
|
||||
contentDescription = null,
|
||||
tint = if (canSendMessage) {
|
||||
if (ElementTheme.colors.isLight) {
|
||||
ElementTheme.colors.iconOnSolidPrimary
|
||||
} else {
|
||||
ElementTheme.colors.iconQuaternary
|
||||
ElementTheme.colors.iconPrimary
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ElementTheme.colors.iconQuaternary
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,9 +109,17 @@ internal fun SendButtonPreview() = ElementPreview {
|
||||
val normalMode = MessageComposerMode.Normal
|
||||
val editMode = MessageComposerMode.Edit(EventId("\$id").toEventOrTransactionId(), "")
|
||||
Row {
|
||||
SendButton(canSendMessage = true, onClick = {}, composerMode = normalMode)
|
||||
SendButton(canSendMessage = false, onClick = {}, composerMode = normalMode)
|
||||
SendButton(canSendMessage = true, onClick = {}, composerMode = editMode)
|
||||
SendButton(canSendMessage = false, onClick = {}, composerMode = editMode)
|
||||
IconButton(onClick = {}) {
|
||||
SendButton(canSendMessage = true, composerMode = normalMode)
|
||||
}
|
||||
IconButton(onClick = {}) {
|
||||
SendButton(canSendMessage = false, composerMode = normalMode)
|
||||
}
|
||||
IconButton(onClick = {}) {
|
||||
SendButton(canSendMessage = true, composerMode = editMode)
|
||||
}
|
||||
IconButton(onClick = {}) {
|
||||
SendButton(canSendMessage = false, composerMode = editMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,8 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
@@ -25,49 +24,25 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
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
|
||||
|
||||
@Composable
|
||||
internal fun VoiceMessageRecorderButton(
|
||||
isRecording: Boolean,
|
||||
onEvent: (VoiceMessageRecorderEvent) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
|
||||
val performHapticFeedback = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
}
|
||||
|
||||
if (isRecording) {
|
||||
StopButton(
|
||||
modifier = modifier,
|
||||
onClick = {
|
||||
performHapticFeedback()
|
||||
onEvent(VoiceMessageRecorderEvent.Stop)
|
||||
}
|
||||
)
|
||||
StopButton(modifier)
|
||||
} else {
|
||||
StartButton(
|
||||
modifier = modifier,
|
||||
onClick = {
|
||||
performHapticFeedback()
|
||||
onEvent(VoiceMessageRecorderEvent.Start)
|
||||
}
|
||||
)
|
||||
StartButton(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StartButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) = IconButton(
|
||||
modifier = modifier.size(48.dp),
|
||||
onClick = onClick,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
modifier = modifier.size(24.dp),
|
||||
imageVector = CompoundIcons.MicOn(),
|
||||
// Note: accessibility is managed in TextComposer.
|
||||
contentDescription = null,
|
||||
@@ -77,41 +52,40 @@ private fun StartButton(
|
||||
|
||||
@Composable
|
||||
private fun StopButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) = IconButton(
|
||||
modifier = modifier
|
||||
.size(48.dp),
|
||||
onClick = onClick,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
modifier
|
||||
.size(36.dp)
|
||||
.background(
|
||||
color = ElementTheme.colors.bgActionPrimaryRest,
|
||||
shape = CircleShape,
|
||||
)
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
resourceId = CommonDrawables.ic_stop,
|
||||
// Note: accessibility is managed in TextComposer.
|
||||
contentDescription = null,
|
||||
tint = ElementTheme.colors.iconOnSolidPrimary,
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
resourceId = CommonDrawables.ic_stop,
|
||||
// Note: accessibility is managed in TextComposer.
|
||||
contentDescription = null,
|
||||
tint = ElementTheme.colors.iconOnSolidPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun VoiceMessageRecorderButtonPreview() = ElementPreview {
|
||||
Row {
|
||||
VoiceMessageRecorderButton(
|
||||
isRecording = false,
|
||||
onEvent = {},
|
||||
)
|
||||
VoiceMessageRecorderButton(
|
||||
isRecording = true,
|
||||
onEvent = {},
|
||||
)
|
||||
IconButton(onClick = {}) {
|
||||
VoiceMessageRecorderButton(
|
||||
isRecording = false,
|
||||
)
|
||||
}
|
||||
IconButton(onClick = {}) {
|
||||
VoiceMessageRecorderButton(
|
||||
isRecording = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user