feat(design-system): add MsSearchableSelect component and update roadmap
- Introduced `MsSearchableSelect` for autocomplete search and selection of objects like riders, horses, or clubs. - Updated `Frontend_Komponenten_Roadmap.md` to mark Phase 3 as complete. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
@@ -39,13 +39,13 @@ Turniermanagement bedeutet Arbeit mit Listen. Wir benötigen mächtige, aber kom
|
||||
* [x] Filter-Chips für schnelle Status-Wechsel.
|
||||
* [x] Anzeige der Trefferanzahl (Result Count).
|
||||
|
||||
## Phase 3: Formular- & Eingabe-System (Die Datenerfassung) 🔵 [IN ARBEIT]
|
||||
## Phase 3: Formular- & Eingabe-System (Die Datenerfassung) ✅ [ABGESCHLOSSEN]
|
||||
|
||||
Eingabe von Stammdaten muss schnell und fehlerfrei erfolgen.
|
||||
|
||||
* [x] **`MsEnumDropdown`:** Automatisches Mapping von Backend-Enums (ÖTO) auf UI-Auswahl.
|
||||
* [x] **`MsValidationWrapper`:** Konsistente Anzeige von Fehlern und Warnungen (z.B. ÖTO-Validierungsregeln).
|
||||
* [ ] **`MsSearchableSelect`:** Für die Verknüpfung von Reitern/Pferden (Autocomplete).
|
||||
* [x] **`MsSearchableSelect`:** Für die Verknüpfung von Reitern/Pferden (Autocomplete-Suche).
|
||||
|
||||
## Phase 4: Layout-Patterns & Navigation ⚪ [ZUKUNFT]
|
||||
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package at.mocode.frontend.core.designsystem.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
|
||||
/**
|
||||
* Eine Komponente zur Suche und Auswahl von Objekten aus einer Liste.
|
||||
* Ideal für die Auswahl von Reitern, Pferden oder Vereinen.
|
||||
*
|
||||
* @param label Das Label über dem Auswahlfeld.
|
||||
* @param selectedOption Das aktuell gewählte Objekt.
|
||||
* @param onOptionSelected Callback bei Auswahl eines Objekts.
|
||||
* @param onSearchQueryChange Callback bei Änderung des Suchbegriffs (für API-Calls).
|
||||
* @param options Die aktuell verfügbaren Optionen (Suchergebnisse).
|
||||
* @param optionLabel Transformation des Objekts in einen lesbaren Text.
|
||||
* @param modifier Modifier für die gesamte Komponente.
|
||||
*/
|
||||
@Composable
|
||||
fun <T> MsSearchableSelect(
|
||||
label: String,
|
||||
selectedOption: T?,
|
||||
onOptionSelected: (T) -> Unit,
|
||||
onSearchQueryChange: (String) -> Unit,
|
||||
options: List<T>,
|
||||
modifier: Modifier = Modifier,
|
||||
optionLabel: (T) -> String = { it.toString() },
|
||||
enabled: Boolean = true,
|
||||
placeholder: String = "Suchen & Auswählen..."
|
||||
) {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
var searchText by remember { mutableStateOf("") }
|
||||
|
||||
Column(modifier = modifier) {
|
||||
// --- 1. Das Anzeige-Feld (sieht aus wie ein TextField, öffnet aber den Dialog) ---
|
||||
OutlinedTextField(
|
||||
value = selectedOption?.let { optionLabel(it) } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(label, style = MaterialTheme.typography.bodySmall) },
|
||||
placeholder = { Text(placeholder, style = MaterialTheme.typography.bodySmall) },
|
||||
trailingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = enabled) { showDialog = true },
|
||||
enabled = enabled,
|
||||
singleLine = true,
|
||||
textStyle = MaterialTheme.typography.bodyMedium,
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
|
||||
// --- 2. Der Such-Dialog (Desktop-zentriert) ---
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = { showDialog = false }) {
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
tonalElevation = 8.dp,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.8f)
|
||||
.fillMaxHeight(0.7f)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
// Internes Suchfeld im Dialog
|
||||
OutlinedTextField(
|
||||
value = searchText,
|
||||
onValueChange = {
|
||||
searchText = it
|
||||
onSearchQueryChange(it)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = { Text("Suchbegriff eingeben...") },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||
singleLine = true,
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Ergebnisliste
|
||||
LazyColumn(modifier = Modifier.weight(1f)) {
|
||||
items(options) { option ->
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = optionLabel(option),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
onOptionSelected(option)
|
||||
showDialog = false
|
||||
searchText = ""
|
||||
}
|
||||
)
|
||||
HorizontalDivider(
|
||||
thickness = 0.5.dp,
|
||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = { showDialog = false }) {
|
||||
Text("Abbrechen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user