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 f4293def17..9a4461bc6f 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 = Modifier.height(16.dp))
+
+ SyncedTabsSectionHeader(
+ sectionText = "Collapsible Google Pixel Pro Max +Ultra 5000",
+ expanded = true,
+ ) { println("Clicked section header") }
- Spacer(Modifier.height(16.dp))
+ 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 58c6c31881..cb3aa2551d 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