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
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:
parent
7d6f041b55
commit
344aa9990d
2 changed files with 100 additions and 7 deletions
|
|
@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentWidth
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
|
@ -88,19 +89,24 @@ fun UserList(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val listState = rememberLazyListState()
|
||||||
val firstFocusRequester = remember { FocusRequester() }
|
val initialFocusRequester = remember { FocusRequester() }
|
||||||
if (users.isNotEmpty()) {
|
val initialIndex =
|
||||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
users.indexOfFirst { it.user.id == currentUser?.id }.takeIf { it >= 0 } ?: 0
|
||||||
|
LaunchedEffect(users, currentUser?.id) {
|
||||||
|
if (users.isNotEmpty()) {
|
||||||
|
listState.scrollToItem(initialIndex)
|
||||||
|
initialFocusRequester.tryRequestFocus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
LazyRow(
|
LazyRow(
|
||||||
|
state = listState,
|
||||||
horizontalArrangement = Arrangement.spacedBy(24.dp), // Spacing to accommodate 20% scale
|
horizontalArrangement = Arrangement.spacedBy(24.dp), // Spacing to accommodate 20% scale
|
||||||
contentPadding = PaddingValues(horizontal = 48.dp, vertical = 16.dp), // Increased padding to accommodate 20% scale
|
contentPadding = PaddingValues(horizontal = 48.dp, vertical = 16.dp), // Increased padding to accommodate 20% scale
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.wrapContentWidth()
|
.wrapContentWidth()
|
||||||
.focusRestorer(firstFocusRequester)
|
.focusRestorer(initialFocusRequester),
|
||||||
.focusRequester(focusRequester),
|
|
||||||
) {
|
) {
|
||||||
itemsIndexed(users) { index, user ->
|
itemsIndexed(users) { index, user ->
|
||||||
UserIconCard(
|
UserIconCard(
|
||||||
|
|
@ -108,7 +114,7 @@ fun UserList(
|
||||||
isCurrentUser = user.user.id == currentUser?.id,
|
isCurrentUser = user.user.id == currentUser?.id,
|
||||||
onClick = { onSwitchUser.invoke(user.user) },
|
onClick = { onSwitchUser.invoke(user.user) },
|
||||||
onLongClick = { showDeleteDialog = 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
|
// Add User card - always rightmost
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue