Bug 1948618 - Refactor the SearchSelector composable to be driven from state r=android-reviewers,gl

SearchSelector will have it's entire UX configured through plain data and will
hoist user interactions through a standardized
> onInteraction: (BrowserToolbarEvent) -> Unit
call.

Differential Revision: https://phabricator.services.mozilla.com/D238984
This commit is contained in:
Mugurell
2025-03-25 19:08:54 +00:00
parent 787b23a23e
commit 7fa78bcec3
10 changed files with 133 additions and 24 deletions

View File

@@ -36,6 +36,7 @@ dependencies {
implementation project(":compose-base")
implementation project(":concept-engine")
implementation project(":concept-menu")
implementation project(":browser-menu2")
implementation project(":browser-state")
implementation project(":feature-session")
implementation project(":lib-state")

View File

@@ -14,12 +14,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import mozilla.components.compose.base.theme.AcornTheme
import mozilla.components.compose.browser.toolbar.concept.Action
import mozilla.components.compose.browser.toolbar.concept.Action.ActionButton
import mozilla.components.compose.browser.toolbar.concept.Action.CustomAction
import mozilla.components.compose.browser.toolbar.concept.Action.DropdownAction
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarInteraction.BrowserToolbarEvent
import mozilla.components.compose.browser.toolbar.ui.SearchSelector
import mozilla.components.ui.icons.R
@@ -27,10 +30,12 @@ import mozilla.components.ui.icons.R
* A container for displaying [Action]s.
*
* @param actions List of [Action]s to display in the container.
* @param onInteraction Callback for handling [BrowserToolbarEvent]s on user interactions.
*/
@Composable
fun ActionContainer(
actions: List<Action>,
onInteraction: (BrowserToolbarEvent) -> Unit,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
for (action in actions) {
@@ -41,6 +46,15 @@ fun ActionContainer(
is CustomAction -> {
action.content()
}
is DropdownAction -> {
SearchSelector(
icon = action.icon,
contentDescription = stringResource(action.contentDescription),
menu = action.menu,
onInteraction = { onInteraction(it) },
)
}
}
}
}
@@ -84,6 +98,7 @@ private fun ActionContainerPreview() {
},
),
),
onInteraction = {},
)
}
}

View File

@@ -23,6 +23,7 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import mozilla.components.compose.base.theme.AcornTheme
import mozilla.components.compose.browser.toolbar.concept.Action
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarInteraction.BrowserToolbarEvent
private val ROUNDED_CORNER_SHAPE = RoundedCornerShape(8.dp)
@@ -42,6 +43,7 @@ private val ROUNDED_CORNER_SHAPE = RoundedCornerShape(8.dp)
* display toolbar (outside of the URL bounding box). Also see:
* [MDN docs](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/user_interface/Browser_action)
* @param onUrlClicked Will be called when the user clicks on the URL.
* @param onInteraction Callback for handling [BrowserToolbarEvent]s on user interactions.
*/
@Composable
fun BrowserDisplayToolbar(
@@ -52,6 +54,7 @@ fun BrowserDisplayToolbar(
pageActions: List<Action> = emptyList(),
browserActions: List<Action> = emptyList(),
onUrlClicked: () -> Unit = {},
onInteraction: (BrowserToolbarEvent) -> Unit = {},
) {
Row(
modifier = Modifier
@@ -60,7 +63,10 @@ fun BrowserDisplayToolbar(
verticalAlignment = Alignment.CenterVertically,
) {
if (navigationActions.isNotEmpty()) {
ActionContainer(actions = navigationActions)
ActionContainer(
actions = navigationActions,
onInteraction = onInteraction,
)
} else {
Spacer(modifier = Modifier.width(8.dp))
}
@@ -86,11 +92,17 @@ fun BrowserDisplayToolbar(
style = textStyle,
)
ActionContainer(actions = pageActions)
ActionContainer(
actions = pageActions,
onInteraction = onInteraction,
)
}
if (browserActions.isNotEmpty()) {
ActionContainer(actions = browserActions)
ActionContainer(
actions = browserActions,
onInteraction = onInteraction,
)
} else {
Spacer(modifier = Modifier.width(8.dp))
}

View File

@@ -31,6 +31,7 @@ import mozilla.components.compose.base.theme.AcornTheme
import mozilla.components.compose.browser.toolbar.concept.Action
import mozilla.components.compose.browser.toolbar.concept.Action.ActionButton
import mozilla.components.compose.browser.toolbar.concept.Action.CustomAction
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarInteraction.BrowserToolbarEvent
import mozilla.components.compose.browser.toolbar.ui.InlineAutocompleteTextField
import mozilla.components.compose.browser.toolbar.ui.SearchSelector
import mozilla.components.ui.icons.R as iconsR
@@ -53,8 +54,10 @@ private val ROUNDED_CORNER_SHAPE = RoundedCornerShape(8.dp)
* parameter of the callback.
* @param onUrlCommitted Will be called when the user has finished editing and wants to initiate
* loading the entered URL. The committed text value comes as a parameter of the callback.
* @param onInteraction Callback for handling [BrowserToolbarEvent]s on user interactions.
*/
@Composable
@Suppress("LongMethod")
fun BrowserEditToolbar(
url: String,
colors: BrowserEditToolbarColors,
@@ -63,6 +66,7 @@ fun BrowserEditToolbar(
editActionsEnd: List<Action> = emptyList(),
onUrlEdit: (String) -> Unit = {},
onUrlCommitted: (String) -> Unit = {},
onInteraction: (BrowserToolbarEvent) -> Unit,
) {
Row(
modifier = Modifier
@@ -98,11 +102,17 @@ fun BrowserEditToolbar(
modifier = Modifier.fillMaxWidth(),
shape = ROUNDED_CORNER_SHAPE,
leadingIcon = {
ActionContainer(actions = editActionsStart)
ActionContainer(
actions = editActionsStart,
onInteraction = onInteraction,
)
},
trailingIcon = {
Row(verticalAlignment = Alignment.CenterVertically) {
ActionContainer(actions = editActionsEnd)
ActionContainer(
actions = editActionsEnd,
onInteraction = onInteraction,
)
if (url.isNotEmpty()) {
ClearButton(
@@ -114,7 +124,10 @@ fun BrowserEditToolbar(
},
)
} else {
ActionContainer(actions = editActionsStart)
ActionContainer(
actions = editActionsStart,
onInteraction = onInteraction,
)
InlineAutocompleteTextField(
url = url,
@@ -124,7 +137,10 @@ fun BrowserEditToolbar(
onUrlCommitted = onUrlCommitted,
)
ActionContainer(actions = editActionsEnd)
ActionContainer(
actions = editActionsEnd,
onInteraction = onInteraction,
)
if (url.isNotEmpty()) {
ClearButton(
@@ -192,6 +208,7 @@ private fun BrowserEditToolbarPreview() {
onClick = {},
),
),
onInteraction = {},
)
}
}

View File

@@ -62,6 +62,7 @@ fun BrowserToolbar(
editActionsEnd = uiState.editState.editActionsEnd,
onUrlCommitted = { text -> onTextCommit(text) },
onUrlEdit = { text -> onTextEdit(text) },
onInteraction = { store.dispatch(it) },
)
}
@@ -75,6 +76,7 @@ fun BrowserToolbar(
onUrlClicked = {
onDisplayToolbarClick()
},
onInteraction = { store.dispatch(it) },
)
}

View File

@@ -52,7 +52,10 @@ fun CustomTabToolbar(
.background(color = colors.background),
verticalAlignment = Alignment.CenterVertically,
) {
ActionContainer(actions = navigationActions)
ActionContainer(
actions = navigationActions,
onInteraction = {},
)
Column(
modifier = Modifier.weight(1f),
@@ -77,9 +80,15 @@ fun CustomTabToolbar(
}
}
ActionContainer(actions = pageActions)
ActionContainer(
actions = pageActions,
onInteraction = {},
)
ActionContainer(actions = browserActions)
ActionContainer(
actions = browserActions,
onInteraction = {},
)
}
}

View File

@@ -4,9 +4,12 @@
package mozilla.components.compose.browser.toolbar.concept
import android.graphics.drawable.Drawable
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarInteraction.BrowserToolbarMenu
/**
* Actions that can be added to the toolbar.
@@ -36,4 +39,19 @@ sealed class Action {
data class CustomAction(
val content: @Composable () -> Unit,
) : Action()
/**
* An action button styled as a dropdown button to be added to the toolbar.
* This wraps the provided [icon] at the start with a down arrow to it's right to indicate that
* clicking this will open a dropdown menu.
*
* @property icon The icon for this button.
* @property contentDescription The content description for this button.
* @property menu The [BrowserToolbarMenu] to show when this button is clicked.
*/
data class DropdownAction(
val icon: Drawable,
@StringRes val contentDescription: Int,
val menu: BrowserToolbarMenu,
) : Action()
}

View File

@@ -35,8 +35,8 @@ sealed interface BrowserToolbarInteraction {
/**
* Item which can be shown in a [BrowserToolbarMenu].
*
* @property icon Optional icon to show in the popup item.
* @property text Optional text to show in the popup item.
* @property icon Optional icon for the menu item.
* @property text Optional text for the menu item.
* @property contentDescription Content description for this item. `null` if not important for accessibility.
* @property onClick Optional [BrowserToolbarEvent] to be dispatched when this item is clicked.
*/

View File

@@ -23,6 +23,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -32,14 +33,20 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import mozilla.components.browser.menu2.BrowserMenuController
import mozilla.components.compose.base.theme.AcornTheme
import mozilla.components.compose.browser.toolbar.R
import mozilla.components.compose.browser.toolbar.databinding.SearchSelectorBinding
import mozilla.components.concept.menu.MenuController
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarInteraction.BrowserToolbarEvent
import mozilla.components.compose.browser.toolbar.store.BrowserToolbarInteraction.BrowserToolbarMenu
import mozilla.components.concept.menu.candidate.DecorativeTextMenuCandidate
import mozilla.components.concept.menu.candidate.DrawableMenuIcon
import mozilla.components.concept.menu.candidate.TextMenuCandidate
import mozilla.components.ui.icons.R as iconsR
/**
@@ -97,25 +104,49 @@ fun SearchSelector(
/**
* Search selector toolbar action.
*
* @param onClick Invoked when the search selector is clicked.
* @param menu [MenuController] that will be used to create a menu when the search selector is
* clicked.
* @param icon [Drawable] to display in the search selector.
* @param contentDescription The content description to use.
* @param menu The [BrowserToolbarMenu] to show when the search selector is clicked.
* @param onInteraction Invoked for user interactions with the menu items.
*/
@Composable
fun SearchSelector(
onClick: () -> Unit,
menu: MenuController? = null,
icon: Drawable? = null,
contentDescription: String? = null,
icon: Drawable,
contentDescription: String,
menu: BrowserToolbarMenu,
onInteraction: (BrowserToolbarEvent) -> Unit,
) {
val items = menu.items().mapNotNull {
if (it.icon == null && it.text != null) {
DecorativeTextMenuCandidate(
text = stringResource(it.text),
)
} else if (it.icon != null && it.text != null) {
TextMenuCandidate(
text = stringResource(it.text),
start = DrawableMenuIcon(
drawable = it.icon,
),
onClick = {
it.onClick?.let { onInteraction(it) }
},
)
} else {
null
}
}
val selector = remember(items) {
BrowserMenuController().apply {
submitList(items)
}
}
AndroidView(
factory = { context ->
SearchSelector(context).apply {
setOnClickListener {
onClick()
menu?.show(anchor = it)
selector.show(anchor = it)
}
setIcon(icon, contentDescription)
@@ -150,8 +181,10 @@ private fun SearchSelectorPreview() {
)
SearchSelector(
onClick = {},
icon = getDrawable(LocalContext.current, iconsR.drawable.mozac_ic_search_24),
icon = getDrawable(LocalContext.current, iconsR.drawable.mozac_ic_search_24)!!,
contentDescription = "Test",
menu = { emptyList() },
onInteraction = {},
)
}
}

View File

@@ -54,6 +54,7 @@ fun BrowserToolbar(
editActionsEnd = uiState.editState.editActionsEnd,
onUrlCommitted = { text -> onTextCommit(text) },
onUrlEdit = { text -> onTextEdit(text) },
onInteraction = {},
)
} else {
BrowserDisplayToolbar(
@@ -77,6 +78,7 @@ fun BrowserToolbar(
editActionsEnd = uiState.editState.editActionsEnd,
onUrlCommitted = { text -> onTextCommit(text) },
onUrlEdit = { text -> onTextEdit(text) },
onInteraction = {},
)
}