The TextField component provides a comprehensive solution for text input in Summon applications. It supports various input types, validation, accessibility features, and both controlled and uncontrolled usage patterns.
Text input is fundamental to user interfaces. The Summon TextField component offers:
import code.yousef.summon.components.input.*
@Composable
fun SimpleTextFieldExample() {
var name by remember { mutableStateOf("") }
TextField(
value = name,
onValueChange = { name = it },
placeholder = "Enter your name",
modifier = Modifier().width("300px")
)
}
@Composable
fun LabeledTextFieldExample() {
var email by remember { mutableStateOf("") }
Column(verticalSpacing = "8px") {
Label("Email Address")
TextField(
value = email,
onValueChange = { email = it },
type = TextFieldType.Email,
placeholder = "you@example.com",
modifier = Modifier().width("100%")
)
}
}
@Composable
fun PasswordFieldExample() {
var password by remember { mutableStateOf("") }
var showPassword by remember { mutableStateOf(false) }
Column(verticalSpacing = "8px") {
Label("Password")
Row(
horizontalSpacing = "8px",
verticalAlignment = "center"
) {
TextField(
value = password,
onValueChange = { password = it },
type = if (showPassword) TextFieldType.Text else TextFieldType.Password,
placeholder = "Enter your password",
modifier = Modifier().flex("1")
)
IconButton(
icon = if (showPassword) "visibility_off" else "visibility",
onClick = { showPassword = !showPassword },
ariaLabel = if (showPassword) "Hide password" else "Show password"
)
}
}
}
@Composable
fun TextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier(),
label: String? = null,
placeholder: String? = null,
type: TextFieldType = TextFieldType.Text,
isError: Boolean = false,
isEnabled: Boolean = true,
isReadOnly: Boolean = false,
validators: List<Validator> = emptyList()
)
| Parameter | Type | Default | Description | |-----------------|--------------------|----------------------|----------------------------------------| | value | String | Required | Current text value (controlled) | | onValueChange | (String) -> Unit | Required | Callback when value changes | | modifier | Modifier | Modifier() | Styling and layout modifier | | label | String? | null | Field label (handled externally) | | placeholder | String? | null | Placeholder text when empty | | type | TextFieldType | TextFieldType.Text | Input type for validation and behavior | | isError | Boolean | false | Whether field is in error state | | isEnabled | Boolean | true | Whether field accepts input | | isReadOnly | Boolean | false | Whether field is read-only | | validators | List<Validator> | emptyList() | Validation rules to apply |
@Composable
fun StatefulTextField(
initialValue: String = "",
onValueChange: (String) -> Unit = {},
modifier: Modifier = Modifier(),
label: String? = null,
placeholder: String? = null,
type: TextFieldType = TextFieldType.Text,
isError: Boolean = false,
isEnabled: Boolean = true,
isReadOnly: Boolean = false,
validators: List<Validator> = emptyList()
)
Enum defining different input types with corresponding HTML input types.
enum class TextFieldType {
Text, // Standard text input
Password, // Password input (hidden characters)
Email, // Email input with validation
Number, // Numeric input
Tel, // Telephone number input
Url, // URL input with validation
Search, // Search input
Date, // Date picker input
Time // Time picker input
}
@Composable
fun ValidatedFormExample() {
var formData by remember { mutableStateOf(UserFormData()) }
var errors by remember { mutableStateOf(emptyMap<String, String>()) }
// Email validators
val emailValidators = listOf(
Validator.required("Email is required"),
Validator.email("Please enter a valid email address")
)
// Password validators
val passwordValidators = listOf(
Validator.required("Password is required"),
Validator.minLength(8, "Password must be at least 8 characters"),
Validator.pattern(
"^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).*$".toRegex(),
"Password must contain uppercase, lowercase, and numbers"
)
)
Column(
verticalSpacing = "16px",
modifier = Modifier().padding("24px")
) {
H2("Create Account")
// Email field
FormField(
label = "Email Address",
error = errors["email"],
required = true
) {
TextField(
value = formData.email,
onValueChange = {
formData = formData.copy(email = it)
errors = errors - "email" // Clear error on change
},
type = TextFieldType.Email,
placeholder = "you@example.com",
validators = emailValidators,
isError = errors.containsKey("email"),
modifier = Modifier().width("100%")
)
}
// Password field
FormField(
label = "Password",
error = errors["password"],
required = true
) {
TextField(
value = formData.password,
onValueChange = {
formData = formData.copy(password = it)
errors = errors - "password"
},
type = TextFieldType.Password,
placeholder = "Enter a secure password",
validators = passwordValidators,
isError = errors.containsKey("password"),
modifier = Modifier().width("100%")
)
}
// Confirm password field
FormField(
label = "Confirm Password",
error = errors["confirmPassword"],
required = true
) {
TextField(
value = formData.confirmPassword,
onValueChange = {
formData = formData.copy(confirmPassword = it)
errors = errors - "confirmPassword"
},
type = TextFieldType.Password,
placeholder = "Confirm your password",
validators = listOf(
Validator.required("Please confirm your password"),
Validator.custom { value ->
if (value == formData.password) {
ValidationResult.valid()
} else {
ValidationResult.invalid("Passwords do not match")
}
}
),
isError = errors.containsKey("confirmPassword"),
modifier = Modifier().width("100%")
)
}
// Submit button
Button(
text = "Create Account",
onClick = {
val validationErrors = validateForm(formData, emailValidators, passwordValidators)
if (validationErrors.isEmpty()) {
// Submit form
submitForm(formData)
} else {
errors = validationErrors
}
},
disabled = !isFormValid(formData),
modifier = Modifier().width("100%")
)
}
}
@Composable
fun SearchFieldExample() {
var query by remember { mutableStateOf("") }
var results by remember { mutableStateOf(emptyList<SearchResult>()) }
var isSearching by remember { mutableStateOf(false) }
// Debounce search
LaunchedEffect(query) {
if (query.isNotBlank()) {
isSearching = true
delay(300) // Debounce delay
try {
results = searchService.search(query)
} finally {
isSearching = false
}
} else {
results = emptyList()
}
}
Column(verticalSpacing = "8px") {
Row(
horizontalSpacing = "8px",
verticalAlignment = "center",
modifier = Modifier()
.backgroundColor("#F5F5F5")
.borderRadius("8px")
.padding("12px")
) {
Icon(
name = "search",
size = "20px",
color = "#666666"
)
TextField(
value = query,
onValueChange = { query = it },
type = TextFieldType.Search,
placeholder = "Search products, articles, or help topics...",
modifier = Modifier()
.flex("1")
.border("none")
.backgroundColor("transparent")
.outline("none")
)
if (isSearching) {
CircularProgress(
size = "16px",
strokeWidth = "2px"
)
} else if (query.isNotBlank()) {
IconButton(
icon = "clear",
onClick = { query = "" },
size = "16px",
ariaLabel = "Clear search"
)
}
}
// Search results
if (results.isNotEmpty()) {
Card(
modifier = Modifier()
.width("100%")
.maxHeight("300px")
.overflow("auto")
.border("1px solid #E0E0E0")
.borderRadius("8px")
) {
LazyColumn {
items(results) { result ->
SearchResultItem(
result = result,
query = query,
onClick = {
// Handle result selection
query = result.title
}
)
}
}
}
}
}
}
@Composable
fun TextAreaExample() {
var description by remember { mutableStateOf("") }
val maxLength = 500
Column(verticalSpacing = "8px") {
Label("Description")
TextArea(
value = description,
onValueChange = {
if (it.length <= maxLength) {
description = it
}
},
placeholder = "Enter a detailed description...",
modifier = Modifier()
.width("100%")
.height("120px")
.resize("vertical")
)
Row(
modifier = Modifier().justifyContent("space-between"),
verticalAlignment = "center"
) {
Text(
text = "${description.length}/$maxLength characters",
fontSize = "12px",
color = if (description.length > maxLength * 0.9) "#FF6B6B" else "#666666"
)
if (description.length > maxLength * 0.9) {
Text(
text = "Approaching character limit",
fontSize = "12px",
color = "#FF6B6B"
)
}
}
}
}
@Composable
fun NumericFieldExample() {
var amount by remember { mutableStateOf("") }
var formattedAmount by remember { mutableStateOf("") }
val numberFormatter = remember {
NumberFormat.getCurrencyInstance(Locale.US)
}
Column(verticalSpacing = "8px") {
Label("Amount")
TextField(
value = amount,
onValueChange = { newValue ->
// Only allow numeric input
val numericValue = newValue.filter { it.isDigit() || it == '.' }
amount = numericValue
// Format for display
val doubleValue = numericValue.toDoubleOrNull()
formattedAmount = if (doubleValue != null) {
numberFormatter.format(doubleValue)
} else {
""
}
},
type = TextFieldType.Number,
placeholder = "0.00",
modifier = Modifier()
.width("200px")
.textAlign("right")
)
if (formattedAmount.isNotBlank()) {
Text(
text = "Formatted: $formattedAmount",
fontSize = "14px",
color = "#666666"
)
}
}
}
@Composable
fun AutoCompleteExample() {
var input by remember { mutableStateOf("") }
var suggestions by remember { mutableStateOf(emptyList<String>()) }
var showSuggestions by remember { mutableStateOf(false) }
val allOptions = listOf(
"JavaScript", "Java", "Kotlin", "Python", "TypeScript",
"React", "Vue", "Angular", "Svelte", "Flutter"
)
LaunchedEffect(input) {
suggestions = if (input.length >= 2) {
allOptions.filter {
it.contains(input, ignoreCase = true)
}.take(5)
} else {
emptyList()
}
showSuggestions = suggestions.isNotEmpty()
}
Box(modifier = Modifier().position("relative")) {
TextField(
value = input,
onValueChange = { input = it },
placeholder = "Type to search technologies...",
modifier = Modifier().width("300px"),
onFocus = { showSuggestions = suggestions.isNotEmpty() },
onBlur = {
// Delay hiding to allow click on suggestions
delay(150)
showSuggestions = false
}
)
if (showSuggestions) {
Card(
modifier = Modifier()
.position("absolute")
.top("100%")
.left("0")
.right("0")
.zIndex("1000")
.maxHeight("200px")
.overflow("auto")
.border("1px solid #E0E0E0")
.borderRadius("4px")
.backgroundColor("white")
.boxShadow("0 2px 8px rgba(0,0,0,0.1)")
) {
suggestions.forEach { suggestion ->
Box(
modifier = Modifier()
.width("100%")
.padding("12px")
.cursor("pointer")
.hover { backgroundColor("#F5F5F5") }
.onClick {
input = suggestion
showSuggestions = false
}
) {
Text(suggestion)
}
}
}
}
}
}
// Common validators
val validators = listOf(
Validator.required("This field is required"),
Validator.minLength(5, "Must be at least 5 characters"),
Validator.maxLength(100, "Must be less than 100 characters"),
Validator.email("Please enter a valid email"),
Validator.pattern(
"^[A-Za-z]+$".toRegex(),
"Only letters are allowed"
),
Validator.custom { value ->
if (value.contains("forbidden")) {
ValidationResult.invalid("Contains forbidden word")
} else {
ValidationResult.valid()
}
}
)
@Composable
fun RealTimeValidationExample() {
var username by remember { mutableStateOf("") }
var isValidating by remember { mutableStateOf(false) }
var isAvailable by remember { mutableStateOf<Boolean?>(null) }
LaunchedEffect(username) {
if (username.length >= 3) {
isValidating = true
delay(500) // Debounce
try {
isAvailable = checkUsernameAvailability(username)
} finally {
isValidating = false
}
} else {
isAvailable = null
}
}
Column(verticalSpacing = "8px") {
Label("Username")
Row(
horizontalSpacing = "8px",
verticalAlignment = "center"
) {
TextField(
value = username,
onValueChange = { username = it },
placeholder = "Choose a username",
modifier = Modifier().flex("1"),
validators = listOf(
Validator.required("Username is required"),
Validator.minLength(3, "Username must be at least 3 characters"),
Validator.pattern(
"^[a-zA-Z0-9_]+$".toRegex(),
"Only letters, numbers, and underscores allowed"
)
)
)
when {
isValidating -> CircularProgress(size = "16px")
isAvailable == true -> Icon("check_circle", color = "#4CAF50")
isAvailable == false -> Icon("error", color = "#F44336")
}
}
when {
isValidating -> Text("Checking availability...", fontSize = "12px", color = "#666")
isAvailable == true -> Text("Username is available", fontSize = "12px", color = "#4CAF50")
isAvailable == false -> Text("Username is already taken", fontSize = "12px", color = "#F44336")
}
}
}
// TextField automatically includes appropriate ARIA attributes
TextField(
value = value,
onValueChange = onValueChange,
// Automatically includes:
// role="textbox"
// aria-invalid="true/false" based on error state
// aria-required="true" for required fields
// aria-describedby for associated error messages
)
// TextField supports standard keyboard interactions:
// - Tab/Shift+Tab for focus navigation
// - Enter for form submission (in forms)
// - Escape to clear focus
// - Standard text editing shortcuts (Ctrl+A, Ctrl+C, etc.)
@Composable
fun AccessibleErrorExample() {
var email by remember { mutableStateOf("") }
var error by remember { mutableStateOf<String?>(null) }
Column(verticalSpacing = "4px") {
Label(
text = "Email Address",
required = true,
htmlFor = "email-input" // Links label to input
)
TextField(
value = email,
onValueChange = {
email = it
error = validateEmail(it)
},
type = TextFieldType.Email,
isError = error != null,
modifier = Modifier()
.id("email-input")
.ariaDescribedBy(if (error != null) "email-error" else null)
)
if (error != null) {
ErrorText(
text = error,
modifier = Modifier()
.id("email-error")
.role("alert") // Announces to screen readers
)
}
}
}
@Composable
fun DebouncedTextField(
value: String,
onValueChange: (String) -> Unit,
debounceMs: Long = 300,
modifier: Modifier = Modifier()
) {
var localValue by remember { mutableStateOf(value) }
LaunchedEffect(localValue) {
delay(debounceMs)
if (localValue != value) {
onValueChange(localValue)
}
}
TextField(
value = localValue,
onValueChange = { localValue = it },
modifier = modifier
)
}
// Use keys for dynamic field lists
@Composable
fun DynamicFieldList(fields: List<FieldConfig>) {
fields.forEach { field ->
key(field.id) {
TextField(
value = field.value,
onValueChange = { updateField(field.id, it) },
placeholder = field.placeholder
)
}
}
}
@Test
fun testTextFieldInput() {
val mockRenderer = MockPlatformRenderer()
var capturedValue = ""
CompositionLocal.provideComposer(MockComposer()) {
LocalPlatformRenderer.provides(mockRenderer) {
TextField(
value = "initial",
onValueChange = { capturedValue = it },
type = TextFieldType.Email
)
// Simulate input change
mockRenderer.simulateTextInput("test@example.com")
assertEquals("test@example.com", capturedValue)
}
}
}
@Test
fun testTextFieldValidation() {
val emailValidator = Validator.email("Invalid email")
var validationResult: ValidationResult? = null
val result = emailValidator.validate("invalid-email")
assertFalse(result.isValid)
assertEquals("Invalid email", result.errorMessage)
val validResult = emailValidator.validate("test@example.com")
assertTrue(validResult.isValid)
}
The TextField component provides a robust foundation for text input across your Summon application, ensuring accessibility, validation, and excellent user experience.