Coverage Summary for Class: SurferSegmentedControlKt (com.georgeci.moneysurfer.uikit.components.base)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| SurferSegmentedControlKt |
100%
(1/1)
|
50%
(1/2)
|
75%
(21/28)
|
100%
(50/50)
|
98.1%
(529/539)
|
package com.georgeci.moneysurfer.uikit.components.base
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
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.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.preview.SurferComponentPreview
import com.georgeci.moneysurfer.uikit.semantics.SurferSemantics
import com.georgeci.moneysurfer.uikit.theme.AppTheme
/**
* Pill-shaped segmented control for picking exactly one value from a small set of options.
* Selected segment gets the secondaryContainer fill plus a leading check; segments are split by
* 1dp outline dividers. Caller renders any section label above the control.
*
* [optionTestTag] tags each segment individually. The row-level [modifier] cannot do that, and
* without a per-segment tag an E2E driver has nothing to aim at but the segment's localized
* label — which is exactly the coupling issue #352 removes.
*/
@Composable
fun <T> SurferSegmentedControl(
options: List<T>,
selected: T,
label: @Composable (T) -> String,
onSelect: (T) -> Unit,
modifier: Modifier = Modifier,
optionTestTag: ((T) -> String)? = null,
) {
val outline = AppTheme.materialColors.outline
Row(
modifier = modifier
.fillMaxWidth()
.height(40.dp)
.clip(RoundedCornerShape(percent = 50))
.border(1.dp, outline, RoundedCornerShape(percent = 50)),
) {
options.forEachIndexed { index, value ->
val isSelected = value == selected
val bg =
if (isSelected) AppTheme.materialColors.secondaryContainer else Color.Transparent
val fg = if (isSelected) {
AppTheme.materialColors.onSecondaryContainer
} else {
AppTheme.materialColors.onSurface
}
Row(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.background(bg)
// `selectable`, not `clickable`: the fill and the check glyph are the only
// things saying which option is active, and the glyph is decorative — without
// this a screen reader announces every segment identically.
.selectable(selected = isSelected, onClick = { onSelect(value) })
.then(optionTestTag?.let { Modifier.testTag(it(value)) } ?: Modifier),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
if (isSelected) {
Icon(
imageVector = SurferIcons.Check,
contentDescription = SurferSemantics.Decorative,
tint = fg,
modifier = Modifier.size(16.dp),
)
Spacer(Modifier.width(6.dp))
}
Text(
text = label(value),
style = AppTheme.typography.labelLarge,
color = fg,
)
}
if (index < options.lastIndex) {
Box(
modifier = Modifier
.width(1.dp)
.fillMaxHeight()
.background(outline),
)
}
}
}
}
@Preview
@Composable
private fun SurferSegmentedControlPreview() {
SurferComponentPreview {
SurferSegmentedControl(
modifier = Modifier.padding(all = 16.dp),
options = listOf("Cash", "Bank", "Card", "Savings"),
selected = "Bank",
label = { it },
onSelect = {},
)
}
}