Bug 1821425 - Add the logic to show the selected tab UI and the multi selection tab UI

fenix/114.1.0
Noah Bond 1 year ago committed by mergify[bot]
parent 45a57fa73d
commit 003db9a42f

@ -27,8 +27,10 @@ import androidx.compose.ui.unit.dp
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.ContentState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.lib.state.ext.observeAsComposableState
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppState
@ -41,6 +43,8 @@ import org.mozilla.fenix.theme.FirefoxTheme
/**
* Top-level UI for displaying the Tabs Tray feature.
*
* @param appStore [AppStore] used to listen for changes to [AppState].
* @param browserStore [BrowserStore] used to listen for changes to [BrowserState].
* @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 onTabClose Invoked when the user clicks to close a tab.
@ -64,6 +68,7 @@ import org.mozilla.fenix.theme.FirefoxTheme
@Composable
fun TabsTray(
appStore: AppStore,
browserStore: BrowserStore,
tabsTrayStore: TabsTrayStore,
displayTabsInGrid: Boolean,
shouldShowInactiveTabsAutoCloseDialog: (Int) -> Boolean,
@ -81,6 +86,8 @@ fun TabsTray(
onInactiveTabClick: (TabSessionState) -> Unit,
onInactiveTabClose: (TabSessionState) -> Unit,
) {
val selectedTabId = browserStore
.observeAsComposableState { state -> state.selectedTabId }.value
val multiselectMode = tabsTrayStore
.observeAsComposableState { state -> state.mode }.value ?: TabsTrayState.Mode.Normal
val selectedPage = tabsTrayStore
@ -168,6 +175,8 @@ fun TabsTray(
TabLayout(
tabs = normalTabs,
displayTabsInGrid = displayTabsInGrid,
selectedTabId = selectedTabId,
selectionMode = multiselectMode,
onTabClose = onTabClose,
onTabMediaClick = onTabMediaClick,
onTabClick = handleTabClick,
@ -179,6 +188,8 @@ fun TabsTray(
TabLayout(
tabs = privateTabs,
displayTabsInGrid = displayTabsInGrid,
selectedTabId = selectedTabId,
selectionMode = multiselectMode,
onTabClose = onTabClose,
onTabMediaClick = onTabMediaClick,
onTabClick = handleTabClick,
@ -202,9 +213,11 @@ fun TabsTray(
@LightDarkPreview
@Composable
private fun TabsTrayPreview() {
val tabs = generateFakeTabsList()
TabsTrayPreviewRoot(
displayTabsInGrid = false,
normalTabs = generateFakeTabsList(),
selectedTabId = tabs[0].id,
normalTabs = tabs,
privateTabs = generateFakeTabsList(
tabCount = 7,
isPrivate = true,
@ -212,12 +225,15 @@ private fun TabsTrayPreview() {
)
}
@Suppress("MagicNumber")
@LightDarkPreview
@Composable
private fun TabsTrayMultiSelectPreview() {
val tabs = generateFakeTabsList()
TabsTrayPreviewRoot(
mode = TabsTrayState.Mode.Select(setOf()),
normalTabs = generateFakeTabsList(),
selectedTabId = tabs[0].id,
mode = TabsTrayState.Mode.Select(tabs.take(4).toSet()),
normalTabs = tabs,
)
}
@ -237,7 +253,7 @@ private fun TabsTrayInactiveTabsPreview() {
private fun TabsTrayPrivateTabsPreview() {
TabsTrayPreviewRoot(
selectedPage = Page.PrivateTabs,
privateTabs = generateFakeTabsList(),
privateTabs = generateFakeTabsList(isPrivate = true),
)
}
@ -253,6 +269,7 @@ private fun TabsTraySyncedTabsPreview() {
private fun TabsTrayPreviewRoot(
displayTabsInGrid: Boolean = true,
selectedPage: Page = Page.NormalTabs,
selectedTabId: String? = null,
mode: TabsTrayState.Mode = TabsTrayState.Mode.Normal,
normalTabs: List<TabSessionState> = emptyList(),
inactiveTabs: List<TabSessionState> = emptyList(),
@ -272,19 +289,26 @@ private fun TabsTrayPreviewRoot(
inactiveTabsExpanded = inactiveTabsExpandedState,
),
)
val browserStore = BrowserStore(
initialState = BrowserState(
tabs = normalTabs + privateTabs,
selectedTabId = selectedTabId,
),
)
val tabsTrayStore = TabsTrayStore(
initialState = TabsTrayState(
selectedPage = selectedPageState,
mode = mode,
inactiveTabs = inactiveTabsState,
normalTabs = normalTabsState,
privateTabs = privateTabs,
privateTabs = privateTabsState,
),
)
FirefoxTheme {
TabsTray(
appStore = appStore,
browserStore = browserStore,
tabsTrayStore = tabsTrayStore,
displayTabsInGrid = displayTabsInGrid,
shouldShowInactiveTabsAutoCloseDialog = { true },
@ -300,8 +324,16 @@ private fun TabsTrayPreviewRoot(
},
onTabMediaClick = {},
onTabClick = {},
onTabMultiSelectClick = {},
onTabLongClick = {},
onTabMultiSelectClick = { tab ->
if (tabsTrayStore.state.mode.selectedTabs.contains(tab)) {
tabsTrayStore.dispatch(TabsTrayAction.RemoveSelectTab(tab))
} else {
tabsTrayStore.dispatch(TabsTrayAction.AddSelectTab(tab))
}
},
onTabLongClick = { tab ->
tabsTrayStore.dispatch(TabsTrayAction.AddSelectTab(tab))
},
onInactiveTabsHeaderClick = {
inactiveTabsExpandedState = !inactiveTabsExpandedState
},

@ -37,6 +37,7 @@ import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.tabstray.browser.InactiveTabsController
import org.mozilla.fenix.tabstray.browser.TabsTrayFabController
import org.mozilla.fenix.tabstray.ext.isActiveDownload
import org.mozilla.fenix.tabstray.ext.isNormalTab
import org.mozilla.fenix.tabstray.ext.isSelect
import org.mozilla.fenix.utils.Settings
import java.util.concurrent.TimeUnit
@ -404,7 +405,7 @@ class DefaultTabsTrayController(
}
override fun handleTabLongClick(tab: TabSessionState): Boolean {
return if (tabsTrayStore.state.mode.selectedTabs.isEmpty()) {
return if (tab.isNormalTab() && tabsTrayStore.state.mode.selectedTabs.isEmpty()) {
Collections.longPress.record(NoExtras())
tabsTrayStore.dispatch(TabsTrayAction.AddSelectTab(tab))
true

@ -220,6 +220,7 @@ class TabsTrayFragment : AppCompatDialogFragment() {
FirefoxTheme(theme = Theme.getTheme(allowPrivateTheme = false)) {
TabsTray(
appStore = requireComponents.appStore,
browserStore = requireComponents.core.store,
tabsTrayStore = tabsTrayStore,
displayTabsInGrid = requireContext().settings().gridTabView,
shouldShowInactiveTabsAutoCloseDialog =

@ -37,6 +37,9 @@ import org.mozilla.fenix.theme.FirefoxTheme
*
* @param tabs The list of [TabSessionState] to display.
* @param displayTabsInGrid Whether the tabs should be displayed in a grid.
* @param selectedTabId The ID of the currently selected tab.
* @param selectionMode [TabsTrayState.Mode] indicating whether the Tabs Tray is in single selection
* or multi-selection and contains the set of selected tabs.
* @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 onTabClick Invoked when the user clicks on a tab.
@ -48,6 +51,8 @@ import org.mozilla.fenix.theme.FirefoxTheme
fun TabLayout(
tabs: List<TabSessionState>,
displayTabsInGrid: Boolean,
selectedTabId: String?,
selectionMode: TabsTrayState.Mode,
onTabClose: (TabSessionState) -> Unit,
onTabMediaClick: (TabSessionState) -> Unit,
onTabClick: (TabSessionState) -> Unit,
@ -57,6 +62,8 @@ fun TabLayout(
if (displayTabsInGrid) {
TabGrid(
tabs = tabs,
selectedTabId = selectedTabId,
selectionMode = selectionMode,
onTabClose = onTabClose,
onTabMediaClick = onTabMediaClick,
onTabClick = onTabClick,
@ -66,6 +73,8 @@ fun TabLayout(
} else {
TabList(
tabs = tabs,
selectedTabId = selectedTabId,
selectionMode = selectionMode,
onTabClose = onTabClose,
onTabMediaClick = onTabMediaClick,
onTabClick = onTabClick,
@ -75,9 +84,12 @@ fun TabLayout(
}
}
@Suppress("LongParameterList")
@Composable
private fun TabGrid(
tabs: List<TabSessionState>,
selectedTabId: String?,
selectionMode: TabsTrayState.Mode,
onTabClose: (TabSessionState) -> Unit,
onTabMediaClick: (TabSessionState) -> Unit,
onTabClick: (TabSessionState) -> Unit,
@ -86,6 +98,7 @@ private fun TabGrid(
) {
val state = rememberLazyGridState()
val tabListBottomPadding = dimensionResource(id = R.dimen.tab_tray_list_bottom_padding)
val isInMultiSelectMode = selectionMode is TabsTrayState.Mode.Select
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = MIN_COLUMN_WIDTH_DP.dp),
@ -104,6 +117,9 @@ private fun TabGrid(
) { tab ->
TabGridItem(
tab = tab,
isSelected = tab.id == selectedTabId,
multiSelectionEnabled = isInMultiSelectMode,
multiSelectionSelected = selectionMode.selectedTabs.contains(tab),
onCloseClick = onTabClose,
onMediaClick = onTabMediaClick,
onClick = onTabClick,
@ -117,9 +133,12 @@ private fun TabGrid(
}
}
@Suppress("LongParameterList")
@Composable
private fun TabList(
tabs: List<TabSessionState>,
selectedTabId: String?,
selectionMode: TabsTrayState.Mode,
onTabClose: (TabSessionState) -> Unit,
onTabMediaClick: (TabSessionState) -> Unit,
onTabClick: (TabSessionState) -> Unit,
@ -128,6 +147,7 @@ private fun TabList(
) {
val state = rememberLazyListState()
val tabListBottomPadding = dimensionResource(id = R.dimen.tab_tray_list_bottom_padding)
val isInMultiSelectMode = selectionMode is TabsTrayState.Mode.Select
LazyColumn(
modifier = Modifier.fillMaxSize(),
@ -145,6 +165,9 @@ private fun TabList(
) { tab ->
TabListItem(
tab = tab,
isSelected = tab.id == selectedTabId,
multiSelectionEnabled = isInMultiSelectMode,
multiSelectionSelected = selectionMode.selectedTabs.contains(tab),
onCloseClick = onTabClose,
onMediaClick = onTabMediaClick,
onClick = onTabClick,
@ -171,6 +194,8 @@ private fun TabListPreview() {
) {
TabLayout(
tabs = tabs,
selectedTabId = tabs[1].id,
selectionMode = TabsTrayState.Mode.Normal,
displayTabsInGrid = false,
onTabClose = tabs::remove,
onTabMediaClick = {},
@ -194,6 +219,8 @@ private fun TabGridPreview() {
) {
TabLayout(
tabs = tabs,
selectedTabId = tabs[0].id,
selectionMode = TabsTrayState.Mode.Normal,
displayTabsInGrid = true,
onTabClose = tabs::remove,
onTabMediaClick = {},
@ -204,6 +231,39 @@ private fun TabGridPreview() {
}
}
@Suppress("MagicNumber")
@LightDarkPreview
@Composable
private fun TabGridMultiSelectPreview() {
val tabs = generateFakeTabsList()
val selectedTabs = remember { tabs.take(4).toMutableStateList() }
FirefoxTheme {
Box(
modifier = Modifier
.fillMaxSize()
.background(FirefoxTheme.colors.layer1),
) {
TabLayout(
tabs = tabs,
selectedTabId = tabs[0].id,
selectionMode = TabsTrayState.Mode.Select(selectedTabs.toSet()),
displayTabsInGrid = false,
onTabClose = {},
onTabMediaClick = {},
onTabClick = { tab ->
if (selectedTabs.contains(tab)) {
selectedTabs.remove(tab)
} else {
selectedTabs.add(tab)
}
},
onTabLongClick = {},
)
}
}
}
private fun generateFakeTabsList(tabCount: Int = 10, isPrivate: Boolean = false): List<TabSessionState> =
List(tabCount) { index ->
TabSessionState(

@ -47,6 +47,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Collections
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.TabsTray
import org.mozilla.fenix.HomeActivity
@ -945,6 +946,53 @@ class DefaultTabsTrayControllerTest {
assertFalse(navigateToHomeAndDeleteSessionInvoked)
}
@Test
fun `GIVEN no tabs are currently selected WHEN a normal tab is long clicked THEN the tab is selected and the metric is reported`() {
val normalTab = TabSessionState(
content = ContentState(url = "https://simulate.com", private = false),
id = "normalTab",
)
every { trayStore.state.mode.selectedTabs } returns emptySet()
assertNull(Collections.longPress.testGetValue())
createController().handleTabLongClick(normalTab)
assertNotNull(Collections.longPress.testGetValue())
verify { trayStore.dispatch(TabsTrayAction.AddSelectTab(normalTab)) }
}
@Test
fun `GIVEN at least one tab is selected WHEN a normal tab is long clicked THEN the long click is ignored`() {
val normalTabClicked = TabSessionState(
content = ContentState(url = "https://simulate.com", private = false),
id = "normalTab",
)
val alreadySelectedTab = TabSessionState(
content = ContentState(url = "https://simulate.com", private = false),
id = "selectedTab",
)
every { trayStore.state.mode.selectedTabs } returns setOf(alreadySelectedTab)
createController().handleTabLongClick(normalTabClicked)
assertNull(Collections.longPress.testGetValue())
verify(exactly = 0) { trayStore.dispatch(any()) }
}
@Test
fun `WHEN a private tab is long clicked THEN the long click is ignored`() {
val privateTab = TabSessionState(
content = ContentState(url = "https://simulate.com", private = true),
id = "privateTab",
)
createController().handleTabLongClick(privateTab)
assertNull(Collections.longPress.testGetValue())
verify(exactly = 0) { trayStore.dispatch(any()) }
}
private fun createController(
navigateToHomeAndDeleteSession: (String) -> Unit = { },
selectTabPosition: (Int, Boolean) -> Unit = { _, _ -> },

Loading…
Cancel
Save