From f816ef03abe76fdad9c28c621d55719880a29bb4 Mon Sep 17 00:00:00 2001 From: justinzeus Date: Thu, 24 Sep 2026 22:00:49 +0200 Subject: [PATCH] Focus the active profile in the user selector --- .../damontecres/wholphin/ui/setup/UserList.kt | 20 +++-- .../wholphin/ui/setup/UserListFocusTest.kt | 87 +++++++++++++++++++ 2 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 app/src/test/java/com/github/damontecres/wholphin/ui/setup/UserListFocusTest.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/UserList.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/UserList.kt index ddc171c9..4d3ff8d3 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/UserList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/UserList.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add @@ -88,19 +89,24 @@ fun UserList( modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center, ) { - val focusRequester = remember { FocusRequester() } - val firstFocusRequester = remember { FocusRequester() } - if (users.isNotEmpty()) { - LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } + val listState = rememberLazyListState() + val initialFocusRequester = remember { FocusRequester() } + val initialIndex = + users.indexOfFirst { it.user.id == currentUser?.id }.takeIf { it >= 0 } ?: 0 + LaunchedEffect(users, currentUser?.id) { + if (users.isNotEmpty()) { + listState.scrollToItem(initialIndex) + initialFocusRequester.tryRequestFocus() + } } LazyRow( + state = listState, horizontalArrangement = Arrangement.spacedBy(24.dp), // Spacing to accommodate 20% scale contentPadding = PaddingValues(horizontal = 48.dp, vertical = 16.dp), // Increased padding to accommodate 20% scale modifier = Modifier .wrapContentWidth() - .focusRestorer(firstFocusRequester) - .focusRequester(focusRequester), + .focusRestorer(initialFocusRequester), ) { itemsIndexed(users) { index, user -> UserIconCard( @@ -108,7 +114,7 @@ fun UserList( isCurrentUser = user.user.id == currentUser?.id, onClick = { onSwitchUser.invoke(user.user) }, onLongClick = { showDeleteDialog = user }, - modifier = if (index == 0) Modifier.focusRequester(firstFocusRequester) else Modifier, + modifier = if (index == initialIndex) Modifier.focusRequester(initialFocusRequester) else Modifier, ) } // Add User card - always rightmost diff --git a/app/src/test/java/com/github/damontecres/wholphin/ui/setup/UserListFocusTest.kt b/app/src/test/java/com/github/damontecres/wholphin/ui/setup/UserListFocusTest.kt new file mode 100644 index 00000000..a6a23244 --- /dev/null +++ b/app/src/test/java/com/github/damontecres/wholphin/ui/setup/UserListFocusTest.kt @@ -0,0 +1,87 @@ +package com.github.damontecres.wholphin.ui.setup + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.test.assertIsFocused +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performKeyInput +import androidx.compose.ui.test.pressKey +import com.github.damontecres.wholphin.data.model.JellyfinUser +import com.github.damontecres.wholphin.ui.theme.WholphinTheme +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import java.util.UUID + +@Config(sdk = [34]) +@RunWith(RobolectricTestRunner::class) +class UserListFocusTest { + @get:Rule val composeTestRule = createComposeRule() + + private val serverId = UUID.fromString("00000000-0000-0000-0000-000000000010") + private val users = + listOf("Ada", "Bert", "Cleo").mapIndexed { index, name -> + JellyfinUserAndImage( + user = + JellyfinUser( + id = UUID(0, (index + 1).toLong()), + name = name, + serverId = serverId, + accessToken = "token", + ), + imageUrl = null, + known = true, + ) + } + + private fun showUsers(currentUser: JellyfinUser?) { + composeTestRule.setContent { + WholphinTheme { + UserList( + users = users, + currentUser = currentUser, + onSwitchUser = {}, + onAddUser = {}, + onRemoveUser = {}, + onSwitchServer = {}, + modifier = Modifier.fillMaxSize(), + ) + } + } + } + + @Test + fun `opening selector focuses active profile and supports D-pad navigation`() { + showUsers(users[1].user) + + composeTestRule.onNodeWithText("B").assertIsFocused().performKeyInput { + pressKey(Key.DirectionRight) + } + composeTestRule.onNodeWithText("C").assertIsFocused() + } + + @Test + fun `opening selector scrolls to an active profile outside the initial viewport`() { + showUsers(users[2].user) + + composeTestRule.onNodeWithText("C").assertIsFocused() + } + + @Test + fun `opening selector without a listed active profile focuses first profile`() { + showUsers(null) + + composeTestRule.onNodeWithText("A").assertIsFocused() + } + + @Test + fun `opening selector with a removed active profile focuses first profile`() { + showUsers(users[1].user.copy(id = UUID(0, 99))) + + composeTestRule.onNodeWithText("A").assertIsFocused() + } +}