aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-02 22:59:02 +0100
committerDanilo M. <danix@danix.xyz>2026-11-02 22:59:02 +0100
commitf173331c235dd2debac483c078f90e8306075db7 (patch)
tree6799dc2b981aa88a7bdb3d42d1f4827981ddfde4 /app
parentbcd2d061288004020cc437694dbf4218abc52c89 (diff)
downloadHardenedPass-f173331c235dd2debac483c078f90e8306075db7.tar.gz
HardenedPass-f173331c235dd2debac483c078f90e8306075db7.zip
fix(app): stop stray search focus and expose list semantics
Out of touch mode the view system gives its default focus to the first Compose focusable, the search field, which kept it and brought the keyboard back on every return to the list. Each return now clears focus once the window has focus (earlier, the view still holds a stale touch mode and clearing refocuses the field). Search on start runs on the first of those returns only, tracked in a saveable flag, so popping back from a folder or rotating no longer refocuses it. The extended FAB label sits under clearAndSetSemantics in Material 3, so the icon now carries the "New entry" description. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'app')
-rw-r--r--app/src/main/java/app/passwordstore/ui/list/ListScreen.kt36
1 files changed, 30 insertions, 6 deletions
diff --git a/app/src/main/java/app/passwordstore/ui/list/ListScreen.kt b/app/src/main/java/app/passwordstore/ui/list/ListScreen.kt
index fdb05ae9..720cfc0a 100644
--- a/app/src/main/java/app/passwordstore/ui/list/ListScreen.kt
+++ b/app/src/main/java/app/passwordstore/ui/list/ListScreen.kt
@@ -43,16 +43,19 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.LifecycleResumeEffect
import app.passwordstore.R
import app.passwordstore.ui.compose.BreadcrumbBar
import app.passwordstore.ui.compose.EntryRow
@@ -99,6 +102,28 @@ fun ListScreen(
val selecting = selected.isNotEmpty()
val listState = rememberLazyListState()
val pullState = rememberPullToRefreshState()
+ val searchFocus = remember { FocusRequester() }
+ val focusManager = LocalFocusManager.current
+ // Saveable, so it survives this destination leaving composition while a folder is open.
+ var launched by rememberSaveable { mutableStateOf(false) }
+ var returned by remember { mutableStateOf(false) }
+ LifecycleResumeEffect(Unit) {
+ returned = true
+ onPauseOrDispose {}
+ }
+ // Out of touch mode (keys, D-pad) the view system hands its default focus to the first
+ // focusable, the search field, which keeps it in touch mode too and brings the keyboard back
+ // when the list returns from a screen that had it up. So every return drops focus, except the
+ // first, where search on start asks for it. Done once the window has focus: before that the
+ // view still holds the touch mode it last saw, and out of it clearing focus refocuses the field.
+ val windowFocused = LocalWindowInfo.current.isWindowFocused
+ LaunchedEffect(returned, windowFocused) {
+ if (returned && windowFocused) {
+ returned = false
+ if (focusSearch && !launched) searchFocus.requestFocus() else focusManager.clearFocus()
+ launched = true
+ }
+ }
// Filtered results always start at the top, where the best match is.
LaunchedEffect(query) { if (query.isNotEmpty()) listState.scrollToItem(0) }
Scaffold(
@@ -108,7 +133,7 @@ fun ListScreen(
SelectionBar(rows, selected, onClearSelection, onMove, onDelete, onRename, onPin)
} else {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
- SearchField(query, gitOps, onQueryChange, onGitOp, onSettings, focusSearch)
+ SearchField(query, gitOps, onQueryChange, onGitOp, onSettings, searchFocus)
BreadcrumbBar(
crumbsFor(path),
stringResource(R.string.list_store_root),
@@ -120,9 +145,10 @@ fun ListScreen(
},
floatingActionButton = {
if (!selecting)
- ExtendedFloatingActionButton(
+ // M3 clears the label's semantics; the FAB takes its description from the icon.
+ ExtendedFloatingActionButton(
text = { Text(stringResource(R.string.list_new_entry)) },
- icon = { Icon(Icons.Filled.Add, contentDescription = null) },
+ icon = { Icon(Icons.Filled.Add, stringResource(R.string.list_new_entry)) },
onClick = onNew,
)
},
@@ -212,10 +238,8 @@ private fun SearchField(
onQueryChange: (String) -> Unit,
onGitOp: (GitOp) -> Unit,
onSettings: () -> Unit,
- focusSearch: Boolean,
+ focus: FocusRequester,
) {
- val focus = remember { FocusRequester() }
- LaunchedEffect(Unit) { if (focusSearch) focus.requestFocus() }
val focusManager = LocalFocusManager.current
var menu by remember { mutableStateOf(false) }
SearchBar(