diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabs.kt b/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabs.kt index f4293def1..9a4461bc6 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabs.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabs.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize @@ -33,6 +34,8 @@ import androidx.compose.material.Divider import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -54,10 +57,13 @@ import org.mozilla.fenix.compose.SecondaryText import org.mozilla.fenix.theme.FirefoxTheme import mozilla.components.browser.storage.sync.Tab as SyncTab +private const val EXPANDED_BY_DEFAULT = true + /** * Top-level list UI for displaying Synced Tabs in the Tabs Tray. * * @param syncedTabs The tab UI items to be displayed. + * @param taskContinuityEnabled Indicates whether the Task Continuity enhancements should be visible for users. * @param onTabClick The lambda for handling clicks on synced tabs. */ @OptIn(ExperimentalFoundationApi::class) @@ -68,30 +74,40 @@ fun SyncedTabsList( onTabClick: (SyncTab) -> Unit, ) { val listState = rememberLazyListState() + val expandedState = remember(syncedTabs) { syncedTabs.map { EXPANDED_BY_DEFAULT }.toMutableStateList() } LazyColumn( modifier = Modifier.fillMaxSize(), state = listState, ) { if (taskContinuityEnabled) { - syncedTabs.forEach { syncedTabItem -> + syncedTabs.forEachIndexed { index, syncedTabItem -> when (syncedTabItem) { is SyncedTabsListItem.DeviceSection -> { + val sectionExpanded = expandedState[index] + stickyHeader { - SyncedTabsDeviceItem(deviceName = syncedTabItem.displayName) + SyncedTabsSectionHeader( + sectionText = syncedTabItem.displayName, + expanded = sectionExpanded, + ) { + expandedState[index] = !sectionExpanded + } } - if (syncedTabItem.tabs.isNotEmpty()) { - items(syncedTabItem.tabs) { syncedTab -> - SyncedTabsTabItem( - tabTitleText = syncedTab.displayTitle, - url = syncedTab.displayURL, - ) { - onTabClick(syncedTab.tab) + if (sectionExpanded) { + if (syncedTabItem.tabs.isNotEmpty()) { + items(syncedTabItem.tabs) { syncedTab -> + SyncedTabsTabItem( + tabTitleText = syncedTab.displayTitle, + url = syncedTab.displayURL, + ) { + onTabClick(syncedTab.tab) + } } + } else { + item { SyncedTabsNoTabsItem() } } - } else { - item { SyncedTabsNoTabsItem() } } } @@ -111,7 +127,7 @@ fun SyncedTabsList( } else { items(syncedTabs) { syncedTabItem -> when (syncedTabItem) { - is SyncedTabsListItem.Device -> SyncedTabsDeviceItem(deviceName = syncedTabItem.displayName) + is SyncedTabsListItem.Device -> SyncedTabsSectionHeader(sectionText = syncedTabItem.displayName) is SyncedTabsListItem.Error -> SyncedTabsErrorItem( errorText = syncedTabItem.errorText, errorButton = syncedTabItem.errorButton @@ -135,32 +151,57 @@ fun SyncedTabsList( item { // The Spacer here is to act as a footer to add padding to the bottom of the list so // the FAB or any potential SnackBar doesn't overlap with the items at the end. - Spacer(Modifier.height(240.dp)) + Spacer(modifier = Modifier.height(240.dp)) } } } /** - * Text header for sections of synced tabs + * Collapsible header for sections of synced tabs * - * @param deviceName The name of the user's device connected that has synced tabs. + * @param sectionText The section title for a group of synced tabs. + * @param expanded Indicates whether the section of content is expanded. If null, the Icon will be hidden. + * @param onClick Optional lambda for handling section header clicks. */ @Composable -fun SyncedTabsDeviceItem(deviceName: String) { +fun SyncedTabsSectionHeader( + sectionText: String, + expanded: Boolean? = null, + onClick: () -> Unit = {}, +) { Column( modifier = Modifier .fillMaxWidth() .background(FirefoxTheme.colors.layer1) + .clickable(onClick = onClick) ) { - PrimaryText( - text = deviceName, + Row( modifier = Modifier .fillMaxWidth() - .padding(start = 16.dp, top = 16.dp, end = 8.dp, bottom = 8.dp), - fontSize = 14.sp, - fontFamily = FontFamily(Font(R.font.metropolis_semibold)), - maxLines = 1 - ) + .padding(top = 16.dp, bottom = 8.dp, start = 16.dp, end = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + PrimaryText( + text = sectionText, + fontSize = 14.sp, + fontFamily = FontFamily(Font(R.font.metropolis_semibold)), + maxLines = 1, + ) + + expanded?.let { + Spacer(modifier = Modifier.width(8.dp)) + + Icon( + painter = painterResource( + if (expanded) R.drawable.ic_chevron_down else R.drawable.ic_chevron_up + ), + contentDescription = stringResource( + if (expanded) R.string.synced_tabs_collapse_group else R.string.synced_tabs_expand_group, + ), + tint = FirefoxTheme.colors.textPrimary, + ) + } + } Divider(color = FirefoxTheme.colors.borderPrimary) } @@ -272,7 +313,7 @@ fun SyncedTabsErrorButton(buttonText: String, onClick: () -> Unit) { tint = FirefoxTheme.colors.textOnColorPrimary, ) - Spacer(Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(8.dp)) Text( text = buttonText, @@ -305,21 +346,28 @@ fun SyncedTabsNoTabsItem() { private fun SyncedTabsListItemsPreview() { FirefoxTheme { Column(Modifier.background(FirefoxTheme.colors.layer1)) { - SyncedTabsDeviceItem(deviceName = "Google Pixel Pro Max +Ultra 5000") + SyncedTabsSectionHeader(sectionText = "Google Pixel Pro Max +Ultra 5000") - Spacer(Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) + + SyncedTabsSectionHeader( + sectionText = "Collapsible Google Pixel Pro Max +Ultra 5000", + expanded = true, + ) { println("Clicked section header") } + + Spacer(modifier = Modifier.height(16.dp)) SyncedTabsTabItem(tabTitleText = "Mozilla", url = "www.mozilla.org") { println("Clicked tab") } - Spacer(Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) SyncedTabsErrorItem(errorText = stringResource(R.string.synced_tabs_reauth)) - Spacer(Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) SyncedTabsNoTabsItem() - Spacer(Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 58c6c3188..cb3aa2551 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1647,6 +1647,10 @@ Sign in to sync No open tabs + + Expand group of synced tabs + + Collapse group of synced tabs