Skip to content

Commit e311a71

Browse files
bmartyElementBot
andauthored
[a11y] voice message improvements (#5980)
* A11Y: ensure a11y focus is not lost and reset to the back button when the user start playing a pending voice message. * 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. * Cleanup code. This if was not necessary. * Small rework to prepare a bugfix. No behavior / UI change. * Ensure that the keyboard focus and accessibility focus is not lost when deleting a pending voice message. * Update screenshots * Improve code readability. * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
2 parents 05026c8 + 2a3843b commit e311a71

15 files changed

+334
-315
lines changed

libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt

Lines changed: 230 additions & 162 deletions
Large diffs are not rendered by default.

libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt renamed to libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButtonIcon.kt

Lines changed: 41 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -29,60 +29,49 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
2929
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
3030
import io.element.android.libraries.designsystem.theme.components.Icon
3131
import io.element.android.libraries.designsystem.theme.components.IconButton
32-
import io.element.android.libraries.matrix.api.core.EventId
33-
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
34-
import io.element.android.libraries.textcomposer.model.MessageComposerMode
3532

3633
/**
3734
* Send button for the message composer.
3835
* Figma: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=1956-37575&node-type=frame&m=dev
3936
* Temporary Figma : https://www.figma.com/design/Ni6Ii8YKtmXCKYNE90cC67/Timeline-(new)?node-id=2274-39944&m=dev
4037
*/
4138
@Composable
42-
internal fun SendButton(
39+
internal fun SendButtonIcon(
4340
canSendMessage: Boolean,
44-
onClick: () -> Unit,
45-
composerMode: MessageComposerMode,
41+
isEditing: Boolean,
4642
modifier: Modifier = Modifier,
4743
) {
48-
IconButton(
44+
val iconVector = when {
45+
isEditing -> CompoundIcons.Check()
46+
else -> CompoundIcons.SendSolid()
47+
}
48+
val iconStartPadding = when {
49+
isEditing -> 0.dp
50+
else -> 2.dp
51+
}
52+
Box(
4953
modifier = modifier
50-
.size(48.dp),
51-
onClick = onClick,
52-
enabled = canSendMessage,
54+
.clip(CircleShape)
55+
.size(36.dp)
56+
.buttonBackgroundModifier(canSendMessage)
5357
) {
54-
val iconVector = when {
55-
composerMode.isEditing -> CompoundIcons.Check()
56-
else -> CompoundIcons.SendSolid()
57-
}
58-
val iconStartPadding = when {
59-
composerMode.isEditing -> 0.dp
60-
else -> 2.dp
61-
}
62-
Box(
58+
Icon(
6359
modifier = Modifier
64-
.clip(CircleShape)
65-
.size(36.dp)
66-
.buttonBackgroundModifier(canSendMessage)
67-
) {
68-
Icon(
69-
modifier = Modifier
70-
.padding(start = iconStartPadding)
71-
.align(Alignment.Center),
72-
imageVector = iconVector,
73-
// Note: accessibility is managed in TextComposer.
74-
contentDescription = null,
75-
tint = if (canSendMessage) {
76-
if (ElementTheme.colors.isLight) {
77-
ElementTheme.colors.iconOnSolidPrimary
78-
} else {
79-
ElementTheme.colors.iconPrimary
80-
}
60+
.padding(start = iconStartPadding)
61+
.align(Alignment.Center),
62+
imageVector = iconVector,
63+
// Note: accessibility is managed in TextComposer.
64+
contentDescription = null,
65+
tint = if (canSendMessage) {
66+
if (ElementTheme.colors.isLight) {
67+
ElementTheme.colors.iconOnSolidPrimary
8168
} else {
82-
ElementTheme.colors.iconQuaternary
69+
ElementTheme.colors.iconPrimary
8370
}
84-
)
85-
}
71+
} else {
72+
ElementTheme.colors.iconQuaternary
73+
}
74+
)
8675
}
8776
}
8877

@@ -113,13 +102,19 @@ private fun Modifier.buttonBackgroundModifier(
113102

114103
@PreviewsDayNight
115104
@Composable
116-
internal fun SendButtonPreview() = ElementPreview {
117-
val normalMode = MessageComposerMode.Normal
118-
val editMode = MessageComposerMode.Edit(EventId("\$id").toEventOrTransactionId(), "")
105+
internal fun SendButtonIconPreview() = ElementPreview {
119106
Row {
120-
SendButton(canSendMessage = true, onClick = {}, composerMode = normalMode)
121-
SendButton(canSendMessage = false, onClick = {}, composerMode = normalMode)
122-
SendButton(canSendMessage = true, onClick = {}, composerMode = editMode)
123-
SendButton(canSendMessage = false, onClick = {}, composerMode = editMode)
107+
IconButton(onClick = {}) {
108+
SendButtonIcon(canSendMessage = true, isEditing = false)
109+
}
110+
IconButton(onClick = {}) {
111+
SendButtonIcon(canSendMessage = false, isEditing = false)
112+
}
113+
IconButton(onClick = {}) {
114+
SendButtonIcon(canSendMessage = true, isEditing = true)
115+
}
116+
IconButton(onClick = {}) {
117+
SendButtonIcon(canSendMessage = false, isEditing = true)
118+
}
124119
}
125120
}

libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageDeleteButton.kt renamed to libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageDeleteButtonIcon.kt

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,41 +23,35 @@ import io.element.android.libraries.designsystem.theme.components.IconButton
2323
import io.element.android.libraries.ui.strings.CommonStrings
2424

2525
@Composable
26-
fun VoiceMessageDeleteButton(
26+
fun VoiceMessageDeleteButtonIcon(
2727
enabled: Boolean,
28-
onClick: () -> Unit,
2928
modifier: Modifier = Modifier,
3029
) {
31-
IconButton(
32-
modifier = modifier
33-
.size(48.dp),
34-
enabled = enabled,
35-
onClick = onClick,
36-
) {
37-
Icon(
38-
modifier = Modifier.size(24.dp),
39-
imageVector = CompoundIcons.Delete(),
40-
contentDescription = stringResource(CommonStrings.a11y_delete),
41-
tint = if (enabled) {
42-
ElementTheme.colors.iconCriticalPrimary
43-
} else {
44-
ElementTheme.colors.iconDisabled
45-
},
46-
)
47-
}
30+
Icon(
31+
modifier = modifier.size(24.dp),
32+
imageVector = CompoundIcons.Delete(),
33+
contentDescription = stringResource(CommonStrings.a11y_delete),
34+
tint = if (enabled) {
35+
ElementTheme.colors.iconCriticalPrimary
36+
} else {
37+
ElementTheme.colors.iconDisabled
38+
},
39+
)
4840
}
4941

5042
@PreviewsDayNight
5143
@Composable
52-
internal fun VoiceMessageDeleteButtonPreview() = ElementPreview {
44+
internal fun VoiceMessageDeleteButtonIconPreview() = ElementPreview {
5345
Row {
54-
VoiceMessageDeleteButton(
55-
enabled = true,
56-
onClick = {},
57-
)
58-
VoiceMessageDeleteButton(
59-
enabled = false,
60-
onClick = {},
61-
)
46+
IconButton(onClick = {}) {
47+
VoiceMessageDeleteButtonIcon(
48+
enabled = true,
49+
)
50+
}
51+
IconButton(onClick = {}) {
52+
VoiceMessageDeleteButtonIcon(
53+
enabled = false,
54+
)
55+
}
6256
}
6357
}

libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -67,32 +67,20 @@ internal fun VoiceMessagePreview(
6767
.heightIn(26.dp),
6868
verticalAlignment = Alignment.CenterVertically,
6969
) {
70-
if (isPlaying) {
71-
PlayerButton(
72-
type = PlayerButtonType.Pause,
73-
onClick = onPauseClick,
74-
enabled = isInteractive,
75-
)
76-
} else {
77-
PlayerButton(
78-
type = PlayerButtonType.Play,
79-
onClick = onPlayClick,
80-
enabled = isInteractive
81-
)
82-
}
83-
70+
PlayerButton(
71+
type = if (isPlaying) PlayerButtonType.Pause else PlayerButtonType.Play,
72+
onClick = if (isPlaying) onPauseClick else onPlayClick,
73+
enabled = isInteractive,
74+
)
8475
Spacer(modifier = Modifier.width(8.dp))
85-
8676
Text(
8777
text = time.formatShort(),
8878
color = ElementTheme.colors.textSecondary,
8979
style = ElementTheme.typography.fontBodySmMedium,
9080
maxLines = 1,
9181
overflow = TextOverflow.Ellipsis,
9282
)
93-
9483
Spacer(modifier = Modifier.width(12.dp))
95-
9684
WaveformPlaybackView(
9785
modifier = Modifier
9886
.weight(1f)

libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButton.kt renamed to libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButtonIcon.kt

Lines changed: 28 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ import androidx.compose.foundation.layout.Row
1414
import androidx.compose.foundation.layout.size
1515
import androidx.compose.foundation.shape.CircleShape
1616
import androidx.compose.runtime.Composable
17+
import androidx.compose.ui.Alignment
1718
import androidx.compose.ui.Modifier
18-
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
19-
import androidx.compose.ui.platform.LocalHapticFeedback
2019
import androidx.compose.ui.unit.dp
2120
import io.element.android.compound.theme.ElementTheme
2221
import io.element.android.compound.tokens.generated.CompoundIcons
@@ -25,49 +24,25 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
2524
import io.element.android.libraries.designsystem.theme.components.Icon
2625
import io.element.android.libraries.designsystem.theme.components.IconButton
2726
import io.element.android.libraries.designsystem.utils.CommonDrawables
28-
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
2927

3028
@Composable
31-
internal fun VoiceMessageRecorderButton(
29+
internal fun VoiceMessageRecorderButtonIcon(
3230
isRecording: Boolean,
33-
onEvent: (VoiceMessageRecorderEvent) -> Unit,
3431
modifier: Modifier = Modifier,
3532
) {
36-
val hapticFeedback = LocalHapticFeedback.current
37-
38-
val performHapticFeedback = {
39-
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
40-
}
41-
4233
if (isRecording) {
43-
StopButton(
44-
modifier = modifier,
45-
onClick = {
46-
performHapticFeedback()
47-
onEvent(VoiceMessageRecorderEvent.Stop)
48-
}
49-
)
34+
StopButton(modifier)
5035
} else {
51-
StartButton(
52-
modifier = modifier,
53-
onClick = {
54-
performHapticFeedback()
55-
onEvent(VoiceMessageRecorderEvent.Start)
56-
}
57-
)
36+
StartButton(modifier)
5837
}
5938
}
6039

6140
@Composable
6241
private fun StartButton(
63-
onClick: () -> Unit,
6442
modifier: Modifier = Modifier,
65-
) = IconButton(
66-
modifier = modifier.size(48.dp),
67-
onClick = onClick,
6843
) {
6944
Icon(
70-
modifier = Modifier.size(24.dp),
45+
modifier = modifier.size(24.dp),
7146
imageVector = CompoundIcons.MicOn(),
7247
// Note: accessibility is managed in TextComposer.
7348
contentDescription = null,
@@ -77,41 +52,40 @@ private fun StartButton(
7752

7853
@Composable
7954
private fun StopButton(
80-
onClick: () -> Unit,
8155
modifier: Modifier = Modifier,
82-
) = IconButton(
83-
modifier = modifier
84-
.size(48.dp),
85-
onClick = onClick,
8656
) {
8757
Box(
88-
Modifier
58+
modifier
8959
.size(36.dp)
9060
.background(
9161
color = ElementTheme.colors.bgActionPrimaryRest,
9262
shape = CircleShape,
93-
)
94-
)
95-
Icon(
96-
modifier = Modifier.size(24.dp),
97-
resourceId = CommonDrawables.ic_stop,
98-
// Note: accessibility is managed in TextComposer.
99-
contentDescription = null,
100-
tint = ElementTheme.colors.iconOnSolidPrimary,
101-
)
63+
),
64+
contentAlignment = Alignment.Center,
65+
) {
66+
Icon(
67+
modifier = Modifier.size(24.dp),
68+
resourceId = CommonDrawables.ic_stop,
69+
// Note: accessibility is managed in TextComposer.
70+
contentDescription = null,
71+
tint = ElementTheme.colors.iconOnSolidPrimary,
72+
)
73+
}
10274
}
10375

10476
@PreviewsDayNight
10577
@Composable
106-
internal fun VoiceMessageRecorderButtonPreview() = ElementPreview {
78+
internal fun VoiceMessageRecorderButtonIconPreview() = ElementPreview {
10779
Row {
108-
VoiceMessageRecorderButton(
109-
isRecording = false,
110-
onEvent = {},
111-
)
112-
VoiceMessageRecorderButton(
113-
isRecording = true,
114-
onEvent = {},
115-
)
80+
IconButton(onClick = {}) {
81+
VoiceMessageRecorderButtonIcon(
82+
isRecording = false,
83+
)
84+
}
85+
IconButton(onClick = {}) {
86+
VoiceMessageRecorderButtonIcon(
87+
isRecording = true,
88+
)
89+
}
11690
}
11791
}

tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Day_0_en.png renamed to tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButtonIcon_Day_0_en.png

File renamed without changes.

tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Night_0_en.png renamed to tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButtonIcon_Night_0_en.png

File renamed without changes.

tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButton_Day_0_en.png renamed to tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButtonIcon_Day_0_en.png

File renamed without changes.

tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButton_Night_0_en.png renamed to tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButtonIcon_Night_0_en.png

File renamed without changes.

tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageRecorderButton_Day_0_en.png renamed to tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageRecorderButtonIcon_Day_0_en.png

File renamed without changes.

0 commit comments

Comments
 (0)