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

Class Class, % Method, % Branch, % Line, % Instruction, %
SurferCurrencyOption 100% (1/1) 100% (1/1) 100% (4/4) 100% (17/17)


 package com.georgeci.moneysurfer.uikit.components
 
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ModalBottomSheet
 import androidx.compose.material3.Text
 import androidx.compose.material3.rememberModalBottomSheetState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import com.georgeci.moneysurfer.uikit.theme.AppTheme
 
 data class SurferCurrencyOption(
     val code: String,
     val symbol: String,
     val name: String,
 )
 
 /**
  * Currency chooser as a modal bottom sheet: title, search field, and one [SurferCurrencyRow]
  * card per currency. The list is as tall as the whole currency set needs, capped at
  * [ListMaxHeight] — a short set reads as a compact sheet instead of a full-height page, and a
  * search query narrows the rows without resizing the sheet under the user's fingers.
  *
  * Departure from the design: the mockup has no search field. The supported list keeps growing,
  * so the field stays — it is the reason this sheet replaced the segmented control at all.
  */
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun SurferCurrencyBottomSheet(
     title: String,
     searchPlaceholder: String,
     currencies: List<SurferCurrencyOption>,
     selectedCode: String?,
     onSelect: (SurferCurrencyOption) -> Unit,
     onDismiss: () -> Unit,
 ) {
     val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
 
     ModalBottomSheet(
         onDismissRequest = onDismiss,
         sheetState = sheetState,
         containerColor = AppTheme.materialColors.surfaceContainerLow,
         shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp),
     ) {
         SurferCurrencyBottomSheetContent(
             title = title,
             searchPlaceholder = searchPlaceholder,
             currencies = currencies,
             selectedCode = selectedCode,
             onSelect = onSelect,
         )
     }
 }
 
 /** Stable selectors for [SurferCurrencyBottomSheet] — see docs/testing/testing-strategy.md. */
 object SurferCurrencyBottomSheetTestTags {
     const val Search = "currencySheet:search"
     const val List = "currencySheet:list"
 }
 
 /**
  * The sheet's body, without the modal shell — public so a UI test can mount it directly, the way
  * `DashboardCardStyleSheetContent` is.
  */
 @Composable
 fun SurferCurrencyBottomSheetContent(
     title: String,
     searchPlaceholder: String,
     currencies: List<SurferCurrencyOption>,
     selectedCode: String?,
     onSelect: (SurferCurrencyOption) -> Unit,
     modifier: Modifier = Modifier,
 ) {
     var query by remember { mutableStateOf("") }
     val filtered = remember(currencies, query) {
         if (query.isBlank()) {
             currencies
         } else {
             val q = query.trim().lowercase()
             currencies.filter { c ->
                 c.code.lowercase().contains(q) ||
                     c.name.lowercase().contains(q) ||
                     c.symbol.contains(q)
             }
         }
     }
 
     Column(modifier = modifier.fillMaxWidth().padding(bottom = 12.dp)) {
         Text(
             text = title,
             style = AppTheme.typography.titleMedium,
             color = AppTheme.materialColors.onSurface,
             modifier = Modifier.padding(start = 20.dp, end = 20.dp, top = 4.dp, bottom = 12.dp),
         )
         SurferSearchField(
             value = query,
             onValueChange = { query = it },
             placeholder = searchPlaceholder,
             modifier = Modifier.testTag(SurferCurrencyBottomSheetTestTags.Search),
         )
         Spacer(Modifier.height(8.dp))
         LazyColumn(
             // Sized from the *unfiltered* list, so typing a query filters the rows without the
             // sheet collapsing around the one that matched and re-growing when it is cleared.
             modifier = Modifier
                 .fillMaxWidth()
                 .height(listHeight(currencies.size))
                 .testTag(SurferCurrencyBottomSheetTestTags.List),
             contentPadding = PaddingValues(horizontal = 16.dp, vertical = 4.dp),
             verticalArrangement = Arrangement.spacedBy(10.dp),
         ) {
             items(filtered, key = { it.code }) { currency ->
                 SurferCurrencyRow(
                     symbol = currency.symbol,
                     code = currency.code,
                     name = currency.name,
                     selected = currency.code == selectedCode,
                     onClick = { onSelect(currency) },
                 )
             }
         }
     }
 }
 
 /**
  * How tall the list is for [currencyCount] currencies: every row it can show, capped at
  * [ListMaxHeight]. An estimate per row rather than a measured one — the only thing it has to get
  * right is that the height does not depend on the *filtered* count, and a few dp of slack under a
  * list too short to fill the sheet is cheaper than a sheet that resizes on every keystroke.
  */
 private fun listHeight(currencyCount: Int): Dp =
     minOf(RowHeight * currencyCount, ListMaxHeight)
 
 private val ListMaxHeight = 420.dp
 
 /** One [SurferCurrencyRow] plus the 10dp gap under it. */
 private val RowHeight = 76.dp