Skip to content

Commit 522503c

Browse files
committed
fix: keyboard navigation in conversations UI
1 parent e32ba54 commit 522503c

15 files changed

Lines changed: 646 additions & 123 deletions

File tree

app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt

Lines changed: 62 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ import androidx.compose.runtime.remember
4949
import androidx.compose.runtime.rememberCoroutineScope
5050
import androidx.compose.runtime.setValue
5151
import androidx.compose.ui.Modifier
52+
import androidx.compose.ui.focus.FocusRequester
53+
import androidx.compose.ui.focus.focusProperties
54+
import androidx.compose.ui.focus.focusRequester
5255
import androidx.compose.ui.graphics.ColorFilter
5356
import androidx.compose.ui.graphics.RectangleShape
5457
import androidx.compose.ui.layout.ContentScale
@@ -265,6 +268,8 @@ fun HomeContent(
265268
modifier: Modifier = Modifier,
266269
) {
267270
val context = LocalContext.current
271+
val searchFocusRequester = remember { FocusRequester() }
272+
val fabFocusRequester = remember { FocusRequester() }
268273

269274
with(homeStateHolder) {
270275
fun openWireHomeDestination(item: HomeDestination) {
@@ -329,6 +334,8 @@ fun HomeContent(
329334
onOpenConversationFilter = {
330335
homeStateHolder.conversationsFilterBottomSheetState.show(Unit)
331336
},
337+
searchFocusRequester = searchFocusRequester,
338+
fabFocusRequester = if (currentNavigationItem.fab != null) fabFocusRequester else null,
332339
)
333340
}
334341
},
@@ -343,7 +350,13 @@ fun HomeContent(
343350
isSearchActive = searchBarState.isSearchActive,
344351
searchBarHint = stringResource(searchBar.hint),
345352
searchQueryTextState = searchBarState.searchQueryTextState,
346-
onActiveChanged = searchBarState::searchActiveChanged,
353+
onCloseSearchClicked = searchBarState::closeSearch,
354+
onActiveChanged = { isFocused ->
355+
if (isFocused) {
356+
searchBarState.openSearch()
357+
}
358+
},
359+
externalFocusRequester = searchFocusRequester,
347360
)
348361
}
349362
}
@@ -388,30 +401,12 @@ fun HomeContent(
388401
enter = scaleIn(),
389402
exit = scaleOut(),
390403
) {
391-
var currentFab by remember { mutableStateOf(currentNavigationItem.fab ?: FabOptions.NewConversation) }
392-
// to keep the fab during the exit animation, we need to keep last known (non-null) fab data
393-
if (currentNavigationItem.fab != null) currentFab = currentNavigationItem.fab!!
394-
395-
FloatingActionButton(
396-
text = stringResource(currentFab.text),
397-
icon = {
398-
Image(
399-
painter = painterResource(currentFab.icon),
400-
contentDescription = stringResource(currentFab.contentDescription),
401-
contentScale = ContentScale.FillBounds,
402-
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimary),
403-
modifier = Modifier
404-
.padding(start = dimensions().spacing4x, top = dimensions().spacing2x)
405-
.size(dimensions().fabIconSize)
406-
)
407-
},
408-
onClick = {
409-
when (currentNavigationItem.fab) {
410-
FabOptions.NewConversation -> onNewConversationClick()
411-
FabOptions.NewMeeting -> homeStateHolder.newMeetingBottomSheetState.show(Unit)
412-
else -> { /* no-op */ }
413-
}
414-
}
404+
HomeFloatingActionButton(
405+
currentNavigationItem = currentNavigationItem,
406+
fabFocusRequester = fabFocusRequester,
407+
searchFocusRequester = searchFocusRequester,
408+
onNewConversationClick = onNewConversationClick,
409+
onNewMeetingClick = { homeStateHolder.newMeetingBottomSheetState.show(Unit) }
415410
)
416411
}
417412
}
@@ -420,3 +415,45 @@ fun HomeContent(
420415
)
421416
}
422417
}
418+
419+
@Composable
420+
private fun HomeFloatingActionButton(
421+
currentNavigationItem: HomeDestination,
422+
fabFocusRequester: FocusRequester,
423+
searchFocusRequester: FocusRequester,
424+
onNewConversationClick: () -> Unit,
425+
onNewMeetingClick: () -> Unit,
426+
) {
427+
var currentFab by remember { mutableStateOf(currentNavigationItem.fab ?: FabOptions.NewConversation) }
428+
// to keep the fab during the exit animation, we need to keep last known (non-null) fab data
429+
currentNavigationItem.fab?.let { currentFab = it }
430+
431+
FloatingActionButton(
432+
text = stringResource(currentFab.text),
433+
modifier = Modifier
434+
.focusRequester(fabFocusRequester)
435+
.focusProperties {
436+
if (currentNavigationItem.searchBar != null) {
437+
next = searchFocusRequester
438+
}
439+
},
440+
icon = {
441+
Image(
442+
painter = painterResource(currentFab.icon),
443+
contentDescription = stringResource(currentFab.contentDescription),
444+
contentScale = ContentScale.FillBounds,
445+
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimary),
446+
modifier = Modifier
447+
.padding(start = dimensions().spacing4x, top = dimensions().spacing2x)
448+
.size(dimensions().fabIconSize)
449+
)
450+
},
451+
onClick = {
452+
when (currentNavigationItem.fab) {
453+
FabOptions.NewConversation -> onNewConversationClick()
454+
FabOptions.NewMeeting -> onNewMeetingClick()
455+
else -> { /* no-op */ }
456+
}
457+
}
458+
)
459+
}

app/src/main/kotlin/com/wire/android/ui/home/HomeTopBar.kt

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ package com.wire.android.ui.home
2020

2121
import androidx.compose.runtime.Composable
2222
import androidx.compose.runtime.remember
23+
import androidx.compose.ui.Modifier
24+
import androidx.compose.ui.focus.FocusRequester
25+
import androidx.compose.ui.focus.focusProperties
2326
import androidx.compose.ui.res.stringResource
2427
import androidx.compose.ui.unit.Dp
2528
import androidx.compose.ui.unit.dp
@@ -52,9 +55,13 @@ fun HomeTopBar(
5255
onHamburgerMenuClick: () -> Unit,
5356
onNavigateToSelfUserProfile: () -> Unit,
5457
onOpenConversationFilter: () -> Unit,
58+
modifier: Modifier = Modifier,
59+
searchFocusRequester: FocusRequester? = null,
60+
fabFocusRequester: FocusRequester? = null,
5561
) {
5662
WireCenterAlignedTopAppBar(
5763
title = title,
64+
modifier = modifier,
5865
onNavigationPressed = onHamburgerMenuClick,
5966
navigationIconType = NavigationIconType.Menu,
6067
actions = {
@@ -82,11 +89,19 @@ fun HomeTopBar(
8289
}
8390
UserProfileAvatar(
8491
avatarData = userAvatarData,
85-
clickable = remember {
92+
modifier = Modifier
93+
.focusProperties {
94+
when {
95+
fabFocusRequester != null -> next = fabFocusRequester
96+
searchFocusRequester != null -> next = searchFocusRequester
97+
}
98+
},
99+
clickable = remember(openLabel, onNavigateToSelfUserProfile) {
86100
Clickable(
87101
enabled = true,
88-
onClickDescription = openLabel
89-
) { onNavigateToSelfUserProfile() }
102+
onClickDescription = openLabel,
103+
onClick = onNavigateToSelfUserProfile
104+
)
90105
},
91106
type = UserProfileAvatarType.WithIndicators.RegularUser(
92107
legalHoldIndicatorVisible = withLegalHoldIndicator

app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchUsersAndAppsScreen.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,12 @@ fun SearchUsersAndAppsScreen(
148148
backIconContentDescription = stringResource(id = R.string.content_description_add_participants_back_btn),
149149
searchBarDescription = stringResource(R.string.content_description_add_participants_search_field),
150150
searchQueryTextState = searchBarState.searchQueryTextState,
151-
onActiveChanged = searchBarState::searchActiveChanged,
151+
onCloseSearchClicked = searchBarState::closeSearch,
152+
onActiveChanged = { isFocused ->
153+
if (isFocused) {
154+
searchBarState.openSearch()
155+
}
156+
},
152157
)
153158
},
154159
topBarFooter = {

app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/AdditionalOptions.kt

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ import androidx.compose.material3.HorizontalDivider
2727
import androidx.compose.material3.MaterialTheme
2828
import androidx.compose.runtime.Composable
2929
import androidx.compose.ui.Modifier
30+
import androidx.compose.ui.focus.FocusRequester
3031
import com.wire.android.ui.common.colorsScheme
32+
import com.wire.android.ui.common.onEscapeOrBackKey
3133
import com.wire.android.ui.home.conversations.ConversationActionPermissionType
3234
import com.wire.android.ui.home.conversations.model.UriAsset
3335
import com.wire.android.ui.home.messagecomposer.recordaudio.RecordAudioComponent
@@ -56,10 +58,18 @@ fun AdditionalOptionsMenu(
5658
onRichOptionButtonClicked: (RichTextMarkdown) -> Unit,
5759
onDrawingModeClicked: () -> Unit,
5860
modifier: Modifier = Modifier,
61+
initialKeyboardFocusRequester: FocusRequester? = null,
5962
onOnSelfDeletingOptionClicked: ((SelfDeletionTimer) -> Unit)? = null,
6063
onGifOptionClicked: (() -> Unit)? = null
6164
) {
62-
Box(modifier.background(colorsScheme().surface)) {
65+
Box(
66+
modifier
67+
.onEscapeOrBackKey(
68+
enabled = additionalOptionsState == AdditionalOptionMenuState.RichTextEditing,
69+
onKeyPressed = onCloseRichEditingButtonClicked
70+
)
71+
.background(colorsScheme().surface)
72+
) {
6373
when (additionalOptionsState) {
6474
AdditionalOptionMenuState.AttachmentAndAdditionalOptionsMenu -> {
6575
AttachmentAndAdditionalOptionsMenuItems(
@@ -75,7 +85,8 @@ fun AdditionalOptionsMenu(
7585
onRichEditingButtonClicked = onRichEditingButtonClicked,
7686
onPingClicked = onPingOptionClicked,
7787
onDrawingModeClicked = onDrawingModeClicked,
78-
isFileSharingEnabled = isFileSharingEnabled
88+
isFileSharingEnabled = isFileSharingEnabled,
89+
initialKeyboardFocusRequester = initialKeyboardFocusRequester
7990
)
8091
}
8192

@@ -146,7 +157,8 @@ fun AttachmentAndAdditionalOptionsMenuItems(
146157
onPingClicked: () -> Unit = {},
147158
onGifButtonClicked: () -> Unit = {},
148159
onRichEditingButtonClicked: () -> Unit = {},
149-
onDrawingModeClicked: () -> Unit = {}
160+
onDrawingModeClicked: () -> Unit = {},
161+
initialKeyboardFocusRequester: FocusRequester? = null
150162
) {
151163
Column(modifier.wrapContentSize()) {
152164
HorizontalDivider(color = MaterialTheme.wireColorScheme.outline)
@@ -163,7 +175,8 @@ fun AttachmentAndAdditionalOptionsMenuItems(
163175
onGifButtonClicked = onGifButtonClicked,
164176
onRichEditingButtonClicked = onRichEditingButtonClicked,
165177
onDrawingModeClicked = onDrawingModeClicked,
166-
isFileSharingEnabled = isFileSharingEnabled
178+
isFileSharingEnabled = isFileSharingEnabled,
179+
initialKeyboardFocusRequester = initialKeyboardFocusRequester
167180
)
168181
}
169182
}

app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/AttachmentOptions.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import androidx.compose.runtime.setValue
4242
import androidx.compose.ui.Alignment
4343
import androidx.compose.ui.Modifier
4444
import androidx.compose.ui.draw.scale
45+
import androidx.compose.ui.focus.FocusRequester
46+
import androidx.compose.ui.focus.focusRequester
4547
import androidx.compose.ui.platform.LocalDensity
4648
import androidx.compose.ui.res.stringResource
4749
import androidx.compose.ui.text.rememberTextMeasurer
@@ -77,6 +79,7 @@ fun AttachmentOptionsComponent(
7779
) {
7880
val density = LocalDensity.current
7981
val textMeasurer = rememberTextMeasurer()
82+
val firstOptionFocusRequester = remember { FocusRequester() }
8083

8184
val attachmentOptions = buildAttachmentOptionItems(
8285
isFileSharingEnabled = isFileSharingEnabled,
@@ -123,6 +126,12 @@ fun AttachmentOptionsComponent(
123126
val (columns, contentPadding) = params
124127
val numberOfColumns = (fullWidth / minColumnWidth).toInt()
125128

129+
LaunchedEffect(optionsVisible, visibleAttachmentOptions.size) {
130+
if (optionsVisible && visibleAttachmentOptions.isNotEmpty()) {
131+
firstOptionFocusRequester.requestFocus()
132+
}
133+
}
134+
126135
LazyVerticalGrid(
127136
columns = columns,
128137
modifier = Modifier.fillMaxSize(),
@@ -162,7 +171,15 @@ fun AttachmentOptionsComponent(
162171
AttachmentButton(
163172
icon = option.icon,
164173
labelStyle = labelStyle,
165-
modifier = Modifier.scale(animatedScale),
174+
modifier = Modifier
175+
.scale(animatedScale)
176+
.then(
177+
if (index == 0) {
178+
Modifier.focusRequester(firstOptionFocusRequester)
179+
} else {
180+
Modifier
181+
}
182+
),
166183
text = stringResource(option.text)
167184
) { option.onClick() }
168185
}

0 commit comments

Comments
 (0)