Bug 1816522 - Add multi selection banner UI

fenix/114.1.0
Alexandru2909 1 year ago committed by mergify[bot]
parent 3f318de778
commit fc40edf578

@ -17,6 +17,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
@ -28,6 +29,7 @@ import org.mozilla.fenix.theme.FirefoxTheme
* @param showMenu Whether or not the menu is currently displayed to the user. * @param showMenu Whether or not the menu is currently displayed to the user.
* @param onDismissRequest Invoked when user dismisses the menu or on orientation changes. * @param onDismissRequest Invoked when user dismisses the menu or on orientation changes.
* @param modifier Modifier to be applied to the menu. * @param modifier Modifier to be applied to the menu.
* @param offset Offset to be added to the position of the menu.
*/ */
@Composable @Composable
fun DropdownMenu( fun DropdownMenu(
@ -35,6 +37,7 @@ fun DropdownMenu(
showMenu: Boolean, showMenu: Boolean,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
offset: DpOffset = DpOffset.Zero,
) { ) {
DisposableEffect(LocalConfiguration.current.orientation) { DisposableEffect(LocalConfiguration.current.orientation) {
onDispose { onDismissRequest() } onDispose { onDismissRequest() }
@ -44,6 +47,7 @@ fun DropdownMenu(
DropdownMenu( DropdownMenu(
expanded = showMenu && menuItems.isNotEmpty(), expanded = showMenu && menuItems.isNotEmpty(),
onDismissRequest = { onDismissRequest() }, onDismissRequest = { onDismissRequest() },
offset = offset,
modifier = Modifier modifier = Modifier
.background(color = FirefoxTheme.colors.layer2) .background(color = FirefoxTheme.colors.layer2)
.then(modifier), .then(modifier),

@ -50,6 +50,7 @@ import mozilla.components.browser.storage.sync.Tab as SyncTab
* @param browserStore [BrowserStore] used to listen for changes to [BrowserState]. * @param browserStore [BrowserStore] used to listen for changes to [BrowserState].
* @param tabsTrayStore [TabsTrayStore] used to listen for changes to [TabsTrayState]. * @param tabsTrayStore [TabsTrayStore] used to listen for changes to [TabsTrayState].
* @param displayTabsInGrid Whether the normal and private tabs should be displayed in a grid. * @param displayTabsInGrid Whether the normal and private tabs should be displayed in a grid.
* @param isInDebugMode True for debug variant or if secret menu is enabled for this session.
* @param onTabClose Invoked when the user clicks to close a tab. * @param onTabClose Invoked when the user clicks to close a tab.
* @param onTabMediaClick Invoked when the user interacts with a tab's media controls. * @param onTabMediaClick Invoked when the user interacts with a tab's media controls.
* @param onTabClick Invoked when the user clicks on a tab. * @param onTabClick Invoked when the user clicks on a tab.
@ -68,13 +69,14 @@ import mozilla.components.browser.storage.sync.Tab as SyncTab
* @param onSyncedTabClick Invoked when the user clicks on a synced tab. * @param onSyncedTabClick Invoked when the user clicks on a synced tab.
*/ */
@OptIn(ExperimentalPagerApi::class, ExperimentalComposeUiApi::class) @OptIn(ExperimentalPagerApi::class, ExperimentalComposeUiApi::class)
@Suppress("LongMethod", "LongParameterList") @Suppress("LongMethod", "LongParameterList", "ComplexMethod")
@Composable @Composable
fun TabsTray( fun TabsTray(
appStore: AppStore, appStore: AppStore,
browserStore: BrowserStore, browserStore: BrowserStore,
tabsTrayStore: TabsTrayStore, tabsTrayStore: TabsTrayStore,
displayTabsInGrid: Boolean, displayTabsInGrid: Boolean,
isInDebugMode: Boolean,
shouldShowInactiveTabsAutoCloseDialog: (Int) -> Boolean, shouldShowInactiveTabsAutoCloseDialog: (Int) -> Boolean,
onTabPageClick: (Page) -> Unit, onTabPageClick: (Page) -> Unit,
onTabClose: (TabSessionState) -> Unit, onTabClose: (TabSessionState) -> Unit,
@ -116,6 +118,12 @@ fun TabsTray(
} }
} }
val shapeModifier = if (isInMultiSelectMode) {
Modifier
} else {
Modifier.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp))
}
LaunchedEffect(selectedPage) { LaunchedEffect(selectedPage) {
pagerState.animateScrollToPage(selectedPage.ordinal) pagerState.animateScrollToPage(selectedPage.ordinal)
} }
@ -123,14 +131,15 @@ fun TabsTray(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) .then(shapeModifier)
.background(FirefoxTheme.colors.layer1), .background(FirefoxTheme.colors.layer1),
) { ) {
Box(modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection())) { Box(modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection())) {
TabsTrayBanner( TabsTrayBanner(
isInMultiSelectMode = isInMultiSelectMode, selectMode = multiselectMode,
selectedPage = selectedPage, selectedPage = selectedPage,
normalTabCount = normalTabs.size + inactiveTabs.size, normalTabCount = normalTabs.size + inactiveTabs.size,
isInDebugMode = isInDebugMode,
onTabPageIndicatorClicked = onTabPageClick, onTabPageIndicatorClicked = onTabPageClick,
) )
} }
@ -341,6 +350,7 @@ private fun TabsTrayPreviewRoot(
browserStore = browserStore, browserStore = browserStore,
tabsTrayStore = tabsTrayStore, tabsTrayStore = tabsTrayStore,
displayTabsInGrid = displayTabsInGrid, displayTabsInGrid = displayTabsInGrid,
isInDebugMode = false,
shouldShowInactiveTabsAutoCloseDialog = { true }, shouldShowInactiveTabsAutoCloseDialog = { true },
onTabPageClick = { page -> onTabPageClick = { page ->
selectedPageState = page selectedPageState = page

@ -39,29 +39,42 @@ import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import mozilla.components.browser.state.state.ContentState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.ui.tabcounter.TabCounter import mozilla.components.ui.tabcounter.TabCounter
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.compose.DropdownMenu
import org.mozilla.fenix.compose.MenuItem
import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
private val ICON_SIZE = 24.dp
/** /**
* Top-level UI for displaying the banner in [TabsTray]. * Top-level UI for displaying the banner in [TabsTray].
* *
* @param isInMultiSelectMode Whether the tab list is in multi-select mode. * @param selectMode Current [TabsTrayState.Mode] used in the tabs tray.
* @param selectedPage The active [Page] of the Tabs Tray. * @param selectedPage The active [Page] of the Tabs Tray.
* @param normalTabCount The total amount of normal browsing tabs currently open. * @param normalTabCount The total amount of normal browsing tabs currently open.
* @param isInDebugMode True for debug variant or if secret menu is enabled for this session.
* @param onTabPageIndicatorClicked Invoked when the user clicks on a tab page indicator. * @param onTabPageIndicatorClicked Invoked when the user clicks on a tab page indicator.
*/ */
@Composable @Composable
fun TabsTrayBanner( fun TabsTrayBanner(
isInMultiSelectMode: Boolean, selectMode: TabsTrayState.Mode,
selectedPage: Page, selectedPage: Page,
normalTabCount: Int, normalTabCount: Int,
isInDebugMode: Boolean,
onTabPageIndicatorClicked: (Page) -> Unit, onTabPageIndicatorClicked: (Page) -> Unit,
) { ) {
if (isInMultiSelectMode) { if (selectMode is TabsTrayState.Mode.Select) {
MultiSelectBanner() MultiSelectBanner(
selectedTabCount = selectMode.selectedTabs.size,
shouldShowInactiveButton = isInDebugMode,
)
} else { } else {
SingleSelectBanner( SingleSelectBanner(
onTabPageIndicatorClicked = onTabPageIndicatorClicked, onTabPageIndicatorClicked = onTabPageIndicatorClicked,
@ -198,21 +211,95 @@ private fun NormalTabsTabIcon(normalTabCount: Int) {
} }
} }
/**
* Banner displayed in multi select mode.
*
* @param selectedTabCount Number of selected tabs.
* @param shouldShowInactiveButton Whether or not to show the inactive tabs menu item.
*/
@Suppress("LongMethod")
@Composable @Composable
private fun MultiSelectBanner() { private fun MultiSelectBanner(
Box( selectedTabCount: Int,
shouldShowInactiveButton: Boolean,
) {
var showMenu by remember { mutableStateOf(false) }
val menuItems = mutableListOf(
MenuItem(
title = stringResource(R.string.tab_tray_multiselect_menu_item_bookmark),
) {},
MenuItem(
title = stringResource(R.string.tab_tray_multiselect_menu_item_close),
) {},
)
if (shouldShowInactiveButton) {
menuItems.add(
MenuItem(
title = stringResource(R.string.inactive_tabs_menu_item),
) {},
)
}
Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(80.dp) .height(88.dp)
.background(color = FirefoxTheme.colors.layerAccent), .background(color = FirefoxTheme.colors.layerAccent),
contentAlignment = Alignment.Center, verticalAlignment = Alignment.CenterVertically,
) { ) {
IconButton(onClick = {}) {
Icon(
painter = painterResource(id = R.drawable.ic_close),
contentDescription = stringResource(id = R.string.tab_tray_close_multiselect_content_description),
tint = FirefoxTheme.colors.iconOnColor,
)
}
Text( Text(
text = "Multi selection mode", text = stringResource(R.string.tab_tray_multi_select_title, selectedTabCount),
color = FirefoxTheme.colors.textOnColorPrimary,
style = FirefoxTheme.typography.body1, style = FirefoxTheme.typography.body1,
color = FirefoxTheme.colors.textOnColorPrimary,
fontSize = 20.sp,
fontWeight = FontWeight.W500,
)
Spacer(modifier = Modifier.weight(1.0f))
IconButton(onClick = {}) {
Icon(
painter = painterResource(id = R.drawable.ic_tab_collection),
contentDescription = stringResource(
id = R.string.tab_tray_collection_button_multiselect_content_description,
),
tint = FirefoxTheme.colors.iconOnColor,
) )
} }
IconButton(onClick = {}) {
Icon(
painter = painterResource(id = R.drawable.ic_share),
contentDescription = stringResource(
id = R.string.tab_tray_multiselect_share_content_description,
),
tint = FirefoxTheme.colors.iconOnColor,
)
}
IconButton(onClick = { showMenu = true }) {
Icon(
painter = painterResource(id = R.drawable.ic_menu),
contentDescription = stringResource(id = R.string.tab_tray_multiselect_menu_content_description),
tint = FirefoxTheme.colors.iconOnColor,
)
DropdownMenu(
menuItems = menuItems,
showMenu = showMenu,
offset = DpOffset(x = 0.dp, y = -ICON_SIZE),
onDismissRequest = { showMenu = false },
)
}
}
} }
@LightDarkPreview @LightDarkPreview
@ -236,13 +323,28 @@ private fun TabsTrayBannerInfinityPreview() {
@Composable @Composable
private fun TabsTrayBannerMultiselectPreview() { private fun TabsTrayBannerMultiselectPreview() {
TabsTrayBannerPreviewRoot( TabsTrayBannerPreviewRoot(
isInMultiSelectMode = true, selectMode = TabsTrayState.Mode.Select(
setOf(
TabSessionState(
id = "1",
content = ContentState(
url = "www.mozilla.com",
),
),
TabSessionState(
id = "2",
content = ContentState(
url = "www.mozilla.com",
),
),
),
),
) )
} }
@Composable @Composable
private fun TabsTrayBannerPreviewRoot( private fun TabsTrayBannerPreviewRoot(
isInMultiSelectMode: Boolean = false, selectMode: TabsTrayState.Mode = TabsTrayState.Mode.Normal,
selectedPage: Page = Page.NormalTabs, selectedPage: Page = Page.NormalTabs,
normalTabCount: Int = 10, normalTabCount: Int = 10,
) { ) {
@ -251,9 +353,10 @@ private fun TabsTrayBannerPreviewRoot(
FirefoxTheme { FirefoxTheme {
Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer1)) { Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer1)) {
TabsTrayBanner( TabsTrayBanner(
isInMultiSelectMode = isInMultiSelectMode, selectMode = selectMode,
selectedPage = selectedPageState, selectedPage = selectedPageState,
normalTabCount = normalTabCount, normalTabCount = normalTabCount,
isInDebugMode = true,
onTabPageIndicatorClicked = { page -> onTabPageIndicatorClicked = { page ->
selectedPageState = page selectedPageState = page
}, },

@ -32,6 +32,7 @@ import mozilla.components.feature.downloads.ui.DownloadCancelDialogFragment
import mozilla.components.feature.tabs.tabstray.TabsFeature import mozilla.components.feature.tabs.tabstray.TabsFeature
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.telemetry.glean.private.NoExtras import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.Config
import org.mozilla.fenix.GleanMetrics.TabsTray import org.mozilla.fenix.GleanMetrics.TabsTray
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.NavGraphDirections
@ -223,6 +224,8 @@ class TabsTrayFragment : AppCompatDialogFragment() {
browserStore = requireComponents.core.store, browserStore = requireComponents.core.store,
tabsTrayStore = tabsTrayStore, tabsTrayStore = tabsTrayStore,
displayTabsInGrid = requireContext().settings().gridTabView, displayTabsInGrid = requireContext().settings().gridTabView,
isInDebugMode = Config.channel.isDebug ||
requireComponents.settings.showSecretDebugMenuThisSession,
shouldShowInactiveTabsAutoCloseDialog = shouldShowInactiveTabsAutoCloseDialog =
requireContext().settings()::shouldShowInactiveTabsAutoCloseDialog, requireContext().settings()::shouldShowInactiveTabsAutoCloseDialog,
onTabPageClick = { page -> onTabPageClick = { page ->

Loading…
Cancel
Save