commit bec8462a0ab530659aa068e76369fcac21e6a470
parent 8b5aaf5f7a084a25fd8fd1f7731b89f7922bb35c
Author: triesap <tyson@radroots.org>
Date: Tue, 4 Aug 2026 00:03:54 +0000
ui: harden account surface accessibility
- remove nested clickable account row controls
- add explicit semantic selection and activation actions
- focus masked import input deterministically
- keep long profiles recoveries and account registries scrollable
Diffstat:
2 files changed, 42 insertions(+), 3 deletions(-)
diff --git a/app/desktop/src/main/kotlin/org/radroots/studio/accounts/ui/StudioScreen.kt b/app/desktop/src/main/kotlin/org/radroots/studio/accounts/ui/StudioScreen.kt
@@ -10,10 +10,16 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.Role
@@ -61,6 +67,7 @@ fun StartupFailureScreen(problem: String) {
.fillMaxSize()
.background(WindowBackgroundColor)
.padding(24.dp)
+ .verticalScroll(rememberScrollState())
.testTag("startup-failure"),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
@@ -144,6 +151,7 @@ private fun ActiveAccountHome(
.fillMaxSize()
.background(WindowBackgroundColor)
.padding(24.dp)
+ .verticalScroll(rememberScrollState())
.testTag("home-screen"),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
@@ -153,7 +161,7 @@ private fun ActiveAccountHome(
BasicText(active.account.publicKeyHex, Modifier.testTag("active-pubkey-hex"))
BasicText("Name: ${active.profile.name}", Modifier.testTag("active-profile-name"))
BasicText("Display name: ${active.profile.displayName}")
- BasicText("NIP-05: ${active.profile.nip05}")
+ BasicText("NIP-05 (unverified): ${active.profile.nip05}")
BasicText("About: ${active.profile.about}", Modifier.testTag("active-profile-about"))
BasicText("Picture: ${active.profile.picture}")
BasicText("Relay: ${active.relayState}", Modifier.testTag("relay-state"))
@@ -284,6 +292,8 @@ private fun AccountEntry(model: StudioUiModel, actions: StudioUiActions) {
)
}
AccountEntryMode.IMPORT -> {
+ val importFocusRequester = remember { FocusRequester() }
+ LaunchedEffect(Unit) { importFocusRequester.requestFocus() }
TextAction(
text = "Back",
testTag = "cancel-account-entry",
@@ -302,6 +312,7 @@ private fun AccountEntry(model: StudioUiModel, actions: StudioUiActions) {
contentDescription = "Nostr secret key"
password()
}
+ .focusRequester(importFocusRequester)
.testTag("import-nsec-input")
.background(InputBackgroundColor)
.padding(8.dp),
@@ -342,7 +353,6 @@ private fun ColumnScope.SavedAccountList(
.fillMaxWidth()
.semantics { selected = account.selected }
.testTag("account-row:${account.publicKeyHex}")
- .clickable { actions.selectAccount(account.publicKeyHex) }
.background(InputBackgroundColor)
.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
@@ -353,6 +363,13 @@ private fun ColumnScope.SavedAccountList(
if (account.selected) BasicText("Selected")
if (account.active) BasicText("Active")
TextAction(
+ text = if (account.selected) "Selected account" else "Select",
+ testTag = "select-account:${account.publicKeyHex}",
+ contentDescription = "Select ${account.label}",
+ enabled = !model.busy && !account.selected,
+ onClick = { actions.selectAccount(account.publicKeyHex) },
+ )
+ TextAction(
text = if (account.active) "Active account" else "Activate",
testTag = "activate-account:${account.publicKeyHex}",
contentDescription = "Activate ${account.label}",
@@ -403,6 +420,7 @@ private fun GeneratedKeyRecoveryScreen(
.fillMaxSize()
.background(WindowBackgroundColor)
.padding(24.dp)
+ .verticalScroll(rememberScrollState())
.testTag("generated-key-backup"),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
diff --git a/app/desktop/src/test/kotlin/org/radroots/studio/accounts/ui/StudioScreenTest.kt b/app/desktop/src/test/kotlin/org/radroots/studio/accounts/ui/StudioScreenTest.kt
@@ -7,12 +7,15 @@ import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
+import androidx.compose.ui.test.performScrollToNode
+import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.v2.runComposeUiTest
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -81,6 +84,7 @@ class StudioScreenTest {
onNodeWithTag("generate-key").performClick()
onNodeWithTag("cancel-account-entry").performClick()
onNodeWithTag("choose-import-account").performClick()
+ onNodeWithTag("import-nsec-input").assertIsFocused()
onNodeWithTag("import-nsec-input").performTextInput("nsec1secret")
onNodeWithTag("import-key").performClick()
@@ -185,7 +189,7 @@ class StudioScreenTest {
onNodeWithTag("saved-account-list").assertIsDisplayed()
onNodeWithTag("account-row:${first.publicKeyHex}").assertIsSelected()
- onNodeWithTag("account-row:${second.publicKeyHex}").performClick()
+ onNodeWithTag("select-account:${second.publicKeyHex}", useUnmergedTree = true).performClick()
onNodeWithTag("activate-account:${second.publicKeyHex}", useUnmergedTree = true).performClick()
assertEquals(listOf(second.publicKeyHex), selected)
assertEquals(listOf(second.publicKeyHex), activated)
@@ -201,6 +205,23 @@ class StudioScreenTest {
}
@Test
+ fun savedAccountListRemainsReachableForLargeRegistries() = runComposeUiTest {
+ val accounts = (0 until 100).map { index ->
+ accountUi(index.toString(16).padStart(64, '0'), selected = index == 0)
+ }
+ setContent {
+ StudioScreen(
+ model = emptyUiModel().copy(accounts = accounts),
+ actions = StudioUiActions(),
+ )
+ }
+
+ val lastTag = "account-row:${accounts.last().publicKeyHex}"
+ onNodeWithTag("saved-account-list").performScrollToNode(hasTestTag(lastTag))
+ onNodeWithTag(lastTag).assertIsDisplayed()
+ }
+
+ @Test
fun activeHomeShowsIdentityProfileRelayAndCommands() = runComposeUiTest {
var refreshCalls = 0
var signOutCalls = 0