commit 92a3feb1ba75906ee6e9d983c41095e9bd8e6c5d
parent be874bd538677f66490799ba25d110ebce8a37cf
Author: Noah Bond <nbond@mozilla.com>
Date: Fri, 21 Nov 2025 03:14:45 +0000
Bug 2001465 - Replace the Tab Manager's bottom app bar with a custom Floating Toolbar r=android-reviewers,calu
The Floating Toolbar is a convention from M3 Expressive, but the API is not available yet, so we coded our own for this instance.
Differential Revision: https://phabricator.services.mozilla.com/D273512
Diffstat:
4 files changed, 424 insertions(+), 451 deletions(-)
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/bottomappbar/TabManagerBottomAppBar.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/bottomappbar/TabManagerBottomAppBar.kt
@@ -1,221 +0,0 @@
-/* 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/. */
-
-@file:OptIn(ExperimentalMaterial3Api::class)
-
-package org.mozilla.fenix.tabstray.ui.bottomappbar
-
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.BottomAppBar
-import androidx.compose.material3.BottomAppBarDefaults
-import androidx.compose.material3.BottomAppBarScrollBehavior
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-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.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.PreviewLightDark
-import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import mozilla.components.compose.base.menu.DropdownMenu
-import mozilla.components.compose.base.menu.MenuItem
-import mozilla.components.compose.base.text.Text
-import mozilla.components.lib.state.ext.observeAsState
-import org.mozilla.fenix.R
-import org.mozilla.fenix.tabstray.Page
-import org.mozilla.fenix.tabstray.TabsTrayAction
-import org.mozilla.fenix.tabstray.TabsTrayState
-import org.mozilla.fenix.tabstray.TabsTrayState.Mode
-import org.mozilla.fenix.tabstray.TabsTrayStore
-import org.mozilla.fenix.tabstray.TabsTrayTestTag
-import org.mozilla.fenix.theme.FirefoxTheme
-import mozilla.components.ui.icons.R as iconsR
-
-/**
- * [BottomAppBar] for the Tab Manager.
- *
- * @param tabsTrayStore [TabsTrayStore] used to listen for changes to [TabsTrayState].
- * @param scrollBehavior Defines how the [BottomAppBar] should behave when the content under it is scrolled.
- * @param onTabSettingsClick Invoked when the user clicks on the tab settings banner menu item.
- * @param onRecentlyClosedClick Invoked when the user clicks on the recently closed banner menu item.
- * @param onAccountSettingsClick Invoked when the user clicks on the account settings banner menu item.
- * @param onDeleteAllTabsClick Invoked when the user clicks on the close all tabs banner menu item.
- * @param modifier The [Modifier] to be applied to this FAB.
- * @param pbmLocked Whether the private browsing mode is currently locked.
- */
-@Composable
-internal fun TabManagerBottomAppBar(
- tabsTrayStore: TabsTrayStore,
- scrollBehavior: BottomAppBarScrollBehavior,
- onTabSettingsClick: () -> Unit,
- onRecentlyClosedClick: () -> Unit,
- onAccountSettingsClick: () -> Unit,
- onDeleteAllTabsClick: () -> Unit,
- modifier: Modifier = Modifier,
- pbmLocked: Boolean = false,
-) {
- val state by tabsTrayStore.observeAsState(initialValue = tabsTrayStore.state) { it }
- val privateTabsLocked = pbmLocked && state.selectedPage == Page.PrivateTabs
-
- AnimatedVisibility(
- visible = state.mode is Mode.Normal && !privateTabsLocked,
- ) {
- val menuItems = generateMenuItems(
- selectedPage = state.selectedPage,
- normalTabCount = state.normalTabs.size,
- privateTabCount = state.privateTabs.size,
- onAccountSettingsClick = onAccountSettingsClick,
- onTabSettingsClick = onTabSettingsClick,
- onRecentlyClosedClick = onRecentlyClosedClick,
- onEnterMultiselectModeClick = {
- tabsTrayStore.dispatch(TabsTrayAction.EnterSelectMode)
- },
- onDeleteAllTabsClick = onDeleteAllTabsClick,
- )
- var showBottomAppBarMenu by remember { mutableStateOf(false) }
-
- BottomAppBar(
- actions = {
- IconButton(
- onClick = {
- tabsTrayStore.dispatch(TabsTrayAction.ThreeDotMenuShown)
- showBottomAppBarMenu = true
- },
- modifier = Modifier.testTag(TabsTrayTestTag.THREE_DOT_BUTTON),
- ) {
- Icon(
- painter = painterResource(iconsR.drawable.mozac_ic_ellipsis_vertical_24),
- contentDescription = stringResource(id = R.string.open_tabs_menu),
- )
-
- DropdownMenu(
- menuItems = menuItems,
- expanded = showBottomAppBarMenu,
- onDismissRequest = { showBottomAppBarMenu = false },
- )
- }
- },
- modifier = modifier,
- containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
- contentColor = MaterialTheme.colorScheme.secondary,
- scrollBehavior = scrollBehavior,
- )
- }
-}
-
-@Suppress("LongParameterList")
-private fun generateMenuItems(
- selectedPage: Page,
- normalTabCount: Int,
- privateTabCount: Int,
- onTabSettingsClick: () -> Unit,
- onRecentlyClosedClick: () -> Unit,
- onEnterMultiselectModeClick: () -> Unit,
- onDeleteAllTabsClick: () -> Unit,
- onAccountSettingsClick: () -> Unit,
-): List<MenuItem> {
- val enterSelectModeItem = MenuItem.IconItem(
- text = Text.Resource(R.string.tabs_tray_select_tabs),
- drawableRes = iconsR.drawable.mozac_ic_checkmark_24,
- testTag = TabsTrayTestTag.SELECT_TABS,
- onClick = onEnterMultiselectModeClick,
- )
- val recentlyClosedTabsItem = MenuItem.IconItem(
- text = Text.Resource(R.string.tab_tray_menu_recently_closed),
- drawableRes = iconsR.drawable.mozac_ic_history_24,
- testTag = TabsTrayTestTag.RECENTLY_CLOSED_TABS,
- onClick = onRecentlyClosedClick,
- )
- val tabSettingsItem = MenuItem.IconItem(
- text = Text.Resource(R.string.tab_tray_menu_tab_settings),
- drawableRes = iconsR.drawable.mozac_ic_settings_24,
- testTag = TabsTrayTestTag.TAB_SETTINGS,
- onClick = onTabSettingsClick,
- )
- val deleteAllTabsItem = MenuItem.IconItem(
- text = Text.Resource(R.string.tab_tray_menu_item_close),
- drawableRes = iconsR.drawable.mozac_ic_delete_24,
- testTag = TabsTrayTestTag.CLOSE_ALL_TABS,
- onClick = onDeleteAllTabsClick,
- )
- val accountSettingsItem = MenuItem.IconItem(
- text = Text.Resource(R.string.tab_tray_menu_account_settings),
- drawableRes = iconsR.drawable.mozac_ic_avatar_circle_24,
- testTag = TabsTrayTestTag.ACCOUNT_SETTINGS,
- onClick = onAccountSettingsClick,
- )
- return when {
- selectedPage == Page.NormalTabs && normalTabCount == 0 ||
- selectedPage == Page.PrivateTabs && privateTabCount == 0 -> listOf(
- recentlyClosedTabsItem,
- tabSettingsItem,
- )
-
- selectedPage == Page.NormalTabs -> listOf(
- enterSelectModeItem,
- recentlyClosedTabsItem,
- tabSettingsItem,
- deleteAllTabsItem,
- )
-
- selectedPage == Page.PrivateTabs -> listOf(
- recentlyClosedTabsItem,
- tabSettingsItem,
- deleteAllTabsItem,
- )
-
- selectedPage == Page.SyncedTabs -> listOf(
- accountSettingsItem,
- recentlyClosedTabsItem,
- )
-
- else -> emptyList()
- }
-}
-
-private class TabManagerBottomAppBarParameterProvider : PreviewParameterProvider<TabsTrayState> {
- override val values: Sequence<TabsTrayState>
- get() = sequenceOf(
- TabsTrayState(),
- TabsTrayState(selectedPage = Page.PrivateTabs),
- TabsTrayState(selectedPage = Page.SyncedTabs),
- )
-}
-
-@PreviewLightDark
-@Composable
-private fun TabManagerBottomAppBarPreview(
- @PreviewParameter(TabManagerBottomAppBarParameterProvider::class) state: TabsTrayState,
-) {
- FirefoxTheme {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(color = MaterialTheme.colorScheme.surface),
- contentAlignment = Alignment.BottomStart,
- ) {
- TabManagerBottomAppBar(
- tabsTrayStore = remember { TabsTrayStore(initialState = state) },
- pbmLocked = false,
- scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior(),
- onTabSettingsClick = {},
- onRecentlyClosedClick = {},
- onAccountSettingsClick = {},
- onDeleteAllTabsClick = {},
- )
- }
- }
-}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/fab/TabManagerFloatingToolbar.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/fab/TabManagerFloatingToolbar.kt
@@ -0,0 +1,410 @@
+/* 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.tabstray.ui.fab
+
+import androidx.annotation.DrawableRes
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import androidx.compose.ui.unit.dp
+import mozilla.components.compose.base.button.ExtendedFloatingActionButton
+import mozilla.components.compose.base.button.FloatingActionButtonDefaults
+import mozilla.components.compose.base.menu.DropdownMenu
+import mozilla.components.compose.base.menu.MenuItem
+import mozilla.components.compose.base.modifier.animateRotation
+import mozilla.components.compose.base.text.Text
+import mozilla.components.lib.state.ext.observeAsState
+import org.mozilla.fenix.R
+import org.mozilla.fenix.tabstray.Page
+import org.mozilla.fenix.tabstray.TabsTrayAction
+import org.mozilla.fenix.tabstray.TabsTrayState
+import org.mozilla.fenix.tabstray.TabsTrayState.Mode
+import org.mozilla.fenix.tabstray.TabsTrayStore
+import org.mozilla.fenix.tabstray.TabsTrayTestTag
+import org.mozilla.fenix.theme.FirefoxTheme
+import androidx.compose.material3.FloatingActionButtonDefaults as M3FloatingActionButtonDefaults
+import mozilla.components.ui.icons.R as iconsR
+
+/**
+ * Floating Toolbar for the Tab Manager.
+ *
+ * @param tabsTrayStore [TabsTrayStore] used to listen for changes to [TabsTrayState].
+ * @param isSignedIn Whether the user is signed into their Firefox account.
+ * @param modifier The [Modifier] to be applied to this FAB.
+ * @param expanded Controls the expansion state of this FAB. In an expanded state, the FAB will
+ * show both the icon and text. In a collapsed state, the FAB will show only the icon.
+ * @param pbmLocked Whether the private browsing mode is currently locked.
+ * @param onOpenNewNormalTabClicked Invoked when the fab is clicked in [Page.NormalTabs].
+ * @param onOpenNewPrivateTabClicked Invoked when the fab is clicked in [Page.PrivateTabs].
+ * @param onSyncedTabsFabClicked Invoked when the fab is clicked in [Page.SyncedTabs].
+ * @param onTabSettingsClick Invoked when the user clicks on the tab settings banner menu item.
+ * @param onRecentlyClosedClick Invoked when the user clicks on the recently closed banner menu item.
+ * @param onAccountSettingsClick Invoked when the user clicks on the account settings banner menu item.
+ * @param onDeleteAllTabsClick Invoked when the user clicks on the close all tabs banner menu item.
+ */
+@Suppress("LongParameterList")
+@Composable
+internal fun TabManagerFloatingToolbar(
+ tabsTrayStore: TabsTrayStore,
+ isSignedIn: Boolean,
+ modifier: Modifier = Modifier,
+ expanded: Boolean = true,
+ pbmLocked: Boolean = false,
+ onOpenNewNormalTabClicked: () -> Unit,
+ onOpenNewPrivateTabClicked: () -> Unit,
+ onSyncedTabsFabClicked: () -> Unit,
+ onTabSettingsClick: () -> Unit,
+ onRecentlyClosedClick: () -> Unit,
+ onAccountSettingsClick: () -> Unit,
+ onDeleteAllTabsClick: () -> Unit,
+) {
+ val state by tabsTrayStore.observeAsState(initialValue = tabsTrayStore.state) { it }
+ val privateTabsLocked = pbmLocked && state.selectedPage == Page.PrivateTabs
+
+ AnimatedVisibility(
+ visible = state.mode is Mode.Normal && !privateTabsLocked,
+ modifier = modifier,
+ enter = fadeIn(),
+ exit = fadeOut(),
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Box(
+ modifier = Modifier.weight(1f),
+ contentAlignment = Alignment.CenterEnd,
+ ) {
+ FloatingToolbarActions(
+ state = state,
+ onMenuShown = {
+ tabsTrayStore.dispatch(TabsTrayAction.ThreeDotMenuShown)
+ },
+ onEnterMultiselectModeClick = {
+ tabsTrayStore.dispatch(TabsTrayAction.EnterSelectMode)
+ },
+ onTabSettingsClick = onTabSettingsClick,
+ onRecentlyClosedClick = onRecentlyClosedClick,
+ onAccountSettingsClick = onAccountSettingsClick,
+ onDeleteAllTabsClick = onDeleteAllTabsClick,
+ )
+ }
+
+ Spacer(modifier = Modifier.width(FirefoxTheme.layout.space.static100))
+
+ Box(
+ modifier = Modifier.weight(1f),
+ contentAlignment = Alignment.CenterStart,
+ ) {
+ FloatingToolbarFAB(
+ state = state,
+ expanded = expanded,
+ isSignedIn = isSignedIn,
+ onOpenNewNormalTabClicked = onOpenNewNormalTabClicked,
+ onOpenNewPrivateTabClicked = onOpenNewPrivateTabClicked,
+ onSyncedTabsFabClicked = onSyncedTabsFabClicked,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun FloatingToolbarActions(
+ state: TabsTrayState,
+ onMenuShown: () -> Unit,
+ onEnterMultiselectModeClick: () -> Unit,
+ onTabSettingsClick: () -> Unit,
+ onRecentlyClosedClick: () -> Unit,
+ onAccountSettingsClick: () -> Unit,
+ onDeleteAllTabsClick: () -> Unit,
+) {
+ var showBottomAppBarMenu by remember { mutableStateOf(false) }
+ val menuItems = generateMenuItems(
+ selectedPage = state.selectedPage,
+ normalTabCount = state.normalTabs.size,
+ privateTabCount = state.privateTabs.size,
+ onAccountSettingsClick = onAccountSettingsClick,
+ onTabSettingsClick = onTabSettingsClick,
+ onRecentlyClosedClick = onRecentlyClosedClick,
+ onEnterMultiselectModeClick = onEnterMultiselectModeClick,
+ onDeleteAllTabsClick = onDeleteAllTabsClick,
+ )
+
+ Card(
+ modifier = Modifier,
+ shape = CircleShape,
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainer,
+ contentColor = MaterialTheme.colorScheme.onSurface,
+ ),
+ elevation = CardDefaults.elevatedCardElevation(defaultElevation = 6.dp),
+ ) {
+ Row(
+ modifier = Modifier.padding(all = FirefoxTheme.layout.space.static100),
+ horizontalArrangement = Arrangement.spacedBy(FirefoxTheme.layout.space.static50),
+ ) {
+ IconButton(
+ onClick = {
+ onMenuShown()
+ showBottomAppBarMenu = true
+ },
+ modifier = Modifier.testTag(TabsTrayTestTag.THREE_DOT_BUTTON),
+ ) {
+ Icon(
+ painter = painterResource(iconsR.drawable.mozac_ic_ellipsis_vertical_24),
+ contentDescription = stringResource(id = R.string.open_tabs_menu),
+ )
+
+ DropdownMenu(
+ menuItems = menuItems,
+ expanded = showBottomAppBarMenu,
+ onDismissRequest = { showBottomAppBarMenu = false },
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun FloatingToolbarFAB(
+ state: TabsTrayState,
+ expanded: Boolean,
+ isSignedIn: Boolean,
+ onOpenNewNormalTabClicked: () -> Unit,
+ onOpenNewPrivateTabClicked: () -> Unit,
+ onSyncedTabsFabClicked: () -> Unit,
+) {
+ @DrawableRes val icon: Int
+ val contentDescription: String
+ val label: String
+ var colors = FloatingActionButtonDefaults.colorsPrimary()
+ var elevation = M3FloatingActionButtonDefaults.elevation()
+ val onClick: () -> Unit
+ var iconModifier: Modifier = Modifier
+ when (state.selectedPage) {
+ Page.NormalTabs -> {
+ icon = iconsR.drawable.mozac_ic_plus_24
+ contentDescription = stringResource(id = R.string.add_tab)
+ label = stringResource(id = R.string.tab_manager_floating_action_button_new_normal_tab)
+ onClick = onOpenNewNormalTabClicked
+ }
+
+ Page.PrivateTabs -> {
+ icon = iconsR.drawable.mozac_ic_plus_24
+ contentDescription = stringResource(id = R.string.add_private_tab)
+ label = stringResource(id = R.string.tab_manager_floating_action_button_new_private_tab)
+ onClick = onOpenNewPrivateTabClicked
+ }
+
+ Page.SyncedTabs -> {
+ icon = iconsR.drawable.mozac_ic_sync_24
+ contentDescription = stringResource(id = R.string.resync_button_content_description)
+ label = if (state.syncing) {
+ stringResource(id = R.string.sync_syncing_in_progress)
+ } else {
+ stringResource(id = R.string.tab_manager_floating_action_button_sync_tabs)
+ }
+ onClick = {
+ if (isSignedIn) {
+ onSyncedTabsFabClicked()
+ }
+ }
+ if (!isSignedIn) {
+ colors = FloatingActionButtonDefaults.colorsDisabled()
+ elevation = M3FloatingActionButtonDefaults.elevation(
+ defaultElevation = 0.dp,
+ pressedElevation = 0.dp,
+ focusedElevation = 0.dp,
+ hoveredElevation = 0.dp,
+ )
+ }
+ iconModifier = Modifier.animateRotation(animate = state.syncing)
+ }
+ }
+
+ ExtendedFloatingActionButton(
+ label = label,
+ onClick = onClick,
+ modifier = Modifier.testTag(TabsTrayTestTag.FAB),
+ expanded = expanded,
+ colors = colors,
+ elevation = elevation,
+ ) {
+ Icon(
+ painter = painterResource(id = icon),
+ contentDescription = contentDescription,
+ modifier = iconModifier,
+ )
+ }
+}
+
+@Suppress("LongParameterList")
+private fun generateMenuItems(
+ selectedPage: Page,
+ normalTabCount: Int,
+ privateTabCount: Int,
+ onTabSettingsClick: () -> Unit,
+ onRecentlyClosedClick: () -> Unit,
+ onEnterMultiselectModeClick: () -> Unit,
+ onDeleteAllTabsClick: () -> Unit,
+ onAccountSettingsClick: () -> Unit,
+): List<MenuItem> {
+ val enterSelectModeItem = MenuItem.IconItem(
+ text = Text.Resource(R.string.tabs_tray_select_tabs),
+ drawableRes = iconsR.drawable.mozac_ic_checkmark_24,
+ testTag = TabsTrayTestTag.SELECT_TABS,
+ onClick = onEnterMultiselectModeClick,
+ )
+ val recentlyClosedTabsItem = MenuItem.IconItem(
+ text = Text.Resource(R.string.tab_tray_menu_recently_closed),
+ drawableRes = iconsR.drawable.mozac_ic_history_24,
+ testTag = TabsTrayTestTag.RECENTLY_CLOSED_TABS,
+ onClick = onRecentlyClosedClick,
+ )
+ val tabSettingsItem = MenuItem.IconItem(
+ text = Text.Resource(R.string.tab_tray_menu_tab_settings),
+ drawableRes = iconsR.drawable.mozac_ic_settings_24,
+ testTag = TabsTrayTestTag.TAB_SETTINGS,
+ onClick = onTabSettingsClick,
+ )
+ val deleteAllTabsItem = MenuItem.IconItem(
+ text = Text.Resource(R.string.tab_tray_menu_item_close),
+ drawableRes = iconsR.drawable.mozac_ic_delete_24,
+ testTag = TabsTrayTestTag.CLOSE_ALL_TABS,
+ onClick = onDeleteAllTabsClick,
+ )
+ val accountSettingsItem = MenuItem.IconItem(
+ text = Text.Resource(R.string.tab_tray_menu_account_settings),
+ drawableRes = iconsR.drawable.mozac_ic_avatar_circle_24,
+ testTag = TabsTrayTestTag.ACCOUNT_SETTINGS,
+ onClick = onAccountSettingsClick,
+ )
+ return when {
+ selectedPage == Page.NormalTabs && normalTabCount == 0 ||
+ selectedPage == Page.PrivateTabs && privateTabCount == 0 -> listOf(
+ recentlyClosedTabsItem,
+ tabSettingsItem,
+ )
+
+ selectedPage == Page.NormalTabs -> listOf(
+ enterSelectModeItem,
+ recentlyClosedTabsItem,
+ tabSettingsItem,
+ deleteAllTabsItem,
+ )
+
+ selectedPage == Page.PrivateTabs -> listOf(
+ recentlyClosedTabsItem,
+ tabSettingsItem,
+ deleteAllTabsItem,
+ )
+
+ selectedPage == Page.SyncedTabs -> listOf(
+ accountSettingsItem,
+ recentlyClosedTabsItem,
+ )
+
+ else -> emptyList()
+ }
+}
+
+private data class TabManagerFloatingToolbarPreviewModel(
+ val state: TabsTrayState,
+ val expanded: Boolean,
+ val isSignedIn: Boolean = true,
+)
+
+private class TabManagerFloatingToolbarParameterProvider :
+ PreviewParameterProvider<TabManagerFloatingToolbarPreviewModel> {
+ override val values: Sequence<TabManagerFloatingToolbarPreviewModel>
+ get() = sequenceOf(
+ TabManagerFloatingToolbarPreviewModel(
+ state = TabsTrayState(selectedPage = Page.NormalTabs),
+ expanded = false,
+ ),
+ TabManagerFloatingToolbarPreviewModel(
+ state = TabsTrayState(selectedPage = Page.NormalTabs),
+ expanded = true,
+ ),
+ TabManagerFloatingToolbarPreviewModel(
+ state = TabsTrayState(selectedPage = Page.PrivateTabs),
+ expanded = false,
+ ),
+ TabManagerFloatingToolbarPreviewModel(
+ state = TabsTrayState(selectedPage = Page.PrivateTabs),
+ expanded = true,
+ ),
+ TabManagerFloatingToolbarPreviewModel(
+ state = TabsTrayState(selectedPage = Page.SyncedTabs),
+ expanded = false,
+ ),
+ TabManagerFloatingToolbarPreviewModel(
+ state = TabsTrayState(selectedPage = Page.SyncedTabs),
+ expanded = true,
+ ),
+ TabManagerFloatingToolbarPreviewModel(
+ state = TabsTrayState(selectedPage = Page.SyncedTabs),
+ expanded = false,
+ isSignedIn = false,
+ ),
+ TabManagerFloatingToolbarPreviewModel(
+ state = TabsTrayState(selectedPage = Page.SyncedTabs),
+ expanded = true,
+ isSignedIn = false,
+ ),
+ )
+}
+
+@Preview
+@Composable
+private fun TabManagerFloatingToolbarPreview(
+ @PreviewParameter(TabManagerFloatingToolbarParameterProvider::class)
+ previewDataModel: TabManagerFloatingToolbarPreviewModel,
+) {
+ FirefoxTheme {
+ TabManagerFloatingToolbar(
+ tabsTrayStore = remember { TabsTrayStore(initialState = previewDataModel.state) },
+ expanded = previewDataModel.expanded,
+ isSignedIn = previewDataModel.isSignedIn,
+ pbmLocked = false,
+ modifier = Modifier
+ .background(color = MaterialTheme.colorScheme.surface)
+ .padding(all = 16.dp),
+ onOpenNewNormalTabClicked = {},
+ onOpenNewPrivateTabClicked = {},
+ onSyncedTabsFabClicked = {},
+ onTabSettingsClick = {},
+ onAccountSettingsClick = {},
+ onDeleteAllTabsClick = {},
+ onRecentlyClosedClick = {},
+ )
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/fab/TabsTrayFab.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/fab/TabsTrayFab.kt
@@ -1,203 +0,0 @@
-/* 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.tabstray.ui.fab
-
-import androidx.annotation.DrawableRes
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import androidx.compose.ui.unit.dp
-import mozilla.components.compose.base.button.ExtendedFloatingActionButton
-import mozilla.components.compose.base.button.FloatingActionButtonDefaults
-import mozilla.components.compose.base.modifier.animateRotation
-import mozilla.components.lib.state.ext.observeAsState
-import org.mozilla.fenix.R
-import org.mozilla.fenix.tabstray.Page
-import org.mozilla.fenix.tabstray.TabsTrayState
-import org.mozilla.fenix.tabstray.TabsTrayState.Mode
-import org.mozilla.fenix.tabstray.TabsTrayStore
-import org.mozilla.fenix.tabstray.TabsTrayTestTag
-import org.mozilla.fenix.theme.FirefoxTheme
-import androidx.compose.material3.FloatingActionButtonDefaults as M3FloatingActionButtonDefaults
-import mozilla.components.ui.icons.R as iconsR
-
-/**
- * Floating action button for the Tab Manager.
- *
- * @param tabsTrayStore [TabsTrayStore] used to listen for changes to [TabsTrayState].
- * @param isSignedIn Whether the user is signed into their Firefox account.
- * @param modifier The [Modifier] to be applied to this FAB.
- * @param expanded Controls the expansion state of this FAB. In an expanded state, the FAB will
- * show both the icon and text. In a collapsed state, the FAB will show only the icon.
- * @param pbmLocked Whether the private browsing mode is currently locked.
- * @param onOpenNewNormalTabClicked Invoked when the fab is clicked in [Page.NormalTabs].
- * @param onOpenNewPrivateTabClicked Invoked when the fab is clicked in [Page.PrivateTabs].
- * @param onSyncedTabsFabClicked Invoked when the fab is clicked in [Page.SyncedTabs].
- */
-@Composable
-internal fun TabsTrayFab(
- tabsTrayStore: TabsTrayStore,
- isSignedIn: Boolean,
- modifier: Modifier = Modifier,
- expanded: Boolean = true,
- pbmLocked: Boolean = false,
- onOpenNewNormalTabClicked: () -> Unit,
- onOpenNewPrivateTabClicked: () -> Unit,
- onSyncedTabsFabClicked: () -> Unit,
-) {
- val state by tabsTrayStore.observeAsState(initialValue = tabsTrayStore.state) { it }
- val privateTabsLocked = pbmLocked && state.selectedPage == Page.PrivateTabs
-
- val isSyncing by tabsTrayStore.observeAsState(initialValue = tabsTrayStore.state.syncing) { state ->
- state.syncing
- }
-
- AnimatedVisibility(
- visible = state.mode is Mode.Normal && !privateTabsLocked,
- ) {
- @DrawableRes val icon: Int
- val contentDescription: String
- val label: String
- var colors = FloatingActionButtonDefaults.colorsPrimary()
- var elevation = if (expanded) {
- M3FloatingActionButtonDefaults.loweredElevation(
- defaultElevation = 0.dp,
- )
- } else {
- M3FloatingActionButtonDefaults.elevation()
- }
- val onClick: () -> Unit
- var iconModifier: Modifier = Modifier
- when (state.selectedPage) {
- Page.NormalTabs -> {
- icon = iconsR.drawable.mozac_ic_plus_24
- contentDescription = stringResource(id = R.string.add_tab)
- label = stringResource(id = R.string.tab_manager_floating_action_button_new_normal_tab)
- onClick = onOpenNewNormalTabClicked
- }
-
- Page.PrivateTabs -> {
- icon = iconsR.drawable.mozac_ic_plus_24
- contentDescription = stringResource(id = R.string.add_private_tab)
- label = stringResource(id = R.string.tab_manager_floating_action_button_new_private_tab)
- onClick = onOpenNewPrivateTabClicked
- }
-
- Page.SyncedTabs -> {
- icon = iconsR.drawable.mozac_ic_sync_24
- contentDescription = stringResource(id = R.string.resync_button_content_description)
- label = if (isSyncing) {
- stringResource(id = R.string.sync_syncing_in_progress)
- } else {
- stringResource(id = R.string.tab_manager_floating_action_button_sync_tabs)
- }
- onClick = onSyncedTabsFabClicked
- if (!isSignedIn) {
- colors = FloatingActionButtonDefaults.colorsDisabled()
- elevation = M3FloatingActionButtonDefaults.elevation(
- defaultElevation = 0.dp,
- pressedElevation = 0.dp,
- focusedElevation = 0.dp,
- hoveredElevation = 0.dp,
- )
- }
- iconModifier = Modifier.animateRotation(animate = isSyncing)
- }
- }
- ExtendedFloatingActionButton(
- label = label,
- onClick = onClick,
- modifier = modifier.testTag(TabsTrayTestTag.FAB),
- expanded = expanded,
- colors = colors,
- elevation = elevation,
- ) {
- Icon(
- painter = painterResource(id = icon),
- contentDescription = contentDescription,
- modifier = iconModifier,
- )
- }
- }
-}
-
-private data class TabsTrayFabPreviewDataModel(
- val state: TabsTrayState,
- val expanded: Boolean,
- val isSignedIn: Boolean = true,
-)
-
-private class TabsTrayFabParameterProvider : PreviewParameterProvider<TabsTrayFabPreviewDataModel> {
- override val values: Sequence<TabsTrayFabPreviewDataModel>
- get() = sequenceOf(
- TabsTrayFabPreviewDataModel(
- state = TabsTrayState(selectedPage = Page.NormalTabs),
- expanded = false,
- ),
- TabsTrayFabPreviewDataModel(
- state = TabsTrayState(selectedPage = Page.NormalTabs),
- expanded = true,
- ),
- TabsTrayFabPreviewDataModel(
- state = TabsTrayState(selectedPage = Page.PrivateTabs),
- expanded = false,
- ),
- TabsTrayFabPreviewDataModel(
- state = TabsTrayState(selectedPage = Page.PrivateTabs),
- expanded = true,
- ),
- TabsTrayFabPreviewDataModel(
- state = TabsTrayState(selectedPage = Page.SyncedTabs),
- expanded = false,
- ),
- TabsTrayFabPreviewDataModel(
- state = TabsTrayState(selectedPage = Page.SyncedTabs),
- expanded = true,
- ),
- TabsTrayFabPreviewDataModel(
- state = TabsTrayState(selectedPage = Page.SyncedTabs),
- expanded = false,
- isSignedIn = false,
- ),
- TabsTrayFabPreviewDataModel(
- state = TabsTrayState(selectedPage = Page.SyncedTabs),
- expanded = true,
- isSignedIn = false,
- ),
- )
-}
-
-@Preview
-@Composable
-private fun TabsTrayFabPreview(
- @PreviewParameter(TabsTrayFabParameterProvider::class) previewDataModel: TabsTrayFabPreviewDataModel,
-) {
- FirefoxTheme {
- TabsTrayFab(
- tabsTrayStore = remember { TabsTrayStore(initialState = previewDataModel.state) },
- expanded = previewDataModel.expanded,
- isSignedIn = previewDataModel.isSignedIn,
- pbmLocked = false,
- modifier = Modifier
- .background(color = MaterialTheme.colorScheme.surfaceContainerHigh)
- .padding(all = 16.dp),
- onOpenNewNormalTabClicked = {},
- onOpenNewPrivateTabClicked = {},
- onSyncedTabsFabClicked = {},
- )
- }
-}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabstray/TabsTray.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabstray/TabsTray.kt
@@ -14,12 +14,10 @@ import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition
import androidx.compose.material3.MaterialTheme
@@ -29,6 +27,7 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -59,8 +58,7 @@ import org.mozilla.fenix.tabstray.TabsTrayTestTag
import org.mozilla.fenix.tabstray.ext.isNormalTab
import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsListItem
import org.mozilla.fenix.tabstray.ui.banner.TabsTrayBanner
-import org.mozilla.fenix.tabstray.ui.bottomappbar.TabManagerBottomAppBar
-import org.mozilla.fenix.tabstray.ui.fab.TabsTrayFab
+import org.mozilla.fenix.tabstray.ui.fab.TabManagerFloatingToolbar
import org.mozilla.fenix.tabstray.ui.tabpage.NormalTabsPage
import org.mozilla.fenix.tabstray.ui.tabpage.PrivateTabsPage
import org.mozilla.fenix.tabstray.ui.tabpage.SyncedTabsPage
@@ -70,12 +68,6 @@ import mozilla.components.browser.storage.sync.Tab as SyncTab
import org.mozilla.fenix.tabstray.ui.syncedtabs.OnTabClick as OnSyncedTabClick
import org.mozilla.fenix.tabstray.ui.syncedtabs.OnTabCloseClick as OnSyncedTabClose
-/**
- * There is a bug in the [Scaffold] code where it miscalculates the FAB padding and it's off by 4
- * when using [FabPosition.EndOverlay], causing the FAB to be too close to the top of the
- * BottomAppBar.
- */
-private val ScaffoldFabOffsetCorrection = 4.dp
private const val SPACER_BACKGROUND_ALPHA = 0.75f
private val DefaultStatusBarHeight = 50.dp
@@ -204,7 +196,11 @@ fun TabsTray(
}
val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
- val bottomAppBarScrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
+ val isScrolled by remember(topAppBarScrollBehavior.state) {
+ derivedStateOf {
+ topAppBarScrollBehavior.state.collapsedFraction == 1f
+ }
+ }
LaunchedEffect(tabsTrayState.selectedPage) {
pagerState.animateScrollToPage(Page.pageToPosition(tabsTrayState.selectedPage))
@@ -213,7 +209,6 @@ fun TabsTray(
Scaffold(
modifier = modifier
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection)
- .nestedScroll(bottomAppBarScrollBehavior.nestedScrollConnection)
.testTag(TabsTrayTestTag.TABS_TRAY),
snackbarHost = {
SnackbarHost(
@@ -251,30 +246,22 @@ fun TabsTray(
},
)
},
- bottomBar = {
- TabManagerBottomAppBar(
- tabsTrayStore = tabsTrayStore,
- pbmLocked = isPbmLocked,
- scrollBehavior = bottomAppBarScrollBehavior,
- onTabSettingsClick = onTabSettingsClick,
- onRecentlyClosedClick = onRecentlyClosedClick,
- onAccountSettingsClick = onAccountSettingsClick,
- onDeleteAllTabsClick = onDeleteAllTabsClick,
- )
- },
floatingActionButton = {
- TabsTrayFab(
+ TabManagerFloatingToolbar(
tabsTrayStore = tabsTrayStore,
- expanded = bottomAppBarScrollBehavior.state.collapsedFraction == 0f,
- modifier = Modifier.offset(y = ScaffoldFabOffsetCorrection),
+ expanded = !isScrolled,
isSignedIn = isSignedIn,
pbmLocked = isPbmLocked,
onOpenNewNormalTabClicked = onOpenNewNormalTabClicked,
onOpenNewPrivateTabClicked = onOpenNewPrivateTabClicked,
onSyncedTabsFabClicked = onSyncedTabsFabClicked,
+ onTabSettingsClick = onTabSettingsClick,
+ onRecentlyClosedClick = onRecentlyClosedClick,
+ onAccountSettingsClick = onAccountSettingsClick,
+ onDeleteAllTabsClick = onDeleteAllTabsClick,
)
},
- floatingActionButtonPosition = FabPosition.EndOverlay,
+ floatingActionButtonPosition = FabPosition.Center,
) { paddingValues ->
Box {
HorizontalPager(