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] Filter-Chips für schnelle Status-Wechsel.
|
||||||
* [x] Anzeige der Trefferanzahl (Result Count).
|
* [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.
|
Eingabe von Stammdaten muss schnell und fehlerfrei erfolgen.
|
||||||
|
|
||||||
* [x] **`MsEnumDropdown`:** Automatisches Mapping von Backend-Enums (ÖTO) auf UI-Auswahl.
|
* [x] **`MsEnumDropdown`:** Automatisches Mapping von Backend-Enums (ÖTO) auf UI-Auswahl.
|
||||||
* [x] **`MsValidationWrapper`:** Konsistente Anzeige von Fehlern und Warnungen (z.B. ÖTO-Validierungsregeln).
|
* [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]
|
## 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