Skip to content

Commit a51cf4d

Browse files
authored
Forms : Update disabled fields style (#308)
1 parent 08d546d commit a51cf4d

File tree

6 files changed

+142
-181
lines changed

6 files changed

+142
-181
lines changed

toolkit/featureforms/src/androidTest/java/com/arcgismaps/toolkit/featureforms/FormTextFieldNumericTests.kt

+4-56
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,10 @@ import com.arcgismaps.mapping.ArcGISMap
2929
import com.arcgismaps.mapping.featureforms.FeatureForm
3030
import com.arcgismaps.mapping.featureforms.FeatureFormDefinition
3131
import com.arcgismaps.mapping.featureforms.FieldFormElement
32-
import com.arcgismaps.mapping.featureforms.TextBoxFormInput
3332
import com.arcgismaps.mapping.layers.FeatureLayer
3433
import com.arcgismaps.toolkit.featureforms.components.text.FormTextField
3534
import com.arcgismaps.toolkit.featureforms.components.text.FormTextFieldState
36-
import com.arcgismaps.toolkit.featureforms.components.text.TextFieldProperties
37-
import com.arcgismaps.toolkit.featureforms.utils.editValue
38-
import com.arcgismaps.toolkit.featureforms.utils.fieldType
39-
import com.arcgismaps.toolkit.featureforms.utils.valueFlow
4035
import junit.framework.TestCase
41-
import kotlinx.coroutines.launch
4236
import kotlinx.coroutines.test.runTest
4337
import org.junit.BeforeClass
4438
import org.junit.Rule
@@ -84,31 +78,8 @@ class FormTextFieldNumericTests {
8478
fun testEnterNonNumericValueIntegerField() = runTest {
8579
composeTestRule.setContent {
8680
val scope = rememberCoroutineScope()
87-
val textFieldProperties = TextFieldProperties(
88-
label = integerField.label,
89-
placeholder = integerField.hint,
90-
description = integerField.description,
91-
value = integerField.valueFlow(scope),
92-
editable = integerField.isEditable,
93-
required = integerField.isRequired,
94-
singleLine = integerField.input is TextBoxFormInput,
95-
domain = integerField.domain,
96-
fieldType = featureForm.fieldType(integerField),
97-
minLength = (integerField.input as TextBoxFormInput).minLength.toInt(),
98-
maxLength = (integerField.input as TextBoxFormInput).maxLength.toInt(),
99-
visible = integerField.isVisible
100-
)
101-
FormTextField(
102-
state = FormTextFieldState(
103-
textFieldProperties,
104-
scope = scope,
105-
onEditValue = {
106-
featureForm.editValue(integerField, it)
107-
scope.launch { featureForm.evaluateExpressions() }
108-
},
109-
defaultValidator = {integerField.getValidationErrors()}
110-
)
111-
)
81+
val state = rememberFieldState(element = integerField, form = featureForm, scope = scope) as FormTextFieldState
82+
FormTextField(state = state)
11283
}
11384
val outlinedTextField = composeTestRule.onNodeWithContentDescription(outlinedTextFieldSemanticLabel)
11485
val text = "lorem ipsum"
@@ -128,31 +99,8 @@ class FormTextFieldNumericTests {
12899
fun testEnterNonNumericValueFloatingPointField() = runTest {
129100
composeTestRule.setContent {
130101
val scope = rememberCoroutineScope()
131-
val textFieldProperties = TextFieldProperties(
132-
label = floatingPointField.label,
133-
placeholder = floatingPointField.hint,
134-
description = floatingPointField.description,
135-
value = floatingPointField.valueFlow(scope),
136-
editable = floatingPointField.isEditable,
137-
required = floatingPointField.isRequired,
138-
singleLine = floatingPointField.input is TextBoxFormInput,
139-
domain = floatingPointField.domain,
140-
fieldType = featureForm.fieldType(floatingPointField),
141-
minLength = (floatingPointField.input as TextBoxFormInput).minLength.toInt(),
142-
maxLength = (floatingPointField.input as TextBoxFormInput).maxLength.toInt(),
143-
visible = floatingPointField.isVisible
144-
)
145-
FormTextField(
146-
state = FormTextFieldState(
147-
textFieldProperties,
148-
scope = scope,
149-
onEditValue = {
150-
featureForm.editValue(floatingPointField, it)
151-
scope.launch { featureForm.evaluateExpressions() }
152-
},
153-
defaultValidator = {floatingPointField.getValidationErrors()}
154-
)
155-
)
102+
val state = rememberFieldState(element = floatingPointField, form = featureForm, scope = scope) as FormTextFieldState
103+
FormTextField(state = state)
156104
}
157105
val outlinedTextField = composeTestRule.onNodeWithContentDescription(outlinedTextFieldSemanticLabel)
158106
val text = "lorem ipsum"

toolkit/featureforms/src/androidTest/java/com/arcgismaps/toolkit/featureforms/FormTextFieldRangeNumericTests.kt

+2-31
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,10 @@ import com.arcgismaps.mapping.ArcGISMap
2929
import com.arcgismaps.mapping.featureforms.FeatureForm
3030
import com.arcgismaps.mapping.featureforms.FeatureFormDefinition
3131
import com.arcgismaps.mapping.featureforms.FieldFormElement
32-
import com.arcgismaps.mapping.featureforms.TextBoxFormInput
3332
import com.arcgismaps.mapping.layers.FeatureLayer
3433
import com.arcgismaps.toolkit.featureforms.components.text.FormTextField
3534
import com.arcgismaps.toolkit.featureforms.components.text.FormTextFieldState
36-
import com.arcgismaps.toolkit.featureforms.components.text.TextFieldProperties
37-
import com.arcgismaps.toolkit.featureforms.utils.editValue
38-
import com.arcgismaps.toolkit.featureforms.utils.fieldType
39-
import com.arcgismaps.toolkit.featureforms.utils.valueFlow
4035
import junit.framework.TestCase
41-
import kotlinx.coroutines.launch
4236
import kotlinx.coroutines.test.runTest
4337
import org.junit.BeforeClass
4438
import org.junit.Rule
@@ -77,31 +71,8 @@ class FormTextFieldRangeNumericTests {
7771
fun testEnterNumericValueOutOfRange() = runTest {
7872
composeTestRule.setContent {
7973
val scope = rememberCoroutineScope()
80-
val textFieldProperties = TextFieldProperties(
81-
label = integerField.label,
82-
placeholder = integerField.hint,
83-
description = integerField.description,
84-
value = integerField.valueFlow(scope),
85-
editable = integerField.isEditable,
86-
required = integerField.isRequired,
87-
singleLine = integerField.input is TextBoxFormInput,
88-
domain = integerField.domain,
89-
fieldType = featureForm.fieldType(integerField),
90-
minLength = (integerField.input as TextBoxFormInput).minLength.toInt(),
91-
maxLength = (integerField.input as TextBoxFormInput).maxLength.toInt(),
92-
visible = integerField.isVisible
93-
)
94-
FormTextField(
95-
state = FormTextFieldState(
96-
textFieldProperties,
97-
scope = scope,
98-
onEditValue = {
99-
featureForm.editValue(integerField, it)
100-
scope.launch { featureForm.evaluateExpressions() }
101-
},
102-
defaultValidator = {integerField.getValidationErrors()}
103-
)
104-
)
74+
val state = rememberFieldState(element = integerField, form = featureForm, scope = scope) as FormTextFieldState
75+
FormTextField(state = state)
10576
}
10677
val outlinedTextField = composeTestRule.onNodeWithContentDescription(outlinedTextFieldSemanticLabel)
10778
val text = "9"

toolkit/featureforms/src/androidTest/java/com/arcgismaps/toolkit/featureforms/FormTextFieldTests.kt

-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import androidx.compose.ui.test.performClick
3030
import androidx.compose.ui.test.performImeAction
3131
import androidx.compose.ui.test.performTextClearance
3232
import androidx.compose.ui.test.performTextInput
33-
import androidx.compose.ui.test.printToLog
3433
import com.arcgismaps.ArcGISEnvironment
3534
import com.arcgismaps.data.ArcGISFeature
3635
import com.arcgismaps.data.QueryParameters
@@ -180,7 +179,6 @@ class FormTextFieldTests {
180179
@Test
181180
fun testEnteredValueUnfocusedState() {
182181
val outlinedTextField = composeTestRule.onNodeWithContentDescription(outlinedTextFieldSemanticLabel, useUnmergedTree = true)
183-
outlinedTextField.printToLog("TAG")
184182
val text = "lorem ipsum"
185183
outlinedTextField.performTextInput(text)
186184
outlinedTextField.assertIsFocused()

toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/components/base/BaseTextField.kt

+91-48
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,22 @@ import androidx.compose.foundation.gestures.detectTapGestures
2121
import androidx.compose.foundation.interaction.MutableInteractionSource
2222
import androidx.compose.foundation.layout.Column
2323
import androidx.compose.foundation.layout.ColumnScope
24+
import androidx.compose.foundation.layout.defaultMinSize
2425
import androidx.compose.foundation.layout.fillMaxWidth
2526
import androidx.compose.foundation.layout.padding
27+
import androidx.compose.foundation.text.BasicTextField
2628
import androidx.compose.foundation.text.KeyboardActions
2729
import androidx.compose.foundation.text.KeyboardOptions
2830
import androidx.compose.material.icons.Icons
2931
import androidx.compose.material.icons.rounded.CheckCircle
3032
import androidx.compose.material.icons.rounded.Clear
3133
import androidx.compose.material.icons.rounded.TextFields
34+
import androidx.compose.material3.ExperimentalMaterial3Api
3235
import androidx.compose.material3.Icon
3336
import androidx.compose.material3.IconButton
37+
import androidx.compose.material3.LocalTextStyle
3438
import androidx.compose.material3.MaterialTheme
35-
import androidx.compose.material3.OutlinedTextField
39+
import androidx.compose.material3.OutlinedTextFieldDefaults
3640
import androidx.compose.material3.Text
3741
import androidx.compose.runtime.Composable
3842
import androidx.compose.runtime.getValue
@@ -41,10 +45,12 @@ import androidx.compose.runtime.remember
4145
import androidx.compose.runtime.setValue
4246
import androidx.compose.ui.Modifier
4347
import androidx.compose.ui.focus.onFocusChanged
48+
import androidx.compose.ui.graphics.takeOrElse
4449
import androidx.compose.ui.graphics.vector.ImageVector
4550
import androidx.compose.ui.input.pointer.pointerInput
4651
import androidx.compose.ui.semantics.contentDescription
4752
import androidx.compose.ui.semantics.semantics
53+
import androidx.compose.ui.text.TextStyle
4854
import androidx.compose.ui.text.input.ImeAction
4955
import androidx.compose.ui.text.input.KeyboardType
5056
import androidx.compose.ui.text.input.VisualTransformation
@@ -91,15 +97,15 @@ private fun trailingIcon(
9197
// multiline editable field
9298
{
9399
// show a done button only when focused
94-
IconButton(onClick = { onDone() }, modifier = Modifier.semantics {
100+
IconButton(onClick = { onDone() }, modifier = Modifier.semantics {
95101
contentDescription = "Save local edit button"
96102
}) {
97103
Icon(
98104
imageVector = Icons.Rounded.CheckCircle, contentDescription = "Done"
99105
)
100106
}
101107
}
102-
108+
103109
} else if (!singleLine && isEditable && text.isNotEmpty()) {
104110
{
105111
// show a clear icon instead if the multiline field is not empty
@@ -117,8 +123,8 @@ private fun trailingIcon(
117123
}
118124

119125
/**
120-
* A base text field component built on top of an [OutlinedTextField] that provides a standard for
121-
* visual and behavioral properties. This can be used to build more customized composite components.
126+
* A base text field component built on top of a [BasicTextField] with an [OutlinedTextFieldDefaults.DecorationBox].
127+
* This provides a standard for visual and behavioral properties and can be used to build more customized composite components.
122128
*
123129
* The BaseTextField also takes care of clearing focus when the keyboard is dismissed or tapped
124130
* outside the input area.
@@ -144,6 +150,7 @@ private fun trailingIcon(
144150
* for this text field.
145151
* @param trailingContent a widget to be displayed at the end of the text field container.
146152
*/
153+
@OptIn(ExperimentalMaterial3Api::class)
147154
@Composable
148155
internal fun BaseTextField(
149156
text: String,
@@ -155,17 +162,26 @@ internal fun BaseTextField(
155162
modifier: Modifier = Modifier,
156163
readOnly: Boolean = !isEditable,
157164
keyboardType: KeyboardType = KeyboardType.Ascii,
165+
textStyle: TextStyle = LocalTextStyle.current,
158166
trailingIcon: ImageVector? = null,
159167
supportingText: @Composable (ColumnScope.() -> Unit)? = null,
160168
onFocusChange: ((Boolean) -> Unit)? = null,
161169
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
162170
trailingContent: (@Composable () -> Unit)? = null
163-
) {
171+
) {
164172
var clearFocus by remember { mutableStateOf(false) }
165173
var isFocused by remember { mutableStateOf(false) }
166-
167-
// if the keyboard is gone clear focus from the field as a side-effect
168-
ClearFocus(clearFocus) { clearFocus = false }
174+
val visualTransformation = if (text.isEmpty())
175+
PlaceholderTransformation(placeholder.ifEmpty { " " })
176+
else VisualTransformation.None
177+
val colors = baseTextFieldColors(
178+
isEditable = isEditable
179+
)
180+
// If color is not provided via the text style, use content color as a default
181+
val textColor = textStyle.color.takeOrElse {
182+
defaultTextColor(isEditable, text.isEmpty(), placeholder.isEmpty())
183+
}
184+
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
169185

170186
Column(modifier = modifier
171187
.onFocusChanged {
@@ -178,65 +194,92 @@ internal fun BaseTextField(
178194
}
179195
.padding(start = 15.dp, end = 15.dp, top = 10.dp, bottom = 10.dp)
180196
) {
181-
OutlinedTextField(
197+
BasicTextField(
182198
value = text,
183199
onValueChange = onValueChange,
184200
modifier = Modifier
201+
// Merge semantics at the beginning of the modifier chain to ensure padding is
202+
// considered part of the text field.
203+
.semantics(mergeDescendants = true) {}
204+
.padding(top = 8.dp)
205+
.defaultMinSize(
206+
minWidth = OutlinedTextFieldDefaults.MinWidth,
207+
minHeight = OutlinedTextFieldDefaults.MinHeight
208+
)
185209
.fillMaxWidth()
186210
.semantics { contentDescription = "outlined text field" },
211+
enabled = true,
187212
readOnly = readOnly,
188-
label = {
189-
Text(
190-
text = label,
191-
modifier = Modifier.semantics { contentDescription = "label" },
192-
overflow = TextOverflow.Ellipsis,
193-
maxLines = 1
194-
)
195-
},
196-
trailingIcon = trailingContent
197-
?: trailingIcon(
198-
text,
199-
isEditable,
200-
singleLine,
201-
isFocused,
202-
trailingIcon,
203-
onValueChange = onValueChange,
204-
onDone = { clearFocus = true }
205-
),
206-
supportingText = {
207-
Column(
208-
modifier = Modifier.clickable {
209-
clearFocus = true
210-
}.semantics { contentDescription = "supporting text" }
211-
) {
212-
supportingText?.invoke(this)
213-
}
214-
},
215-
visualTransformation = if (text.isEmpty())
216-
PlaceholderTransformation(placeholder.ifEmpty { " " })
217-
else VisualTransformation.None,
213+
textStyle = mergedTextStyle,
214+
visualTransformation = visualTransformation,
218215
keyboardActions = KeyboardActions(
219216
onDone = { clearFocus = true }
220217
),
221218
keyboardOptions = KeyboardOptions.Default.copy(
222219
imeAction = if (singleLine) ImeAction.Done else ImeAction.None,
223220
keyboardType = keyboardType
224221
),
225-
singleLine = singleLine,
226222
interactionSource = interactionSource,
227-
colors = baseTextFieldColors(
228-
isEditable = isEditable,
229-
isEmpty = text.isEmpty(),
230-
isPlaceholderEmpty = placeholder.isEmpty()
231-
)
223+
singleLine = singleLine,
224+
decorationBox = @Composable { innerTextField ->
225+
OutlinedTextFieldDefaults.DecorationBox(
226+
value = text,
227+
visualTransformation = visualTransformation,
228+
innerTextField = innerTextField,
229+
label = {
230+
Text(
231+
text = label,
232+
modifier = Modifier.semantics { contentDescription = "label" },
233+
overflow = TextOverflow.Ellipsis,
234+
maxLines = 1
235+
)
236+
},
237+
trailingIcon = trailingContent
238+
?: trailingIcon(
239+
text,
240+
isEditable,
241+
singleLine,
242+
isFocused,
243+
trailingIcon,
244+
onValueChange = onValueChange,
245+
onDone = { clearFocus = true }
246+
),
247+
supportingText = {
248+
Column(
249+
modifier = Modifier
250+
.clickable {
251+
clearFocus = true
252+
}
253+
) {
254+
supportingText?.invoke(this)
255+
}
256+
},
257+
singleLine = singleLine,
258+
enabled = true,
259+
isError = false,
260+
interactionSource = interactionSource,
261+
colors = colors,
262+
container = {
263+
OutlinedTextFieldDefaults.ContainerBox(
264+
enabled = true,
265+
isError = false,
266+
interactionSource,
267+
colors,
268+
OutlinedTextFieldDefaults.shape,
269+
focusedBorderThickness = if (isEditable) 2.dp else 1.dp
270+
)
271+
}
272+
)
273+
}
232274
)
233275
}
276+
// if the keyboard is gone clear focus from the field as a side-effect
277+
ClearFocus(clearFocus) { clearFocus = false }
234278
}
235279

236-
237280
@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF)
238281
@Composable
239-
private fun BaseTextFieldPreview() {
282+
private fun BaseTextFieldV2Preview() {
240283
MaterialTheme {
241284
BaseTextField(
242285
text = "",

0 commit comments

Comments
 (0)