Skip to content

Commit b6fe5ee

Browse files
committed
feat(TextField): add keyboard options, icons, and character counter
Add comprehensive keyboard configuration and visual enhancements to TextField component: - Add keyboard type options (email, number, phone, decimal, url) - Add IME action types (done, go, next, search, send) with onSubmitEditing callback - Add auto-capitalization modes (none, sentences, words, characters) - Add auto-correct toggle - Add leading/trailing Material Icons support (Search, Email, Phone, Person, Lock, Clear, Close, Check, Edit, Info, Warning) - Add interactive trailing icon with onTrailingIconPress callback - Add automatic character counter display when maxLength is set (with showCounter toggle) - Update README with new props table and supported icons section
1 parent 609b554 commit b6fe5ee

File tree

6 files changed

+318
-19
lines changed

6 files changed

+318
-19
lines changed

README.md

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ function MyComponent() {
311311

312312
### TextField
313313

314-
A Material 3 OutlinedTextField for text input with floating labels, error states, and helper text.
314+
A Material 3 OutlinedTextField for text input with floating labels, error states, icons, and keyboard configuration.
315315

316316
```tsx
317317
import { useState } from "react";
@@ -326,6 +326,9 @@ function MyComponent() {
326326
value={email}
327327
label="Email"
328328
placeholder="Enter your email"
329+
leadingIcon="Email"
330+
keyboardType="email"
331+
autoCapitalize="none"
329332
error={error}
330333
helperText={error ? "Invalid email address" : undefined}
331334
onChange={(text) => {
@@ -339,22 +342,39 @@ function MyComponent() {
339342

340343
#### Props
341344

342-
| Prop | Type | Default | Description |
343-
| ----------------- | ---------------------- | ------- | ------------------------------------ |
344-
| `value` | `string` | - | Current text value |
345-
| `label` | `string` | - | Floating label text |
346-
| `placeholder` | `string` | - | Placeholder when empty |
347-
| `disabled` | `boolean` | `false` | Whether the field is disabled |
348-
| `editable` | `boolean` | `true` | Whether text can be edited |
349-
| `multiline` | `boolean` | `false` | Enable multiline input |
350-
| `maxLength` | `number` | - | Maximum character count |
351-
| `secureTextEntry` | `boolean` | `false` | Hide text for password fields |
352-
| `error` | `boolean` | `false` | Show error state styling |
353-
| `helperText` | `string` | - | Helper or error text below field |
354-
| `onChange` | `(text: string) => void` | - | Called when text changes |
355-
| `onFocus` | `() => void` | - | Called when field gains focus |
356-
| `onBlur` | `() => void` | - | Called when field loses focus |
357-
| `style` | `StyleProp<ViewStyle>` | - | Custom styles |
345+
| Prop | Type | Default | Description |
346+
| ------------------- | --------------------------- | ------------- | ---------------------------------------------- |
347+
| `value` | `string` | - | Current text value |
348+
| `label` | `string` | - | Floating label text |
349+
| `placeholder` | `string` | - | Placeholder when empty |
350+
| `disabled` | `boolean` | `false` | Whether the field is disabled |
351+
| `editable` | `boolean` | `true` | Whether text can be edited |
352+
| `multiline` | `boolean` | `false` | Enable multiline input |
353+
| `maxLength` | `number` | - | Maximum character count |
354+
| `secureTextEntry` | `boolean` | `false` | Hide text for password fields |
355+
| `error` | `boolean` | `false` | Show error state styling |
356+
| `helperText` | `string` | - | Helper or error text below field |
357+
| `keyboardType` | `string` | `"default"` | Keyboard type: default, email, number, phone, decimal, url |
358+
| `returnKeyType` | `string` | `"done"` | IME action: done, go, next, search, send |
359+
| `autoCapitalize` | `string` | `"sentences"` | Capitalization: none, sentences, words, characters |
360+
| `autoCorrect` | `boolean` | `true` | Enable/disable auto-correct |
361+
| `leadingIcon` | `string` | - | Leading icon name (see supported icons) |
362+
| `trailingIcon` | `string` | - | Trailing icon name (see supported icons) |
363+
| `showCounter` | `boolean` | `true` | Show character counter when maxLength is set |
364+
| `onChange` | `(text: string) => void` | - | Called when text changes |
365+
| `onFocus` | `() => void` | - | Called when field gains focus |
366+
| `onBlur` | `() => void` | - | Called when field loses focus |
367+
| `onSubmitEditing` | `() => void` | - | Called when IME action button is pressed |
368+
| `onTrailingIconPress` | `() => void` | - | Called when trailing icon is pressed |
369+
| `style` | `StyleProp<ViewStyle>` | - | Custom styles |
370+
371+
#### Supported Icons
372+
373+
The following Material Icons are available for `leadingIcon` and `trailingIcon`:
374+
375+
- **Common**: `Search`, `Clear`, `Close`, `Check`, `Edit`, `Info`
376+
- **Input**: `Email`, `Phone`, `Person`, `Lock`
377+
- **Status**: `Warning`
358378

359379
## Theming
360380

android/src/main/java/com/mgcrea/reactnative/jetpackcompose/TextFieldView.kt

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,43 @@
11
package com.mgcrea.reactnative.jetpackcompose
22

3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Row
35
import androidx.compose.foundation.layout.fillMaxWidth
6+
import androidx.compose.foundation.text.KeyboardActions
7+
import androidx.compose.foundation.text.KeyboardOptions
8+
import androidx.compose.material.icons.Icons
9+
import androidx.compose.material.icons.filled.Check
10+
import androidx.compose.material.icons.filled.Clear
11+
import androidx.compose.material.icons.filled.Close
12+
import androidx.compose.material.icons.filled.Edit
13+
import androidx.compose.material.icons.filled.Email
14+
import androidx.compose.material.icons.filled.Info
15+
import androidx.compose.material.icons.filled.Lock
16+
import androidx.compose.material.icons.filled.Person
17+
import androidx.compose.material.icons.filled.Phone
18+
import androidx.compose.material.icons.filled.Search
19+
import androidx.compose.material.icons.filled.Warning
20+
import androidx.compose.material3.Icon
21+
import androidx.compose.material3.IconButton
422
import androidx.compose.material3.OutlinedTextField
523
import androidx.compose.material3.Text
624
import androidx.compose.runtime.Composable
725
import androidx.compose.runtime.mutableStateOf
826
import androidx.compose.ui.Modifier
927
import androidx.compose.ui.focus.onFocusChanged
28+
import androidx.compose.ui.graphics.vector.ImageVector
29+
import androidx.compose.ui.text.input.ImeAction
30+
import androidx.compose.ui.text.input.KeyboardCapitalization
31+
import androidx.compose.ui.text.input.KeyboardType
1032
import androidx.compose.ui.text.input.PasswordVisualTransformation
1133
import androidx.compose.ui.text.input.VisualTransformation
1234
import com.facebook.react.uimanager.ThemedReactContext
1335
import com.mgcrea.reactnative.jetpackcompose.core.InlineComposeView
1436
import com.mgcrea.reactnative.jetpackcompose.events.TextFieldChangeEvent
1537
import com.mgcrea.reactnative.jetpackcompose.events.TextFieldBlurEvent
1638
import com.mgcrea.reactnative.jetpackcompose.events.TextFieldFocusEvent
39+
import com.mgcrea.reactnative.jetpackcompose.events.TextFieldSubmitEvent
40+
import com.mgcrea.reactnative.jetpackcompose.events.TextFieldTrailingIconPressEvent
1741

1842
internal class TextFieldView(reactContext: ThemedReactContext) :
1943
InlineComposeView(reactContext, TAG) {
@@ -35,6 +59,15 @@ internal class TextFieldView(reactContext: ThemedReactContext) :
3559
private val _helperText = mutableStateOf<String?>(null)
3660
private val _isFocused = mutableStateOf(false)
3761

62+
// New state for keyboard and icons
63+
private val _keyboardType = mutableStateOf("default")
64+
private val _returnKeyType = mutableStateOf("done")
65+
private val _autoCapitalize = mutableStateOf("sentences")
66+
private val _autoCorrect = mutableStateOf(true)
67+
private val _leadingIcon = mutableStateOf<String?>(null)
68+
private val _trailingIcon = mutableStateOf<String?>(null)
69+
private val _showCounter = mutableStateOf(true)
70+
3871
// Property setters called by ViewManager
3972
fun setValue(value: String?) {
4073
_value.value = value ?: ""
@@ -76,6 +109,73 @@ internal class TextFieldView(reactContext: ThemedReactContext) :
76109
_helperText.value = value
77110
}
78111

112+
fun setKeyboardType(value: String?) {
113+
_keyboardType.value = value ?: "default"
114+
}
115+
116+
fun setReturnKeyType(value: String?) {
117+
_returnKeyType.value = value ?: "done"
118+
}
119+
120+
fun setAutoCapitalize(value: String?) {
121+
_autoCapitalize.value = value ?: "sentences"
122+
}
123+
124+
fun setAutoCorrect(value: Boolean) {
125+
_autoCorrect.value = value
126+
}
127+
128+
fun setLeadingIcon(value: String?) {
129+
_leadingIcon.value = value
130+
}
131+
132+
fun setTrailingIcon(value: String?) {
133+
_trailingIcon.value = value
134+
}
135+
136+
fun setShowCounter(value: Boolean) {
137+
_showCounter.value = value
138+
}
139+
140+
private fun getKeyboardType(type: String): KeyboardType = when (type) {
141+
"email" -> KeyboardType.Email
142+
"number" -> KeyboardType.Number
143+
"phone" -> KeyboardType.Phone
144+
"decimal" -> KeyboardType.Decimal
145+
"url" -> KeyboardType.Uri
146+
else -> KeyboardType.Text
147+
}
148+
149+
private fun getImeAction(type: String): ImeAction = when (type) {
150+
"go" -> ImeAction.Go
151+
"next" -> ImeAction.Next
152+
"search" -> ImeAction.Search
153+
"send" -> ImeAction.Send
154+
else -> ImeAction.Done
155+
}
156+
157+
private fun getCapitalization(type: String): KeyboardCapitalization = when (type) {
158+
"none" -> KeyboardCapitalization.None
159+
"words" -> KeyboardCapitalization.Words
160+
"characters" -> KeyboardCapitalization.Characters
161+
else -> KeyboardCapitalization.Sentences
162+
}
163+
164+
private fun getIcon(name: String?): ImageVector? = when (name?.lowercase()) {
165+
"search" -> Icons.Default.Search
166+
"email" -> Icons.Default.Email
167+
"phone" -> Icons.Default.Phone
168+
"person" -> Icons.Default.Person
169+
"lock" -> Icons.Default.Lock
170+
"clear" -> Icons.Default.Clear
171+
"close" -> Icons.Default.Close
172+
"check" -> Icons.Default.Check
173+
"edit" -> Icons.Default.Edit
174+
"info" -> Icons.Default.Info
175+
"warning" -> Icons.Default.Warning
176+
else -> null
177+
}
178+
79179
@Composable
80180
override fun ComposeContent() {
81181
val value = _value.value
@@ -88,6 +188,17 @@ internal class TextFieldView(reactContext: ThemedReactContext) :
88188
val secureTextEntry = _secureTextEntry.value
89189
val error = _error.value
90190
val helperText = _helperText.value
191+
val keyboardType = _keyboardType.value
192+
val returnKeyType = _returnKeyType.value
193+
val autoCapitalize = _autoCapitalize.value
194+
val autoCorrect = _autoCorrect.value
195+
val leadingIconName = _leadingIcon.value
196+
val trailingIconName = _trailingIcon.value
197+
val showCounter = _showCounter.value
198+
199+
val onSubmit = {
200+
dispatchEvent(TextFieldSubmitEvent(getSurfaceId(), id))
201+
}
91202

92203
OutlinedTextField(
93204
modifier = Modifier
@@ -125,12 +236,53 @@ internal class TextFieldView(reactContext: ThemedReactContext) :
125236
isError = error,
126237
label = label?.let { { Text(it) } },
127238
placeholder = placeholder?.let { { Text(it, maxLines = 1) } },
128-
supportingText = helperText?.let { { Text(it) } },
239+
supportingText = if (helperText != null || (showCounter && maxLength != null)) {
240+
{
241+
Row(
242+
modifier = Modifier.fillMaxWidth(),
243+
horizontalArrangement = Arrangement.SpaceBetween
244+
) {
245+
Text(helperText ?: "")
246+
if (showCounter && maxLength != null) {
247+
Text("${value.length}/$maxLength")
248+
}
249+
}
250+
}
251+
} else null,
129252
visualTransformation = if (secureTextEntry) {
130253
PasswordVisualTransformation()
131254
} else {
132255
VisualTransformation.None
133256
},
257+
keyboardOptions = KeyboardOptions(
258+
keyboardType = getKeyboardType(keyboardType),
259+
imeAction = getImeAction(returnKeyType),
260+
capitalization = getCapitalization(autoCapitalize),
261+
autoCorrectEnabled = autoCorrect
262+
),
263+
keyboardActions = KeyboardActions(
264+
onDone = { onSubmit() },
265+
onGo = { onSubmit() },
266+
onNext = { onSubmit() },
267+
onSearch = { onSubmit() },
268+
onSend = { onSubmit() }
269+
),
270+
leadingIcon = leadingIconName?.let { iconName ->
271+
getIcon(iconName)?.let { icon ->
272+
{ Icon(icon, contentDescription = iconName) }
273+
}
274+
},
275+
trailingIcon = trailingIconName?.let { iconName ->
276+
getIcon(iconName)?.let { icon ->
277+
{
278+
IconButton(onClick = {
279+
dispatchEvent(TextFieldTrailingIconPressEvent(getSurfaceId(), id))
280+
}) {
281+
Icon(icon, contentDescription = iconName)
282+
}
283+
}
284+
}
285+
},
134286
)
135287
}
136288
}

android/src/main/java/com/mgcrea/reactnative/jetpackcompose/TextFieldViewManager.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,34 @@ internal class TextFieldViewManager :
7272
view.setHelperText(value)
7373
}
7474

75+
override fun setKeyboardType(view: TextFieldView, value: String?) {
76+
view.setKeyboardType(value)
77+
}
78+
79+
override fun setReturnKeyType(view: TextFieldView, value: String?) {
80+
view.setReturnKeyType(value)
81+
}
82+
83+
override fun setAutoCapitalize(view: TextFieldView, value: String?) {
84+
view.setAutoCapitalize(value)
85+
}
86+
87+
override fun setAutoCorrect(view: TextFieldView, value: Boolean) {
88+
view.setAutoCorrect(value)
89+
}
90+
91+
override fun setLeadingIcon(view: TextFieldView, value: String?) {
92+
view.setLeadingIcon(value)
93+
}
94+
95+
override fun setTrailingIcon(view: TextFieldView, value: String?) {
96+
view.setTrailingIcon(value)
97+
}
98+
99+
override fun setShowCounter(view: TextFieldView, value: Boolean) {
100+
view.setShowCounter(value)
101+
}
102+
75103
companion object {
76104
const val NAME = "TextFieldView"
77105
}

android/src/main/java/com/mgcrea/reactnative/jetpackcompose/events/TextFieldEvents.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,33 @@ class TextFieldBlurEvent(surfaceId: Int, viewId: Int) :
5151
const val EVENT_NAME = "topTextFieldBlur"
5252
}
5353
}
54+
55+
/**
56+
* Fired when the IME action button is pressed.
57+
*/
58+
class TextFieldSubmitEvent(surfaceId: Int, viewId: Int) :
59+
Event<TextFieldSubmitEvent>(surfaceId, viewId) {
60+
61+
override fun getEventName(): String = EVENT_NAME
62+
63+
override fun getEventData(): WritableMap = Arguments.createMap()
64+
65+
companion object {
66+
const val EVENT_NAME = "topSubmitEditing"
67+
}
68+
}
69+
70+
/**
71+
* Fired when the trailing icon is pressed.
72+
*/
73+
class TextFieldTrailingIconPressEvent(surfaceId: Int, viewId: Int) :
74+
Event<TextFieldTrailingIconPressEvent>(surfaceId, viewId) {
75+
76+
override fun getEventName(): String = EVENT_NAME
77+
78+
override fun getEventData(): WritableMap = Arguments.createMap()
79+
80+
companion object {
81+
const val EVENT_NAME = "topTrailingIconPress"
82+
}
83+
}

0 commit comments

Comments
 (0)