From f173331c235dd2debac483c078f90e8306075db7 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 2 Nov 2026 22:59:02 +0100 Subject: 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 --- .../java/app/passwordstore/ui/list/ListScreen.kt | 36 ++++++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) (limited to 'app/src/main/java') 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( -- cgit v1.2.3