Coverage Summary for Class: SurferToolbarKt (com.georgeci.moneysurfer.uikit.components.base)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| SurferToolbarKt |
100%
(1/1)
|
61.1%
(11/18)
|
43.8%
(35/80)
|
95.7%
(90/94)
|
89.5%
(1371/1532)
|
package com.georgeci.moneysurfer.uikit.components.base
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.georgeci.moneysurfer.uikit.icons.SurferIcons
import com.georgeci.moneysurfer.uikit.navigation.LocalSurferCanNavigateBack
import com.georgeci.moneysurfer.uikit.preview.SurferComponentPreview
import com.georgeci.moneysurfer.uikit.semantics.SurferSemantics
import com.georgeci.moneysurfer.uikit.theme.AppTheme
import moneysurfer.uikit.generated.resources.Res
import moneysurfer.uikit.generated.resources.uikit_back
import org.jetbrains.compose.resources.stringResource
/**
* The app's top app bar.
*
* @param onBack drawn as the navigation icon, but only while [LocalSurferCanNavigateBack] says the
* affordance leads somewhere. A screen states what back *means* for it; whether there is anything
* to go back to is the navigation host's to know, and a section the drawer reset to has nothing.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SurferToolbar(
title: String,
modifier: Modifier = Modifier,
onBack: (() -> Unit)? = null,
navigationIcon: (@Composable () -> Unit)? = null,
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
actions: @Composable RowScope.() -> Unit = {},
) {
val canNavigateBack = LocalSurferCanNavigateBack.current
SurferToolbar(
modifier = modifier,
navigationIcon = {
when {
onBack != null && canNavigateBack -> IconButton(
onClick = onBack,
modifier = Modifier.testTag(SurferToolbarTestTags.Back),
) {
Icon(
imageVector = SurferIcons.Back,
contentDescription = stringResource(Res.string.uikit_back),
)
}
navigationIcon != null -> navigationIcon()
}
},
colors = colors,
actions = actions,
title = { Text(title) },
)
}
/**
* Stable selectors for [SurferToolbar] — see docs/testing/testing-strategy.md.
*
* [Back] is on the navigation [IconButton] every screen with an `onBack` gets, so an E2E flow
* can go back by id instead of matching the localized "Back" content description.
*/
object SurferToolbarTestTags {
const val Back = "toolbar:back"
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SurferToolbar(
modifier: Modifier = Modifier,
navigationIcon: @Composable () -> Unit = {},
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
actions: @Composable RowScope.() -> Unit = {},
title: @Composable () -> Unit,
) {
TopAppBar(
title = title,
modifier = modifier,
navigationIcon = navigationIcon,
actions = {
SurferSyncStatusBadge()
actions()
},
colors = colors,
)
}
/**
* Icon action for the trailing edge of [SurferToolbar] / [SurferDashboardToolbar]. Wraps
* [IconButton] so every toolbar action gets the same 48dp tap target, tint, and content-
* description handling without repeating boilerplate at every call site.
*/
@Composable
fun SurferToolbarAction(
icon: ImageVector,
contentDescription: String?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
tint: Color = AppTheme.materialColors.onSurface,
) {
IconButton(onClick = onClick, modifier = modifier) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = tint,
)
}
}
/**
* Pill-shaped tonal button with a leading icon and a label, sized for the trailing edge of
* [SurferToolbar]. Use for primary commit-style toolbar actions ("Save", "Update") that need
* more weight than a [SurferToolbarAction] icon.
*/
@Composable
fun SurferToolbarButtonAction(
icon: ImageVector,
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
FilledTonalButton(
onClick = onClick,
enabled = enabled,
shape = RoundedCornerShape(18.dp),
modifier = modifier.padding(end = 8.dp),
) {
Icon(
imageVector = icon,
contentDescription = SurferSemantics.Decorative,
modifier = Modifier.size(16.dp),
)
Spacer(Modifier.width(6.dp))
Text(text = text, style = AppTheme.typography.labelLarge)
}
}
/**
* Dashboard-flavoured top app bar: a colored letter bubble on the left, a two-line title
* (small label + larger headline), and trailing actions. Use for the home/dashboard screen
* where the toolbar needs to identify the active workspace.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SurferDashboardToolbar(
letter: String,
primaryText: String,
secondaryText: String,
modifier: Modifier = Modifier,
letterBackgroundColor: Color = AppTheme.materialColors.primaryContainer,
letterContentColor: Color = AppTheme.materialColors.onPrimaryContainer,
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
actions: @Composable RowScope.() -> Unit = {},
) {
SurferToolbar(
modifier = modifier,
navigationIcon = {
Box(
modifier = Modifier
.padding(start = 12.dp)
.size(40.dp)
.clip(CircleShape)
.background(letterBackgroundColor),
contentAlignment = Alignment.Center,
) {
Text(
text = letter,
style = AppTheme.typography.titleSmall,
color = letterContentColor,
)
}
},
colors = colors,
actions = actions,
title = {
Column {
Text(
text = primaryText,
style = AppTheme.typography.labelMedium,
color = AppTheme.materialColors.onSurfaceVariant,
)
Text(
text = secondaryText,
style = AppTheme.typography.titleSmall,
color = AppTheme.materialColors.onSurface,
)
}
},
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
private fun SurferToolbarPreview() {
SurferComponentPreview {
SurferToolbar(title = "Screen Title")
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
private fun SurferToolbarWithBackPreview() {
SurferComponentPreview {
SurferToolbar(
title = "Details with back",
// onBack = {},
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
private fun SurferToolbarWithActionsPreview() {
SurferComponentPreview {
SurferToolbar(
title = "Dashboard",
actions = {
SurferToolbarAction(
icon = SurferIcons.Settings,
contentDescription = "Settings",
onClick = {},
)
},
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
private fun SurferToolbarFullPreview() {
SurferComponentPreview {
SurferToolbar(
title = "Edit Account",
onBack = {},
actions = {
SurferToolbarAction(
icon = SurferIcons.Delete,
contentDescription = "Delete",
onClick = {},
)
},
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
private fun SurferToolbarWithButtonActionPreview() {
SurferComponentPreview {
SurferToolbar(
title = "New transaction",
onBack = {},
actions = {
SurferToolbarButtonAction(
icon = SurferIcons.Check,
text = "Save",
onClick = {},
)
},
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
private fun SurferToolbarWithButtonActionDisabledPreview() {
SurferComponentPreview {
SurferToolbar(
title = "Edit account",
onBack = {},
actions = {
SurferToolbarButtonAction(
icon = SurferIcons.Check,
text = "Update",
onClick = {},
enabled = false,
)
},
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
private fun SurferDashboardToolbarPreview() {
SurferComponentPreview {
SurferDashboardToolbar(
letter = "F",
primaryText = "Family",
secondaryText = "Good evening, Kasia",
actions = {
SurferToolbarAction(
icon = SurferIcons.Notifications,
contentDescription = "Notifications",
onClick = {},
)
SurferToolbarAction(
icon = SurferIcons.Settings,
contentDescription = "Settings",
onClick = {},
)
},
)
}
}