commit 58c125b817e5630bb1e5e7b4871810deb25fa820
parent 281a36b6f36c84b6408e904efc5e90e60ad921e6
Author: Harrison Oglesby <oglesby.harrison@gmail.com>
Date: Thu, 23 Oct 2025 19:12:47 +0000
Bug 1989460 - Create SettingsSearchResultItem UI r=android-reviewers,android-l10n-reviewers,delphine,petru
Differential Revision: https://phabricator.services.mozilla.com/D265892
Diffstat:
6 files changed, 272 insertions(+), 97 deletions(-)
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchBar.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchBar.kt
@@ -0,0 +1,106 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.settings.settingssearch
+
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import mozilla.components.compose.base.button.IconButton
+import mozilla.components.compose.base.textfield.TextField
+import mozilla.components.lib.state.ext.observeAsComposableState
+import org.mozilla.fenix.R
+import org.mozilla.fenix.theme.FirefoxTheme
+import mozilla.components.ui.icons.R as iconsR
+
+/**
+ * Composable for the settings search bar.
+ *
+ * @param store [SettingsSearchStore] for the screen.
+ * @param onBackClick Invoked when the app bar's back button is clicked.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SettingsSearchBar(
+ store: SettingsSearchStore,
+ onBackClick: () -> Unit,
+) {
+ val state by store.observeAsComposableState { it }
+ var searchQuery by remember { mutableStateOf(state.searchQuery) }
+
+ TopAppBar(
+ title = {
+ TextField(
+ value = searchQuery,
+ onValueChange = { value ->
+ searchQuery = value
+ store.dispatch(SettingsSearchAction.SearchQueryUpdated(value))
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(end = 8.dp),
+ placeholder = stringResource(R.string.settings_search_title),
+ singleLine = true,
+ errorText = stringResource(R.string.settings_search_error_message),
+ trailingIcons = {
+ when (state) {
+ is SettingsSearchState.SearchInProgress,
+ is SettingsSearchState.NoSearchResults,
+ -> {
+ IconButton(
+ onClick = {
+ searchQuery = ""
+ store.dispatch(SettingsSearchAction.SearchQueryUpdated(""))
+ },
+ contentDescription = stringResource(
+ R.string.content_description_settings_search_clear_search,
+ ),
+ ) {
+ Icon(
+ painter = painterResource(iconsR.drawable.mozac_ic_cross_circle_fill_24),
+ contentDescription = null,
+ tint = FirefoxTheme.colors.textPrimary,
+ )
+ }
+ }
+ else -> Unit
+ }
+ },
+ )
+ },
+ navigationIcon = {
+ IconButton(
+ onClick = onBackClick,
+ contentDescription =
+ stringResource(
+ R.string.content_description_settings_search_navigate_back,
+ ),
+ ) {
+ Icon(
+ painter = painterResource(
+ iconsR.drawable.mozac_ic_back_24,
+ ),
+ contentDescription = null,
+ tint = FirefoxTheme.colors.textPrimary,
+ )
+ }
+ },
+ windowInsets = WindowInsets(
+ top = 0.dp,
+ bottom = 0.dp,
+ ),
+ )
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchMiddleware.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchMiddleware.kt
@@ -23,9 +23,14 @@ class SettingsSearchMiddleware(
next: (SettingsSearchAction) -> Unit,
action: SettingsSearchAction,
) {
+ val store = context.store as SettingsSearchStore
when (action) {
is SettingsSearchAction.SearchQueryUpdated -> {
next(action)
+
+ if (action.query.isNotBlank()) {
+ store.dispatch(SettingsSearchAction.NoResultsFound(action.query))
+ }
}
else -> {
next(action)
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchResultItem.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchResultItem.kt
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.settings.settingssearch
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import androidx.compose.ui.unit.dp
+import org.mozilla.fenix.theme.FirefoxTheme
+
+/**
+ * Composable for the settings search result item.
+ *
+ * @param item [SettingsSearchItem] to display.
+ * @param onClick Callback for when the item is clicked.
+ */
+@Composable
+fun SettingsSearchResultItem(
+ item: SettingsSearchItem,
+ onClick: () -> Unit,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = onClick)
+ .padding(16.dp),
+ ) {
+ if (item.breadcrumbs.isNotEmpty()) {
+ Text(
+ text = item.breadcrumbs.joinToString(" > "),
+ style = FirefoxTheme.typography.caption,
+ color = FirefoxTheme.colors.textSecondary,
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+
+ Text(
+ text = item.title,
+ style = FirefoxTheme.typography.subtitle1,
+ color = FirefoxTheme.colors.textPrimary,
+ )
+ if (item.summary.isNotBlank()) {
+ Text(
+ text = item.summary,
+ style = FirefoxTheme.typography.caption,
+ color = FirefoxTheme.colors.textSecondary,
+ modifier = Modifier.padding(top = 4.dp),
+ )
+ } else {
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+ }
+}
+
+private class SettingsSearchResultItemParameterProvider : PreviewParameterProvider<SettingsSearchItem> {
+ override val values: Sequence<SettingsSearchItem>
+ get() = sequenceOf(
+ SettingsSearchItem(
+ title = "Search Engine",
+ summary = "Set your preferred search engine for browsing.",
+ preferenceKey = "search_engine_main",
+ breadcrumbs = listOf("Search", "Default Search Engine"),
+ ),
+ SettingsSearchItem(
+ title = "Advanced Settings",
+ summary = "", // Empty or blank summary
+ preferenceKey = "advanced_stuff",
+ breadcrumbs = listOf("Developer", "Experiments"),
+ ),
+ )
+}
+
+/**
+ * Preview for the Settings Search Result Item.
+ */
+@PreviewLightDark
+@Composable
+private fun SettingsSearchResultItemFullPreview(
+ @PreviewParameter(SettingsSearchResultItemParameterProvider::class) item: SettingsSearchItem,
+) {
+ FirefoxTheme {
+ SettingsSearchResultItem(
+ item = item,
+ onClick = {},
+ )
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchScreen.kt
@@ -5,30 +5,22 @@
package org.mozilla.fenix.settings.settingssearch
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark
-import androidx.compose.ui.unit.dp
-import mozilla.components.compose.base.button.IconButton
-import mozilla.components.compose.base.textfield.TextField
import mozilla.components.lib.state.ext.observeAsComposableState
import org.mozilla.fenix.R
import org.mozilla.fenix.theme.FirefoxTheme
-import mozilla.components.ui.icons.R as iconsR
/**
* Composable for the settings search screen.
@@ -45,103 +37,74 @@ fun SettingsSearchScreen(
Scaffold(
topBar = {
SettingsSearchBar(
- query = state.searchQuery,
- onSearchQueryChanged = {
- store.dispatch(SettingsSearchAction.SearchQueryUpdated(it))
- },
- onClearSearchClicked = {
- store.dispatch(SettingsSearchAction.SearchQueryUpdated(""))
- },
+ store = store,
onBackClick = onBackClick,
)
},
) { paddingValues ->
- Column(
- modifier = Modifier.padding(paddingValues),
- ) {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center,
- ) {
- Text(
- text = stringResource(R.string.settings_search_empty_query_placeholder),
- style = FirefoxTheme.typography.body2,
- color = FirefoxTheme.colors.textSecondary,
+ when (state) {
+ is SettingsSearchState.Default -> {
+ SettingsSearchMessageContent(
+ modifier = Modifier
+ .padding(paddingValues)
+ .fillMaxSize(),
)
}
+ is SettingsSearchState.NoSearchResults -> {
+ SettingsSearchMessageContent(
+ modifier = Modifier
+ .padding(paddingValues)
+ .fillMaxSize(),
+ currentUserQuery = state.searchQuery,
+ )
+ }
+ is SettingsSearchState.SearchInProgress -> {
+ LazyColumn(
+ modifier = Modifier
+ .padding(paddingValues)
+ .fillMaxSize(),
+ ) {
+ items(state.searchResults.size) { index ->
+ if (index != 0) {
+ HorizontalDivider()
+ }
+ SettingsSearchResultItem(
+ item = state.searchResults[index],
+ onClick = {
+ // dispatch navigation click event
+ },
+ )
+ }
+ }
+ }
}
}
}
-/**
- * Composable for the settings search bar.
- *
- * @param query Current search query [String].
- * @param onSearchQueryChanged Callback for when the search query changes.
- * @param onClearSearchClicked Callback for when the clear search button is clicked.
- * @param onBackClick Callback for when the back button is clicked.
- */
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
-private fun SettingsSearchBar(
- query: String,
- onSearchQueryChanged: (String) -> Unit,
- onClearSearchClicked: () -> Unit,
- onBackClick: () -> Unit,
+private fun SettingsSearchMessageContent(
+ modifier: Modifier = Modifier,
+ currentUserQuery: String = "",
) {
- TopAppBar(
- title = {
- TextField(
- value = query,
- onValueChange = { value ->
- onSearchQueryChanged(value)
- },
- modifier = Modifier
- .fillMaxWidth()
- .padding(end = 8.dp),
- placeholder = stringResource(R.string.settings_search_title),
- singleLine = true,
- errorText = stringResource(R.string.settings_search_error_message),
- trailingIcons = {
- if (query.isNotBlank()) {
- IconButton(
- onClick = onClearSearchClicked,
- contentDescription = stringResource(
- R.string.content_description_settings_search_clear_search,
- ),
- ) {
- Icon(
- painter = painterResource(iconsR.drawable.mozac_ic_cross_24),
- contentDescription = null,
- tint = FirefoxTheme.colors.textPrimary,
- )
- }
- }
- },
- )
- },
- navigationIcon = {
- IconButton(
- onClick = onBackClick,
- contentDescription =
- stringResource(
- R.string.content_description_settings_search_navigate_back,
- ),
- ) {
- Icon(
- painter = painterResource(
- iconsR.drawable.mozac_ic_back_24,
- ),
- contentDescription = null,
- tint = FirefoxTheme.colors.textPrimary,
- )
- }
- },
- windowInsets = WindowInsets(
- top = 0.dp,
- bottom = 0.dp,
- ),
- )
+ val displayMessage = if (currentUserQuery.isBlank()) {
+ stringResource(R.string.settings_search_empty_query_placeholder)
+ } else {
+ stringResource(
+ R.string.setttings_search_no_results_found_message,
+ currentUserQuery,
+ )
+ }
+ Box(
+ modifier = modifier,
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = displayMessage,
+ textAlign = TextAlign.Center,
+ style = FirefoxTheme.typography.body2,
+ color = FirefoxTheme.colors.textSecondary,
+ )
+ }
}
/**
diff --git a/mobile/android/fenix/app/src/main/res/values/static_strings.xml b/mobile/android/fenix/app/src/main/res/values/static_strings.xml
@@ -272,4 +272,7 @@
<string name="settings_search_empty_query_placeholder">Search for settings</string>
<!-- Message when error happens with Settings Search -->
<string name="settings_search_error_message">Error occurred.</string>
+ <!-- Message displayed when the search field is not empty but search results is empty.
+ %s will be replaced by user query in search field. -->
+ <string name="setttings_search_no_results_found_message">No results found for %s</string>
</resources>
diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchMiddlewareTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchMiddlewareTest.kt
@@ -48,7 +48,7 @@ class SettingsSearchMiddlewareTest {
store.dispatch(SettingsSearchAction.SearchQueryUpdated(query))
store.waitUntilIdle()
- assert(store.state is SettingsSearchState.SearchInProgress)
+ assert(store.state is SettingsSearchState.NoSearchResults)
assert(store.state.searchQuery == query)
}
}