Focus the active profile in the user selector
All checks were successful
review/firstmate Diff byte-identical to Justin's upstream commit f816ef03 (Wholphin#2003); worker ran make test + apk-debug green

This commit is contained in:
justinzeus 2026-09-24 22:00:49 +02:00
parent 7d6f041b55
commit 344aa9990d
2 changed files with 100 additions and 7 deletions

View file

@ -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

View file

@ -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()
}
}