Coverage Summary for Class: SurferPeriodSwitchKt (com.georgeci.moneysurfer.uikit.components.base)

Class Class, % Method, % Branch, % Line, % Instruction, %
SurferPeriodSwitchKt 100% (1/1) 66.7% (2/3) 58.3% (14/24) 100% (44/44) 97.1% (466/480)


 package com.georgeci.moneysurfer.uikit.components.base
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.selection.selectable
 import androidx.compose.foundation.selection.selectableGroup
 import androidx.compose.foundation.shape.RoundedCornerShape
 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.preview.SurferComponentPreview
 import com.georgeci.moneysurfer.uikit.theme.AppTheme
 
 /** Track height, and the height of each segment's tap target. */
 private val SwitchHeight = 32.dp
 
 /** Margin the thumb is drawn inside its slot — visual only, the whole slot is tappable. */
 private val TrackInset = 3.dp
 
 /** Horizontal breathing room around a segment's label, inside the thumb. */
 private val OptionPadding = 14.dp
 
 /**
  * Segmented chip for the span a whole screen is read at — Week vs Month over the dashboard's
  * spend widgets. A pill track carrying one raised thumb, so the unselected options still read as
  * part of one control rather than as separate buttons.
  *
  * Distinct from [SurferSegmentedControl] by role, not just by paint: that one is a full-width
  * form field for picking a value the user is about to save, and states the selection twice (fill
  * plus a check) because getting it wrong costs a round trip. This is inline screen chrome, sized
  * to its labels, sitting beside content it re-reads immediately — so it drops the checks and the
  * dividers, which at header size are noise around two words.
  *
  * [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> SurferPeriodSwitch(
     options: List<T>,
     selected: T,
     label: @Composable (T) -> String,
     onSelect: (T) -> Unit,
     modifier: Modifier = Modifier,
     optionTestTag: ((T) -> String)? = null,
 ) {
     val pill = RoundedCornerShape(percent = 50)
     Row(
         modifier = modifier
             .height(SwitchHeight)
             .clip(pill)
             .background(AppTheme.materialColors.surfaceContainerHighest)
             .selectableGroup(),
         verticalAlignment = Alignment.CenterVertically,
     ) {
         options.forEach { option ->
             val isSelected = option == selected
             Box(
                 // The tap target is the segment's whole slot, full track height — [TrackInset] is
                 // a margin the thumb is drawn inside, not a border around what responds. A control
                 // painted as a filled pill is aimed at as one, so an inset that ate the top and
                 // bottom of it would turn the obvious target into a near miss. `Modifier.selectable`
                 // is foundation, not Material, so nothing expands the target back out for us.
                 modifier = Modifier
                     .fillMaxHeight()
                     .clip(pill)
                     // `selectable` rather than `clickable`: it publishes the selected state and the
                     // radio-button role, which is what a screen reader needs to say "1 of 2" here.
                     .selectable(selected = isSelected, onClick = { onSelect(option) })
                     .then(optionTestTag?.let { Modifier.testTag(it(option)) } ?: Modifier)
                     .padding(TrackInset),
                 contentAlignment = Alignment.Center,
             ) {
                 Box(
                     modifier = Modifier
                         .fillMaxHeight()
                         .clip(pill)
                         .background(if (isSelected) AppTheme.materialColors.surface else Color.Transparent)
                         .padding(horizontal = OptionPadding),
                     contentAlignment = Alignment.Center,
                 ) {
                     Text(
                         text = label(option),
                         style = AppTheme.typography.labelLarge,
                         color = if (isSelected) {
                             AppTheme.materialColors.onSurface
                         } else {
                             AppTheme.materialColors.onSurfaceVariant
                         },
                         maxLines = 1,
                     )
                 }
             }
         }
     }
 }
 
 @Preview
 @Composable
 private fun SurferPeriodSwitchPreview() {
     SurferComponentPreview {
         SurferPeriodSwitch(
             modifier = Modifier.padding(all = 16.dp),
             options = listOf("Week", "Month"),
             selected = "Month",
             label = { it },
             onSelect = {},
         )
     }
 }