I have created a wrapper composable around a normal M3 OutlinedTextField like so:
@Composable
fun FormTextField(
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier,
labelTxt: String,
placeholderTxt: String,
focusedBorderColor: Color,
textColor: Color = colorResource(id = R.color.black),
keyboardOptions: KeyboardOptions,
keyboardActions: KeyboardActions,
trailingIcon: @Composable() (() -> Unit)? = null,
) {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
label = {
Text(labelTxt, color = focusedBorderColor, fontSize = 12.sp)
},
placeholder = {
Text(placeholderTxt, fontSize = 12.sp)
},
textStyle = TextStyle(
color = textColor
),
shape = RoundedCornerShape(8.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = focusedBorderColor,
cursorColor = focusedBorderColor,
unfocusedBorderColor = Color(0xFFBEBEBE),
),
modifier = modifier,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
trailingIcon = trailingIcon
)
}
Now I am trying to use it in order to create a form field that accepts only numbers. The issue I am facing right now is that the text that I type does not seem to be selectable at all, and the cursor cannot be moved to shift through the text as you would normally expect.
Here is my bottomSheet where this is used:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditProductStockBottomSheet(
modifier: Modifier = Modifier,
productLocationViewModel: ProductLocationViewModel = koinInject(),
productDetails: ProductDetails,
stockLocations: List<ProductLocation>,
onStockChanged: (Int) -> Unit,
onDismissRequest: () -> Unit
) {
val context = LocalContext.current
val navViewModel = LocalNavViewModel.current
val scannedBarcode by navViewModel.scannedBarcode.observeAsState()
val coroutineContext = rememberCoroutineScope()
val bottomSheetState = rememberModalBottomSheetState(true)
var locationUUIDToUpdateStock by remember {
mutableStateOf("")
}
var updatedStock by remember {
mutableStateOf(TextFieldValue("0"))
}
val locationToUpdateStock by remember {
derivedStateOf {
stockLocations.firstOrNull { it.uuid == locationUUIDToUpdateStock }
}
}
var reasonForStockUpdate by remember {
mutableStateOf(TextFieldValue(""))
}
LaunchedEffect(key1 = Unit) {
navViewModel.setActiveBarcodeReceivingDestination(BarcodeScanReceivingDestination.EDIT_PRODUCT_STOCK_BOTTOM_SHEET)
}
LaunchedEffect(key1 = scannedBarcode) {
if (scannedBarcode.isNullOrEmpty() || !productDetails.getPossibleBarcodes()
.contains(scannedBarcode)
) return@LaunchedEffect
val updatedStockAsInt = updatedStock.text.toIntOrNull()
if (updatedStockAsInt != null) {
updatedStock = TextFieldValue(
updatedStockAsInt.plus(1).toString()
)
}
navViewModel.setScannedBarcode("")
}
ModalBottomSheet(
onDismissRequest = {
onDismissRequest()
coroutineContext.launch {
bottomSheetState.hide()
}
},
sheetState = bottomSheetState,
contentColor = Color.Black,
scrimColor = Color.DarkGray.copy(alpha = 0.4f),
modifier = modifier
.wrapContentHeight(),
containerColor = colorResource(id = R.color.white),
) {
Column(
modifier = Modifier.padding(horizontal = 16.dp)
) {
Text(
text = stringResource(R.string.stock_title_txt),
fontSize = 14.sp,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(Modifier.height(16.dp))
LyraDropDown(
modifier = Modifier
.fillMaxWidth(),
label = stringResource(R.string.location_input_field_label),
onClick = {
locationUUIDToUpdateStock = stockLocations[it].uuid
},
data = stockLocations.map { it.location.name },
) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
LocationBox(locationName = it)
}
}
Spacer(Modifier.height(16.dp))
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
FormTextField(
modifier = Modifier
.weight(0.7f, true),
value = updatedStock,
onValueChange = { newVal ->
val newText = newVal.text.filter { it.isDigit() || it == '-' || it == '.' }
updatedStock = TextFieldValue(newText)
},
labelTxt = stringResource(id = R.string.stock_title_txt),
placeholderTxt = "0",
focusedBorderColor = colorResource(id = R.color.blue_500),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
),
keyboardActions = KeyboardActions(),
)
Row(
Modifier
.weight(0.3f, true)
.padding(top = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly
) {
Text(
"${locationToUpdateStock?.stock ?: 0}",
fontSize = 12.sp,
color = colorResource(id = R.color.gray_700)
)
Icon(
painter = painterResource(id = R.drawable.ic_arrow_right_long),
contentDescription = null,
tint = colorResource(
id = R.color.gray_700
),
modifier = Modifier.size(24.dp)
)
Text(
"${locationToUpdateStock?.stock?.plus(updatedStock.text.toIntOrNull() ?: 0) ?: 0}",
fontSize = 12.sp,
color = colorResource(id = R.color.gray_700)
)
}
}
Spacer(modifier = Modifier.height(16.dp))
FormTextField(
modifier = Modifier.fillMaxWidth(),
value = reasonForStockUpdate,
onValueChange = { newVal ->
reasonForStockUpdate = newVal
},
labelTxt = stringResource(R.string.reason_form_field_txt),
placeholderTxt = stringResource(id = R.string.reason_form_field_txt),
focusedBorderColor = colorResource(id = R.color.blue_500),
keyboardOptions = KeyboardOptions(
),
keyboardActions = KeyboardActions(onDone = {
if (updatedStock.text.toIntOrNull() != null) onStockChanged(updatedStock.text.toInt())
else Toast.makeText(
context,
context.getString(R.string.invalid_stock_value_msg),
Toast.LENGTH_LONG
).show()
})
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.reason_text_field_desc),
modifier = Modifier.padding(horizontal = 8.dp),
color = colorResource(
id = R.color.gray_700
),
fontSize = 11.sp,
lineHeight = 11.sp,
fontWeight = FontWeight.Light
)
Spacer(modifier = Modifier.height(16.dp))
Row(
Modifier
.fillMaxWidth()
.padding(end = 16.dp, top = 16.dp),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
ActionButton(
text = stringResource(R.string.cancel_btn_txt)
) {
onDismissRequest()
coroutineContext.launch {
bottomSheetState.hide()
}
}
Spacer(Modifier.width(8.dp))
ActionButton(
modifier = Modifier.width(100.dp),
text = stringResource(R.string.add_location_btn_txt),
isEnabled = locationUUIDToUpdateStock.isNotEmpty() && updatedStock.text.toIntOrNull() != null,
containerColor = colorResource(id = R.color.blue_500),
contentColor = colorResource(id = R.color.white)
) {
onStockChanged(updatedStock.text.toInt())
coroutineContext.launch {
bottomSheetState.hide()
onDismissRequest()
}
}
}
}
Spacer(Modifier.height(50.dp))
}
}
Could this be related to this issue from 2021 around text selection during focus or am I messing something up internally with the way I filter the numbers and assign a new TextFieldValue in the onValueChange of the field?
I have managed to fix the problem by using a String
value and callback in the slot API of the composable and basing the TextField's value and onValueChange on a TextFieldValue
that is hoisted at the top of the composable itself instead of passing it down from my other screens:
@Composable
fun FormTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
labelTxt: String,
placeholderTxt: String,
focusedBorderColor: Color,
textColor: Color = colorResource(id = R.color.black),
keyboardOptions: KeyboardOptions,
keyboardActions: KeyboardActions,
selectAllOnFocus: Boolean = false,
trailingIcon: @Composable (() -> Unit)? = null,
) {
var state by remember {
mutableStateOf(TextFieldValue(value))
}
OutlinedTextField(
value = state,
onValueChange = {
onValueChange(it.text)
state = it
},
label = {
Text(labelTxt, color = focusedBorderColor, fontSize = 12.sp)
},
placeholder = {
Text(placeholderTxt, fontSize = 12.sp)
},
textStyle = TextStyle(
color = textColor
),
shape = RoundedCornerShape(8.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = focusedBorderColor,
cursorColor = focusedBorderColor,
unfocusedBorderColor = Color(0xFFBEBEBE),
selectionColors = TextSelectionColors(
handleColor = focusedBorderColor,
backgroundColor = colorResource(id = R.color.gray_200)
)
),
modifier = modifier.onFocusChanged { focusState ->
if (selectAllOnFocus && focusState.isFocused) {
val text = state.text
state = state.copy(
selection = TextRange(0, text.length)
)
}
},
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
trailingIcon = trailingIcon
)
}