Merge "Enforce minimum space for text in M3 text fields." into androidx-main
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt
index ae46835..fa4a4be 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt
@@ -76,6 +76,7 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
@@ -86,7 +87,6 @@
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
-import kotlin.math.max
import kotlin.math.roundToInt
import org.junit.Rule
import org.junit.Test
@@ -227,39 +227,58 @@
@Test
fun testOutlinedTextField_labelPosition_initial_singleLine() {
- val labelSize = Ref<IntSize>()
val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
- Box {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- singleLine = true,
- label = {
- Text(
- text = "label",
- modifier = Modifier.onGloballyPositioned {
- labelPosition.value = it.positionInRoot()
- labelSize.value = it.size
- }
- )
- }
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ singleLine = true,
+ label = {
+ Box(Modifier
+ .size(MinTextLineHeight)
+ .onGloballyPositioned {
+ labelPosition.value = it.positionInRoot()
+ }
+ )
+ }
+ )
}
rule.runOnIdleWithDensity {
- // size
- assertThat(labelSize.value).isNotNull()
- assertThat(labelSize.value?.height).isGreaterThan(0)
- assertThat(labelSize.value?.width).isGreaterThan(0)
- assertThat(labelPosition.value?.x).isEqualTo(
- ExpectedPadding.roundToPx().toFloat()
+ // x position is start + padding
+ assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ // y position is centered, plus additional padding allowance on top
+ assertThat(labelPosition.value?.y).isWithin(1f).of(
+ ((ExpectedMinimumTextFieldHeight - MinTextLineHeight) / 2 +
+ OutlinedTextFieldTopPadding).toPx()
)
- // label is centered in 56.dp default container, plus additional 8.dp padding on top
- val minimumHeight = ExpectedMinimumTextFieldHeight.roundToPx()
- assertThat(labelPosition.value?.y).isEqualTo(
- ((minimumHeight - labelSize.value!!.height) / 2f).roundToInt() + 8.dp.roundToPx()
+ }
+ }
+
+ @Test
+ fun testOutlinedTextField_labelPosition_initial_withDefaultHeight() {
+ val labelPosition = Ref<Offset>()
+ rule.setMaterialContent(lightColorScheme()) {
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = {
+ Box(Modifier
+ .size(MinTextLineHeight)
+ .onGloballyPositioned {
+ labelPosition.value = it.positionInRoot()
+ }
+ )
+ }
+ )
+ }
+
+ rule.runOnIdleWithDensity {
+ // x position is start + padding
+ assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ // y position is top + default padding + label padding allowance
+ assertThat(labelPosition.value?.y).isWithin(1f).of(
+ (ExpectedPadding + OutlinedTextFieldTopPadding).toPx()
)
}
}
@@ -268,22 +287,22 @@
fun testOutlinedTextField_labelPosition_initial_withMultiLineLabel() {
val textFieldWidth = 200.dp
val labelSize = Ref<IntSize>()
+ val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
- Box {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- modifier = Modifier.requiredWidth(textFieldWidth),
- label = {
- Text(
- text = "long long long long long long long long long long long long",
- modifier = Modifier.onGloballyPositioned {
- labelSize.value = it.size
- }
- )
- }
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ modifier = Modifier.requiredWidth(textFieldWidth),
+ label = {
+ Text(
+ text = "long long long long long long long long long long long long",
+ modifier = Modifier.onGloballyPositioned {
+ labelSize.value = it.size
+ labelPosition.value = it.positionInRoot()
+ }
+ )
+ }
+ )
}
rule.runOnIdleWithDensity {
@@ -292,50 +311,20 @@
assertThat(labelSize.value?.height).isGreaterThan(0)
assertThat(labelSize.value?.width)
.isEqualTo(textFieldWidth.roundToPx() - 2 * ExpectedPadding.roundToPx())
- }
- }
- @Test
- fun testOutlinedTextField_labelPosition_initial_withDefaultHeight() {
- val labelSize = Ref<IntSize>()
- val labelPosition = Ref<Offset>()
- rule.setMaterialContent(lightColorScheme()) {
- Box {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = {
- Text(
- text = "label",
- modifier = Modifier.onGloballyPositioned {
- labelPosition.value = it.positionInRoot()
- labelSize.value = it.size
- }
- )
- }
- )
- }
- }
-
- rule.runOnIdleWithDensity {
- // size
- assertThat(labelSize.value).isNotNull()
- assertThat(labelSize.value?.height).isGreaterThan(0)
- assertThat(labelSize.value?.width).isGreaterThan(0)
- assertThat(labelPosition.value?.x).isEqualTo(
- ExpectedPadding.roundToPx().toFloat()
- )
- // label is aligned to the top with padding, plus additional 8.dp padding on top
- assertThat(labelPosition.value?.y).isEqualTo(
- TextFieldPadding.roundToPx() + 8.dp.roundToPx()
+ // x position is start + padding
+ assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ // y position is top + default padding + label padding allowance
+ assertThat(labelPosition.value?.y).isWithin(1f).of(
+ (ExpectedPadding + OutlinedTextFieldTopPadding).toPx()
)
}
}
@Test
fun testOutlinedTextField_labelPosition_whenFocused() {
- val labelSize = Ref<IntSize>()
val labelPosition = Ref<Offset>()
+ val labelSize = MinFocusedLabelLineHeight
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
@@ -343,11 +332,10 @@
value = "",
onValueChange = {},
label = {
- Text(
- text = "label",
- modifier = Modifier.onGloballyPositioned {
+ Box(Modifier
+ .size(MinFocusedLabelLineHeight)
+ .onGloballyPositioned {
labelPosition.value = it.positionInRoot()
- labelSize.value = it.size
}
)
}
@@ -358,15 +346,10 @@
rule.onNodeWithTag(TextFieldTag).performClick()
rule.runOnIdleWithDensity {
- // size
- assertThat(labelSize.value).isNotNull()
- assertThat(labelSize.value?.height).isGreaterThan(0)
- assertThat(labelSize.value?.width).isGreaterThan(0)
- // label position
- assertThat(labelPosition.value?.x).isEqualTo(
- ExpectedPadding.roundToPx().toFloat()
+ assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ assertThat(labelPosition.value?.y).isWithin(1f).of(
+ getLabelPosition(labelSize.roundToPx()).toFloat()
)
- assertThat(labelPosition.value?.y).isEqualTo(getLabelPosition(labelSize))
}
}
@@ -374,22 +357,22 @@
fun testOutlinedTextField_labelPosition_whenFocused_withMultiLineLabel() {
val textFieldWidth = 200.dp
val labelSize = Ref<IntSize>()
+ val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
- Box {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- modifier = Modifier.testTag(TextFieldTag).requiredWidth(textFieldWidth),
- label = {
- Text(
- text = "long long long long long long long long long long long long",
- modifier = Modifier.onGloballyPositioned {
- labelSize.value = it.size
- }
- )
- }
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ modifier = Modifier.testTag(TextFieldTag).requiredWidth(textFieldWidth),
+ label = {
+ Text(
+ text = "long long long long long long long long long long long long",
+ modifier = Modifier.onGloballyPositioned {
+ labelSize.value = it.size
+ labelPosition.value = it.positionInRoot()
+ }
+ )
+ }
+ )
}
// click to focus
@@ -399,8 +382,15 @@
// label size
assertThat(labelSize.value).isNotNull()
assertThat(labelSize.value?.height).isGreaterThan(0)
- assertThat(labelSize.value?.width)
- .isEqualTo(textFieldWidth.roundToPx() - 2 * ExpectedPadding.roundToPx())
+ assertThat(labelSize.value?.width!!.toFloat()).isWithin(1f).of(
+ (textFieldWidth - ExpectedPadding * 2).toPx()
+ )
+
+ // label position
+ assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ assertThat(labelPosition.value?.y).isWithin(1f).of(
+ getLabelPosition(labelSize.value!!.height).toFloat()
+ )
}
}
@@ -455,18 +445,17 @@
@Test
fun testOutlinedTextField_labelPosition_whenInput() {
- val labelSize = Ref<IntSize>()
+ val labelSize = MinFocusedLabelLineHeight
val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "input",
onValueChange = {},
label = {
- Text(
- text = "label",
- modifier = Modifier.onGloballyPositioned {
+ Box(Modifier
+ .size(labelSize)
+ .onGloballyPositioned {
labelPosition.value = it.positionInRoot()
- labelSize.value = it.size
}
)
}
@@ -474,15 +463,11 @@
}
rule.runOnIdleWithDensity {
- // size
- assertThat(labelSize.value).isNotNull()
- assertThat(labelSize.value?.height).isGreaterThan(0)
- assertThat(labelSize.value?.width).isGreaterThan(0)
// label position
- assertThat(labelPosition.value?.x).isEqualTo(
- ExpectedPadding.roundToPx().toFloat()
+ assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ assertThat(labelPosition.value?.y).isWithin(1f).of(
+ getLabelPosition(labelSize.roundToPx()).toFloat()
)
- assertThat(labelPosition.value?.y).isEqualTo(getLabelPosition(labelSize))
}
}
@@ -513,7 +498,7 @@
@Test
fun testOutlinedTextField_placeholderPosition_withLabel() {
- val placeholderSize = Ref<IntSize>()
+ val placeholderSize = MinTextLineHeight
val placeholderPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
Box {
@@ -521,13 +506,12 @@
modifier = Modifier.testTag(TextFieldTag),
value = "",
onValueChange = {},
- label = { Text("label") },
+ label = { Box(Modifier.size(MinFocusedLabelLineHeight)) },
placeholder = {
- Text(
- text = "placeholder",
- modifier = Modifier.onGloballyPositioned {
+ Box(Modifier
+ .size(placeholderSize)
+ .onGloballyPositioned {
placeholderPosition.value = it.positionInRoot()
- placeholderSize.value = it.size
}
)
}
@@ -538,57 +522,38 @@
rule.onNodeWithTag(TextFieldTag).performClick()
rule.runOnIdleWithDensity {
- // size
- assertThat(placeholderSize.value).isNotNull()
- assertThat(placeholderSize.value?.height).isGreaterThan(0)
- assertThat(placeholderSize.value?.width).isGreaterThan(0)
- // position
- assertThat(placeholderPosition.value?.x).isEqualTo(
- ExpectedPadding.roundToPx().toFloat()
- )
- // placeholder is aligned to the top with padding, plus additional 8.dp padding on top
- assertThat(placeholderPosition.value?.y).isEqualTo(
- TextFieldPadding.roundToPx() + 8.dp.roundToPx()
+ assertThat(placeholderPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ assertThat(placeholderPosition.value?.y).isWithin(1f).of(
+ (ExpectedPadding + OutlinedTextFieldTopPadding).toPx()
)
}
}
@Test
fun testOutlinedTextField_placeholderPosition_whenNoLabel() {
- val placeholderSize = Ref<IntSize>()
+ val placeholderSize = MinTextLineHeight
val placeholderPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
- Box {
- OutlinedTextField(
- modifier = Modifier.testTag(TextFieldTag),
- value = "",
- onValueChange = {},
- placeholder = {
- Text(
- text = "placeholder",
- modifier = Modifier.onGloballyPositioned {
- placeholderPosition.value = it.positionInRoot()
- placeholderSize.value = it.size
- }
- )
- }
- )
- }
+ OutlinedTextField(
+ modifier = Modifier.testTag(TextFieldTag),
+ value = "",
+ onValueChange = {},
+ placeholder = {
+ Box(Modifier
+ .size(placeholderSize)
+ .onGloballyPositioned {
+ placeholderPosition.value = it.positionInRoot()
+ }
+ )
+ }
+ )
}
// click to focus
rule.onNodeWithTag(TextFieldTag).performClick()
rule.runOnIdleWithDensity {
- // size
- assertThat(placeholderSize.value).isNotNull()
- assertThat(placeholderSize.value?.height).isGreaterThan(0)
- assertThat(placeholderSize.value?.width).isGreaterThan(0)
- // position
- assertThat(placeholderPosition.value?.x).isEqualTo(
- ExpectedPadding.roundToPx().toFloat()
- )
- // placeholder is placed with fixed padding
- assertThat(placeholderPosition.value?.y).isEqualTo(TextFieldPadding.roundToPx())
+ assertThat(placeholderPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ assertThat(placeholderPosition.value?.y).isWithin(1f).of(ExpectedPadding.toPx())
}
}
@@ -597,22 +562,20 @@
val placeholderSize = Ref<IntSize>()
val placeholderPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
- Column {
- OutlinedTextField(
- modifier = Modifier.testTag(TextFieldTag),
- value = "input",
- onValueChange = {},
- placeholder = {
- Text(
- text = "placeholder",
- modifier = Modifier.onGloballyPositioned {
- placeholderPosition.value = it.positionInRoot()
- placeholderSize.value = it.size
- }
- )
- }
- )
- }
+ OutlinedTextField(
+ modifier = Modifier.testTag(TextFieldTag),
+ value = "input",
+ onValueChange = {},
+ placeholder = {
+ Text(
+ text = "placeholder",
+ modifier = Modifier.onGloballyPositioned {
+ placeholderPosition.value = it.positionInRoot()
+ placeholderSize.value = it.size
+ }
+ )
+ }
+ )
}
// click to focus
@@ -644,6 +607,61 @@
}
@Test
+ fun testOutlinedTextField_labelAndPlaceholderPosition_whenSmallerThanMinimumHeight() {
+ val labelSize = 10.dp
+ val labelPosition = Ref<Offset>()
+ val placeholderSize = 20.dp
+ val placeholderPosition = Ref<Offset>()
+ rule.setMaterialContent(lightColorScheme()) {
+ OutlinedTextField(
+ modifier = Modifier.testTag(TextFieldTag),
+ value = "",
+ onValueChange = {},
+ label = {
+ Box(Modifier
+ .size(labelSize)
+ .onGloballyPositioned {
+ labelPosition.value = it.positionInRoot()
+ }
+ )
+ },
+ placeholder = {
+ Box(Modifier
+ .size(placeholderSize)
+ .onGloballyPositioned {
+ placeholderPosition.value = it.positionInRoot()
+ }
+ )
+ }
+ )
+ }
+
+ // click to focus
+ rule.onNodeWithTag(TextFieldTag).performClick()
+
+ rule.runOnIdleWithDensity {
+ // size
+ assertThat(labelSize).isLessThan(MinFocusedLabelLineHeight)
+ assertThat(placeholderSize).isLessThan(MinTextLineHeight)
+
+ // label position
+ assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ assertThat(labelPosition.value?.y).isWithin(1f).of(
+ getLabelPosition(labelSize.roundToPx()).toFloat()
+ )
+
+ // placeholder position
+ assertThat(placeholderPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ // placeholder y position is top + default padding + label padding allowance, then
+ // centered within allocated space
+ assertThat(placeholderPosition.value?.y).isWithin(1f).of(
+ (ExpectedPadding + OutlinedTextFieldTopPadding +
+ (MinTextLineHeight - placeholderSize) / 2).toPx()
+ )
+ }
+ }
+
+ @Test
fun testOutlinedTextField_trailingAndLeading_sizeAndPosition_defaultIcon() {
val textFieldWidth = 300.dp
val leadingPosition = Ref<Offset>()
@@ -841,11 +859,10 @@
@Test
fun testOutlinedTextField_prefixAndSuffixPosition_withLabel() {
val textFieldWidth = 300.dp
- val textFieldHeight = 60.dp
- val labelSize = Ref<IntSize>()
val prefixPosition = Ref<Offset>()
+ val prefixSize = MinTextLineHeight
val suffixPosition = Ref<Offset>()
- val suffixSize = Ref<IntSize>()
+ val suffixSize = MinTextLineHeight
val density = Density(2f)
rule.setMaterialContent(lightColorScheme()) {
@@ -853,29 +870,21 @@
OutlinedTextField(
value = "text",
onValueChange = {},
- modifier = Modifier.size(textFieldWidth, textFieldHeight),
- label = {
- Text(
- text = "label",
- modifier = Modifier.onGloballyPositioned {
- labelSize.value = it.size
- }
- )
- },
+ modifier = Modifier.width(textFieldWidth),
+ label = { Box(Modifier.size(MinFocusedLabelLineHeight)) },
prefix = {
- Text(
- text = "P",
- modifier = Modifier.onGloballyPositioned {
+ Box(Modifier
+ .size(prefixSize)
+ .onGloballyPositioned {
prefixPosition.value = it.positionInRoot()
}
)
},
suffix = {
- Text(
- text = "S",
- modifier = Modifier.onGloballyPositioned {
+ Box(Modifier
+ .size(suffixSize)
+ .onGloballyPositioned {
suffixPosition.value = it.positionInRoot()
- suffixSize.value = it.size
}
)
}
@@ -888,14 +897,16 @@
// prefix
assertThat(prefixPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
assertThat(prefixPosition.value?.y).isWithin(1f).of(
- (ExpectedPadding + 8.dp).toPx())
+ (ExpectedPadding + OutlinedTextFieldTopPadding).toPx()
+ )
// suffix
assertThat(suffixPosition.value?.x).isWithin(1f).of(
- (textFieldWidth - ExpectedPadding - suffixSize.value!!.width.toDp()).toPx()
+ (textFieldWidth - ExpectedPadding - suffixSize).toPx()
)
assertThat(suffixPosition.value?.y).isWithin(1f).of(
- (ExpectedPadding + 8.dp).toPx())
+ (ExpectedPadding + OutlinedTextFieldTopPadding).toPx()
+ )
}
}
}
@@ -903,10 +914,10 @@
@Test
fun testOutlinedTextField_prefixAndSuffixPosition_whenNoLabel() {
val textFieldWidth = 300.dp
- val textFieldHeight = 60.dp
val prefixPosition = Ref<Offset>()
+ val prefixSize = MinTextLineHeight
val suffixPosition = Ref<Offset>()
- val suffixSize = Ref<IntSize>()
+ val suffixSize = MinTextLineHeight
val density = Density(2f)
rule.setMaterialContent(lightColorScheme()) {
@@ -914,21 +925,20 @@
OutlinedTextField(
value = "text",
onValueChange = {},
- modifier = Modifier.size(textFieldWidth, textFieldHeight),
+ modifier = Modifier.width(textFieldWidth),
prefix = {
- Text(
- text = "P",
- modifier = Modifier.onGloballyPositioned {
+ Box(Modifier
+ .size(prefixSize)
+ .onGloballyPositioned {
prefixPosition.value = it.positionInRoot()
}
)
},
suffix = {
- Text(
- text = "S",
- modifier = Modifier.onGloballyPositioned {
+ Box(Modifier
+ .size(suffixSize)
+ .onGloballyPositioned {
suffixPosition.value = it.positionInRoot()
- suffixSize.value = it.size
}
)
}
@@ -944,7 +954,7 @@
// suffix
assertThat(suffixPosition.value?.x).isWithin(1f).of(
- (textFieldWidth - ExpectedPadding - suffixSize.value!!.width.toDp()).toPx()
+ (textFieldWidth - ExpectedPadding - suffixSize).toPx()
)
assertThat(suffixPosition.value?.y).isWithin(1f).of(ExpectedPadding.toPx())
}
@@ -954,10 +964,10 @@
@Test
fun testOutlinedTextField_prefixAndSuffixPosition_withIcons() {
val textFieldWidth = 300.dp
- val textFieldHeight = 60.dp
val prefixPosition = Ref<Offset>()
+ val prefixSize = MinTextLineHeight
val suffixPosition = Ref<Offset>()
- val suffixSize = Ref<IntSize>()
+ val suffixSize = MinTextLineHeight
val density = Density(2f)
rule.setMaterialContent(lightColorScheme()) {
@@ -965,21 +975,20 @@
OutlinedTextField(
value = "text",
onValueChange = {},
- modifier = Modifier.size(textFieldWidth, textFieldHeight),
+ modifier = Modifier.width(textFieldWidth),
prefix = {
- Text(
- text = "P",
- modifier = Modifier.onGloballyPositioned {
+ Box(Modifier
+ .size(prefixSize)
+ .onGloballyPositioned {
prefixPosition.value = it.positionInRoot()
}
)
},
suffix = {
- Text(
- text = "S",
- modifier = Modifier.onGloballyPositioned {
+ Box(Modifier
+ .size(suffixSize)
+ .onGloballyPositioned {
suffixPosition.value = it.positionInRoot()
- suffixSize.value = it.size
}
)
},
@@ -995,13 +1004,13 @@
// prefix
assertThat(prefixPosition.value?.x).isWithin(1f).of(
- (ExpectedPadding + IconPadding + iconSize).toPx())
+ (ExpectedPadding + IconPadding + iconSize).toPx()
+ )
assertThat(prefixPosition.value?.y).isWithin(1f).of(ExpectedPadding.toPx())
// suffix
assertThat(suffixPosition.value?.x).isWithin(1f).of(
- (textFieldWidth - IconPadding - iconSize - ExpectedPadding -
- suffixSize.value!!.width.toDp()).toPx()
+ (textFieldWidth - IconPadding - iconSize - ExpectedPadding - suffixSize).toPx()
)
assertThat(suffixPosition.value?.y).isWithin(1f).of(ExpectedPadding.toPx())
}
@@ -1012,28 +1021,26 @@
fun testOutlinedTextField_labelPositionX_initial_withTrailingAndLeading() {
val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
- Box {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = {
- Text(
- text = "label",
- modifier = Modifier.onGloballyPositioned {
- labelPosition.value = it.positionInRoot()
- }
- )
- },
- trailingIcon = { Icon(Icons.Default.Favorite, null) },
- leadingIcon = { Icon(Icons.Default.Favorite, null) }
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = {
+ Text(
+ text = "label",
+ modifier = Modifier.onGloballyPositioned {
+ labelPosition.value = it.positionInRoot()
+ }
+ )
+ },
+ trailingIcon = { Icon(Icons.Default.Favorite, null) },
+ leadingIcon = { Icon(Icons.Default.Favorite, null) }
+ )
}
rule.runOnIdleWithDensity {
val iconSize = 24.dp // default icon size
- assertThat(labelPosition.value?.x).isEqualTo(
- (ExpectedPadding + IconPadding + iconSize).roundToPx().toFloat()
+ assertThat(labelPosition.value?.x).isWithin(1f).of(
+ (ExpectedPadding + IconPadding + iconSize).toPx()
)
}
}
@@ -1042,28 +1049,24 @@
fun testOutlinedTextField_labelPositionX_initial_withNullTrailingAndLeading() {
val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
- Box {
- OutlinedTextField(
- value = "",
- onValueChange = {},
- label = {
- Text(
- text = "label",
- modifier = Modifier.onGloballyPositioned {
- labelPosition.value = it.positionInRoot()
- }
- )
- },
- trailingIcon = null,
- leadingIcon = null
- )
- }
+ OutlinedTextField(
+ value = "",
+ onValueChange = {},
+ label = {
+ Text(
+ text = "label",
+ modifier = Modifier.onGloballyPositioned {
+ labelPosition.value = it.positionInRoot()
+ }
+ )
+ },
+ trailingIcon = null,
+ leadingIcon = null
+ )
}
rule.runOnIdleWithDensity {
- assertThat(labelPosition.value?.x).isEqualTo(
- ExpectedPadding.roundToPx().toFloat()
- )
+ assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
}
}
@@ -1106,21 +1109,16 @@
@Test
fun testOutlinedTextField_supportingText_position() {
- val tfSize = Ref<IntSize>()
- val supportingSize = Ref<IntSize>()
val supportingPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
value = "",
onValueChange = {},
- modifier = Modifier.onGloballyPositioned {
- tfSize.value = it.size
- },
+ textStyle = TextStyle(fontSize = 1.sp), // ensure text size is minimum
supportingText = {
- Text(
- text = "Supporting",
- modifier = Modifier.onGloballyPositioned {
- supportingSize.value = it.size
+ Box(Modifier
+ .size(MinSupportingTextLineHeight)
+ .onGloballyPositioned {
supportingPosition.value = it.positionInRoot()
}
)
@@ -1129,11 +1127,9 @@
}
rule.runOnIdleWithDensity {
- assertThat(supportingPosition.value?.x).isEqualTo(
- ExpectedPadding.roundToPx().toFloat()
- )
- assertThat(supportingPosition.value?.y).isEqualTo(
- tfSize.value!!.height - supportingSize.value!!.height
+ assertThat(supportingPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ assertThat(supportingPosition.value?.y).isWithin(1f).of(
+ (ExpectedMinimumTextFieldHeight + SupportingTopPadding).toPx()
)
}
}
@@ -1277,7 +1273,7 @@
}
@Test
- fun testErrorSemantics_defaultMessage() {
+ fun testOutlinedTextField_errorSemantics_defaultMessage() {
lateinit var errorMessage: String
rule.setMaterialContent(lightColorScheme()) {
OutlinedTextField(
@@ -1296,7 +1292,7 @@
}
@Test
- fun testErrorSemantics_messageOverridable() {
+ fun testOutlinedTextField_errorSemantics_messageOverridable() {
val errorMessage = "Special symbols not allowed"
rule.setMaterialContent(lightColorScheme()) {
val isError = remember { mutableStateOf(true) }
@@ -1602,7 +1598,7 @@
OutlinedTextField(
value = text.value,
onValueChange = { text.value = it },
- placeholder = { Text("placeholder") }
+ textStyle = TextStyle(fontSize = 1.sp), // ensure text size is minimum
)
Divider(Modifier.fillMaxHeight())
}
@@ -1626,7 +1622,7 @@
OutlinedTextField(
value = text.value,
onValueChange = { text.value = it },
- placeholder = { Text("placeholder") },
+ textStyle = TextStyle(fontSize = 1.sp), // ensure text size is minimum
leadingIcon = { Icon(Icons.Default.Favorite, null) }
)
Divider(Modifier.fillMaxHeight())
@@ -1651,7 +1647,7 @@
OutlinedTextField(
value = text.value,
onValueChange = { text.value = it },
- placeholder = { Text("placeholder") },
+ textStyle = TextStyle(fontSize = 1.sp), // ensure text size is minimum
trailingIcon = { Icon(Icons.Default.Favorite, null) }
)
Divider(Modifier.fillMaxHeight())
@@ -1697,11 +1693,11 @@
}
}
- private fun getLabelPosition(labelSize: Ref<IntSize>): Int {
- val labelHalfHeight = labelSize.value!!.height / 2
+ private fun getLabelPosition(labelHeight: Int): Int {
+ val labelHalfHeight = labelHeight / 2
val paddingTop = with(rule.density) { OutlinedTextFieldTopPadding.toPx() }
- // vertical position is the default padding - half height
- // in case negative position, fix to 0
- return max(paddingTop - labelHalfHeight, 0f).roundToInt()
+ // Vertical position is the default padding - half height.
+ // This can be negative, meaning default padding is not enough for the focused label.
+ return (paddingTop - labelHalfHeight).roundToInt()
}
}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldTest.kt
index 447e330..4b994e6 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldTest.kt
@@ -31,7 +31,6 @@
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -126,7 +125,7 @@
val rule = createComposeRule()
@Test
- fun testTextField_minimumHeight() {
+ fun testTextField_setSmallHeight() {
rule.setMaterialContentForSizeAssertions {
TextField(
value = "input",
@@ -150,6 +149,18 @@
}
@Test
+ fun testTextField_defaultHeight() {
+ rule.setMaterialContentForSizeAssertions {
+ TextField(
+ value = "",
+ onValueChange = {},
+ textStyle = TextStyle(fontSize = 1.sp), // ensure text size is minimum
+ )
+ }
+ .assertHeightIsEqualTo(ExpectedDefaultTextFieldHeight)
+ }
+
+ @Test
fun testTextField_defaultWidth() {
rule.setMaterialContentForSizeAssertions {
TextField(
@@ -322,293 +333,207 @@
@Test
fun testTextField_labelPosition_initial_singleLine() {
- val labelSize = Ref<IntSize>()
val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
- Box {
- TextField(
- value = "",
- onValueChange = {},
- singleLine = true,
- label = {
- Text(
- text = "label",
- fontSize = 10.sp,
- modifier = Modifier
- .onGloballyPositioned {
- labelPosition.value = it.positionInRoot()
- labelSize.value = it.size
- }
- )
- },
- modifier = Modifier.height(56.dp)
- )
- }
+ TextField(
+ value = "",
+ onValueChange = {},
+ singleLine = true,
+ label = {
+ Box(Modifier
+ .size(MinTextLineHeight)
+ .onGloballyPositioned {
+ labelPosition.value = it.positionInRoot()
+ }
+ )
+ }
+ )
}
rule.runOnIdleWithDensity {
- // size
- assertThat(labelSize.value).isNotNull()
- assertThat(labelSize.value?.height).isGreaterThan(0)
- assertThat(labelSize.value?.width).isGreaterThan(0)
- // centered position
- assertThat(labelPosition.value?.x).isEqualTo(
- ExpectedPadding.roundToPx().toFloat()
- )
- assertThat(labelPosition.value?.y).isEqualTo(
- ((ExpectedDefaultTextFieldHeight.roundToPx() - labelSize.value!!.height) / 2f)
- .roundToInt().toFloat()
+ // x position is start + padding
+ assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ // y position is centered
+ assertThat(labelPosition.value?.y).isWithin(1f).of(
+ (ExpectedDefaultTextFieldHeight - MinTextLineHeight).toPx() / 2f
)
}
}
@Test
fun testTextField_labelPosition_initial_withDefaultHeight() {
- val labelSize = Ref<IntSize>()
val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
- Box {
- TextField(
- value = "",
- onValueChange = {},
- label = {
- Text(
- text = "label",
- fontSize = 10.sp,
- modifier = Modifier
- .onGloballyPositioned {
- labelPosition.value = it.positionInRoot()
- labelSize.value = it.size
- }
- )
- },
- modifier = Modifier.height(56.dp)
- )
- }
+ TextField(
+ value = "",
+ onValueChange = {},
+ label = {
+ Box(Modifier
+ .size(MinTextLineHeight)
+ .onGloballyPositioned {
+ labelPosition.value = it.positionInRoot()
+ }
+ )
+ }
+ )
}
rule.runOnIdleWithDensity {
- // size
- assertThat(labelSize.value).isNotNull()
- assertThat(labelSize.value?.height).isGreaterThan(0)
- assertThat(labelSize.value?.width).isGreaterThan(0)
- // centered position
- assertThat(labelPosition.value?.x).isEqualTo(
- ExpectedPadding.roundToPx().toFloat()
- )
- assertThat(labelPosition.value?.y).isEqualTo(
- ExpectedPadding.roundToPx()
- )
+ // x position is start + padding
+ assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ // y position is top + padding
+ assertThat(labelPosition.value?.y).isWithin(1f).of(ExpectedPadding.toPx())
}
}
@Test
fun testTextField_labelPosition_initial_withCustomHeight() {
val height = 80.dp
- val labelSize = Ref<IntSize>()
val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
- Box {
- TextField(
- value = "",
- onValueChange = {},
- modifier = Modifier.height(height),
- label = {
- Text(
- text = "label",
- modifier = Modifier.onGloballyPositioned {
- labelPosition.value = it.positionInRoot()
- labelSize.value = it.size
- }
- )
- }
- )
- }
+ TextField(
+ value = "",
+ onValueChange = {},
+ modifier = Modifier.height(height),
+ label = {
+ Box(Modifier
+ .size(MinTextLineHeight)
+ .onGloballyPositioned {
+ labelPosition.value = it.positionInRoot()
+ }
+ )
+ }
+ )
}
rule.runOnIdleWithDensity {
- // size
- assertThat(labelSize.value).isNotNull()
- assertThat(labelSize.value?.height).isGreaterThan(0)
- assertThat(labelSize.value?.width).isGreaterThan(0)
- assertThat(labelPosition.value?.x).isEqualTo(
- ExpectedPadding.roundToPx().toFloat()
- )
- assertThat(labelPosition.value?.y).isEqualTo(
- ExpectedPadding.roundToPx()
- )
+ // x position is start + padding
+ assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ // y position is top + padding
+ assertThat(labelPosition.value?.y).isWithin(1f).of(ExpectedPadding.toPx())
}
}
@Test
fun testTextField_labelPosition_whenFocused() {
- val labelSize = Ref<IntSize>()
val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
- Box {
- TextField(
- modifier = Modifier.testTag(TextFieldTag),
- value = "",
- onValueChange = {},
- label = {
- Text(
- text = "label",
- modifier = Modifier.onGloballyPositioned {
- labelPosition.value = it.positionInRoot()
- labelSize.value = it.size
- }
- )
- }
- )
- }
+ TextField(
+ modifier = Modifier.testTag(TextFieldTag),
+ value = "",
+ onValueChange = {},
+ label = {
+ Box(Modifier
+ .size(MinFocusedLabelLineHeight)
+ .onGloballyPositioned {
+ labelPosition.value = it.positionInRoot()
+ }
+ )
+ }
+ )
}
// click to focus
rule.onNodeWithTag(TextFieldTag).performClick()
rule.runOnIdleWithDensity {
- // size
- assertThat(labelSize.value).isNotNull()
- assertThat(labelSize.value?.height).isGreaterThan(0)
- assertThat(labelSize.value?.width).isGreaterThan(0)
-
- assertThat(labelPosition.value?.x).isEqualTo(
- ExpectedPadding.roundToPx().toFloat()
- )
- assertThat(labelPosition.value?.y).isEqualTo(
- TextFieldWithLabelVerticalPadding.roundToPx().toFloat()
+ // x position is start + padding
+ assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ // y position is top + (different) padding
+ assertThat(labelPosition.value?.y).isWithin(1f).of(
+ TextFieldWithLabelVerticalPadding.toPx()
)
}
}
@Test
fun testTextField_labelPosition_whenInput() {
- val labelSize = Ref<IntSize>()
val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
- Box {
- TextField(
- value = "input",
- onValueChange = {},
- label = {
- Text(
- text = "label",
- modifier = Modifier.onGloballyPositioned {
- labelPosition.value = it.positionInRoot()
- labelSize.value = it.size
- }
- )
- }
- )
- }
+ TextField(
+ value = "input",
+ onValueChange = {},
+ label = {
+ Box(Modifier
+ .size(MinFocusedLabelLineHeight)
+ .onGloballyPositioned {
+ labelPosition.value = it.positionInRoot()
+ }
+ )
+ }
+ )
}
rule.runOnIdleWithDensity {
- // size
- assertThat(labelSize.value).isNotNull()
- assertThat(labelSize.value?.height).isGreaterThan(0)
- assertThat(labelSize.value?.width).isGreaterThan(0)
-
- assertThat(labelPosition.value?.x).isEqualTo(
- ExpectedPadding.roundToPx().toFloat()
- )
- assertThat(labelPosition.value?.y).isEqualTo(
- TextFieldWithLabelVerticalPadding.roundToPx().toFloat()
+ // x position is start + padding
+ assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ // y position is top + (different) padding
+ assertThat(labelPosition.value?.y).isWithin(1f).of(
+ TextFieldWithLabelVerticalPadding.toPx()
)
}
}
@Test
fun testTextField_placeholderPosition_withLabel() {
- val labelSize = Ref<IntSize>()
- val placeholderSize = Ref<IntSize>()
val placeholderPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
- Box {
- TextField(
- modifier = Modifier
- .height(60.dp)
- .testTag(TextFieldTag),
- value = "",
- onValueChange = {},
- label = {
- Text(
- text = "label",
- modifier = Modifier.onGloballyPositioned {
- labelSize.value = it.size
- }
- )
- },
- placeholder = {
- Text(
- text = "placeholder",
- modifier = Modifier.onGloballyPositioned {
- placeholderPosition.value = it.positionInRoot()
- placeholderSize.value = it.size
- }
- )
- }
- )
- }
+ TextField(
+ modifier = Modifier.testTag(TextFieldTag),
+ value = "",
+ onValueChange = {},
+ label = { Box(Modifier.size(MinFocusedLabelLineHeight)) },
+ placeholder = {
+ Box(Modifier
+ .size(MinTextLineHeight)
+ .onGloballyPositioned {
+ placeholderPosition.value = it.positionInRoot()
+ }
+ )
+ }
+ )
}
+
// click to focus
rule.onNodeWithTag(TextFieldTag).performClick()
rule.runOnIdleWithDensity {
- // size
- assertThat(labelSize.value).isNotNull()
- assertThat(placeholderSize.value).isNotNull()
- assertThat(placeholderSize.value?.height).isGreaterThan(0)
- assertThat(placeholderSize.value?.width).isGreaterThan(0)
- // placeholder's position
+ // x position is start + padding
assertThat(placeholderPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
- assertThat(placeholderPosition.value?.y).isWithin(1f)
- .of(
- TextFieldWithLabelVerticalPadding.toPx() +
- labelSize.value!!.height.toFloat()
- )
+ // y position is top + padding + label height
+ assertThat(placeholderPosition.value?.y).isWithin(1f).of(
+ (TextFieldWithLabelVerticalPadding + MinFocusedLabelLineHeight).toPx()
+ )
}
}
@Test
fun testTextField_placeholderPosition_whenNoLabel() {
- val placeholderSize = Ref<IntSize>()
val placeholderPosition = Ref<Offset>()
- val height = 60.dp
rule.setMaterialContent(lightColorScheme()) {
- Box {
- TextField(
- modifier = Modifier.height(height).testTag(TextFieldTag),
- value = "",
- onValueChange = {},
- placeholder = {
- Text(
- text = "placeholder",
- modifier = Modifier.requiredHeight(20.dp)
- .onGloballyPositioned {
- placeholderPosition.value = it.positionInRoot()
- placeholderSize.value = it.size
- }
- )
- }
- )
- }
+ TextField(
+ modifier = Modifier.testTag(TextFieldTag),
+ value = "",
+ onValueChange = {},
+ placeholder = {
+ Box(Modifier
+ .size(MinTextLineHeight)
+ .onGloballyPositioned {
+ placeholderPosition.value = it.positionInRoot()
+ }
+ )
+ }
+ )
}
+
// click to focus
rule.onNodeWithTag(TextFieldTag).performClick()
rule.runOnIdleWithDensity {
- // size
- assertThat(placeholderSize.value).isNotNull()
- assertThat(placeholderSize.value?.height).isEqualTo(20.dp.roundToPx())
- assertThat(placeholderSize.value?.width).isGreaterThan(0)
- // centered position
- assertThat(placeholderPosition.value?.x).isEqualTo(
- ExpectedPadding.roundToPx().toFloat()
- )
- assertThat(placeholderPosition.value?.y).isEqualTo(
- TextFieldPadding.roundToPx()
- )
+ // x position is start + padding
+ assertThat(placeholderPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ // y position is top + padding
+ assertThat(placeholderPosition.value?.y).isWithin(1f).of(ExpectedPadding.toPx())
}
}
@@ -617,23 +542,20 @@
val placeholderSize = Ref<IntSize>()
val placeholderPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
- Column {
- TextField(
- modifier = Modifier.testTag(TextFieldTag),
- value = "input",
- onValueChange = {},
-
- placeholder = {
- Text(
- text = "placeholder",
- modifier = Modifier.onGloballyPositioned {
- placeholderPosition.value = it.positionInRoot()
- placeholderSize.value = it.size
- }
- )
- }
- )
- }
+ TextField(
+ modifier = Modifier.testTag(TextFieldTag),
+ value = "input",
+ onValueChange = {},
+ placeholder = {
+ Text(
+ text = "placeholder",
+ modifier = Modifier.onGloballyPositioned {
+ placeholderPosition.value = it.positionInRoot()
+ placeholderSize.value = it.size
+ }
+ )
+ }
+ )
}
// click to focus
@@ -665,6 +587,63 @@
}
@Test
+ fun testTextField_labelAndPlaceholderPosition_whenSmallerThanMinimumHeight() {
+ val labelSize = 10.dp
+ val labelPosition = Ref<Offset>()
+ val placeholderSize = 20.dp
+ val placeholderPosition = Ref<Offset>()
+ rule.setMaterialContent(lightColorScheme()) {
+ TextField(
+ modifier = Modifier.testTag(TextFieldTag),
+ value = "",
+ onValueChange = {},
+ label = {
+ Box(Modifier
+ .size(labelSize)
+ .onGloballyPositioned {
+ labelPosition.value = it.positionInRoot()
+ }
+ )
+ },
+ placeholder = {
+ Box(Modifier
+ .size(placeholderSize)
+ .onGloballyPositioned {
+ placeholderPosition.value = it.positionInRoot()
+ }
+ )
+ }
+ )
+ }
+
+ // click to focus
+ rule.onNodeWithTag(TextFieldTag).performClick()
+
+ rule.runOnIdleWithDensity {
+ // size
+ assertThat(labelSize).isLessThan(MinFocusedLabelLineHeight)
+ assertThat(placeholderSize).isLessThan(MinTextLineHeight)
+
+ // label x position is start + padding
+ assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ // label y position is top + padding, then centered within allocated space
+ assertThat(labelPosition.value?.y).isWithin(1f).of(
+ (TextFieldWithLabelVerticalPadding + (MinFocusedLabelLineHeight - labelSize) / 2)
+ .toPx()
+ )
+
+ // placeholder x position is start + padding
+ assertThat(placeholderPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ // placeholder y position is top + padding + label height, then centered within
+ // allocated space
+ assertThat(placeholderPosition.value?.y).isWithin(1f).of(
+ (TextFieldWithLabelVerticalPadding + MinFocusedLabelLineHeight +
+ (MinTextLineHeight - placeholderSize) / 2).toPx()
+ )
+ }
+ }
+
+ @Test
fun testTextField_trailingAndLeading_sizeAndPosition_defaultIcon() {
val textFieldHeight = 60.dp
val leadingPosition = Ref<Offset>()
@@ -868,11 +847,10 @@
@Test
fun testTextField_prefixAndSuffixPosition_withLabel() {
- val textFieldHeight = 60.dp
- val labelSize = Ref<IntSize>()
val prefixPosition = Ref<Offset>()
+ val prefixSize = MinTextLineHeight
val suffixPosition = Ref<Offset>()
- val suffixSize = Ref<IntSize>()
+ val suffixSize = MinTextLineHeight
val density = Density(2f)
rule.setMaterialContent(lightColorScheme()) {
@@ -880,29 +858,21 @@
TextField(
value = "text",
onValueChange = {},
- modifier = Modifier.size(TextFieldWidth, textFieldHeight),
- label = {
- Text(
- text = "label",
- modifier = Modifier.onGloballyPositioned {
- labelSize.value = it.size
- }
- )
- },
+ modifier = Modifier.width(TextFieldWidth),
+ label = { Box(Modifier.size(MinFocusedLabelLineHeight)) },
prefix = {
- Text(
- text = "P",
- modifier = Modifier.onGloballyPositioned {
+ Box(Modifier
+ .size(prefixSize)
+ .onGloballyPositioned {
prefixPosition.value = it.positionInRoot()
}
)
},
suffix = {
- Text(
- text = "S",
- modifier = Modifier.onGloballyPositioned {
+ Box(Modifier
+ .size(suffixSize)
+ .onGloballyPositioned {
suffixPosition.value = it.positionInRoot()
- suffixSize.value = it.size
}
)
}
@@ -914,33 +884,27 @@
with(density) {
// prefix
assertThat(prefixPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
- assertThat(prefixPosition.value?.y)
- .isWithin(1f)
- .of(
- TextFieldWithLabelVerticalPadding.toPx() +
- labelSize.value!!.height.toFloat()
- )
+ assertThat(prefixPosition.value?.y).isWithin(1f).of(
+ (TextFieldWithLabelVerticalPadding + MinFocusedLabelLineHeight).toPx()
+ )
// suffix
assertThat(suffixPosition.value?.x).isWithin(1f).of(
- (TextFieldWidth - ExpectedPadding - suffixSize.value!!.width.toDp()).toPx()
+ (TextFieldWidth - ExpectedPadding - suffixSize).toPx()
)
- assertThat(suffixPosition.value?.y)
- .isWithin(1f)
- .of(
- TextFieldWithLabelVerticalPadding.toPx() +
- labelSize.value!!.height.toFloat()
- )
+ assertThat(suffixPosition.value?.y).isWithin(1f).of(
+ (TextFieldWithLabelVerticalPadding + MinFocusedLabelLineHeight).toPx()
+ )
}
}
}
@Test
fun testTextField_prefixAndSuffixPosition_whenNoLabel() {
- val textFieldHeight = 60.dp
val prefixPosition = Ref<Offset>()
+ val prefixSize = MinTextLineHeight
val suffixPosition = Ref<Offset>()
- val suffixSize = Ref<IntSize>()
+ val suffixSize = MinTextLineHeight
val density = Density(2f)
rule.setMaterialContent(lightColorScheme()) {
@@ -948,21 +912,20 @@
TextField(
value = "text",
onValueChange = {},
- modifier = Modifier.size(TextFieldWidth, textFieldHeight),
+ modifier = Modifier.width(TextFieldWidth),
prefix = {
- Text(
- text = "P",
- modifier = Modifier.onGloballyPositioned {
+ Box(Modifier
+ .size(prefixSize)
+ .onGloballyPositioned {
prefixPosition.value = it.positionInRoot()
}
)
},
suffix = {
- Text(
- text = "S",
- modifier = Modifier.onGloballyPositioned {
+ Box(Modifier
+ .size(suffixSize)
+ .onGloballyPositioned {
suffixPosition.value = it.positionInRoot()
- suffixSize.value = it.size
}
)
}
@@ -978,7 +941,7 @@
// suffix
assertThat(suffixPosition.value?.x).isWithin(1f).of(
- (TextFieldWidth - ExpectedPadding - suffixSize.value!!.width.toDp()).toPx()
+ (TextFieldWidth - ExpectedPadding - suffixSize).toPx()
)
assertThat(suffixPosition.value?.y).isWithin(1f).of(ExpectedPadding.toPx())
}
@@ -987,10 +950,10 @@
@Test
fun testTextField_prefixAndSuffixPosition_withIcons() {
- val textFieldHeight = 60.dp
val prefixPosition = Ref<Offset>()
+ val prefixSize = MinTextLineHeight
val suffixPosition = Ref<Offset>()
- val suffixSize = Ref<IntSize>()
+ val suffixSize = MinTextLineHeight
val density = Density(2f)
rule.setMaterialContent(lightColorScheme()) {
@@ -998,21 +961,20 @@
TextField(
value = "text",
onValueChange = {},
- modifier = Modifier.size(TextFieldWidth, textFieldHeight),
+ modifier = Modifier.width(TextFieldWidth),
prefix = {
- Text(
- text = "P",
- modifier = Modifier.onGloballyPositioned {
+ Box(Modifier
+ .size(prefixSize)
+ .onGloballyPositioned {
prefixPosition.value = it.positionInRoot()
}
)
},
suffix = {
- Text(
- text = "S",
- modifier = Modifier.onGloballyPositioned {
+ Box(Modifier
+ .size(suffixSize)
+ .onGloballyPositioned {
suffixPosition.value = it.positionInRoot()
- suffixSize.value = it.size
}
)
},
@@ -1028,13 +990,13 @@
// prefix
assertThat(prefixPosition.value?.x).isWithin(1f).of(
- (ExpectedPadding + IconPadding + iconSize).toPx())
+ (ExpectedPadding + IconPadding + iconSize).toPx()
+ )
assertThat(prefixPosition.value?.y).isWithin(1f).of(ExpectedPadding.toPx())
// suffix
assertThat(suffixPosition.value?.x).isWithin(1f).of(
- (TextFieldWidth - IconPadding - iconSize - ExpectedPadding -
- suffixSize.value!!.width.toDp()).toPx()
+ (TextFieldWidth - IconPadding - iconSize - ExpectedPadding - suffixSize).toPx()
)
assertThat(suffixPosition.value?.y).isWithin(1f).of(ExpectedPadding.toPx())
}
@@ -1043,64 +1005,54 @@
@Test
fun testTextField_labelPositionX_initial_withTrailingAndLeading() {
- val height = 60.dp
val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
- Box {
- TextField(
- value = "",
- onValueChange = {},
- modifier = Modifier.height(height),
- label = {
- Text(
- text = "label",
- modifier = Modifier.onGloballyPositioned {
- labelPosition.value = it.positionInRoot()
- }
- )
- },
- trailingIcon = { Icon(Icons.Default.Favorite, null) },
- leadingIcon = { Icon(Icons.Default.Favorite, null) }
- )
- }
+ TextField(
+ value = "",
+ onValueChange = {},
+ label = {
+ Text(
+ text = "label",
+ modifier = Modifier.onGloballyPositioned {
+ labelPosition.value = it.positionInRoot()
+ }
+ )
+ },
+ trailingIcon = { Icon(Icons.Default.Favorite, null) },
+ leadingIcon = { Icon(Icons.Default.Favorite, null) }
+ )
}
rule.runOnIdleWithDensity {
val iconSize = 24.dp // default icon size
- assertThat(labelPosition.value?.x).isEqualTo(
- (ExpectedPadding + IconPadding + iconSize).roundToPx().toFloat()
+ assertThat(labelPosition.value?.x).isWithin(1f).of(
+ (ExpectedPadding + IconPadding + iconSize).toPx()
)
}
}
@Test
fun testTextField_labelPositionX_initial_withNullTrailingAndLeading() {
- val height = 60.dp
val labelPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
- Box {
- TextField(
- value = "",
- onValueChange = {},
- modifier = Modifier.height(height),
- label = {
- Text(
- text = "label",
- modifier = Modifier.onGloballyPositioned {
- labelPosition.value = it.positionInRoot()
- }
- )
- },
- trailingIcon = null,
- leadingIcon = null
- )
- }
+ TextField(
+ value = "",
+ onValueChange = {},
+ label = {
+ Text(
+ text = "label",
+ modifier = Modifier.onGloballyPositioned {
+ labelPosition.value = it.positionInRoot()
+ }
+ )
+ },
+ trailingIcon = null,
+ leadingIcon = null
+ )
}
rule.runOnIdleWithDensity {
- assertThat(labelPosition.value?.x).isEqualTo(
- ExpectedPadding.roundToPx().toFloat()
- )
+ assertThat(labelPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
}
}
@@ -1142,22 +1094,17 @@
}
@Test
- fun testTextField_supportingText_position() {
- val tfSize = Ref<IntSize>()
- val supportingSize = Ref<IntSize>()
+ fun testTextField_supportingTextPosition_withDefaultHeight() {
val supportingPosition = Ref<Offset>()
rule.setMaterialContent(lightColorScheme()) {
TextField(
value = "",
onValueChange = {},
- modifier = Modifier.onGloballyPositioned {
- tfSize.value = it.size
- },
+ textStyle = TextStyle(fontSize = 1.sp), // ensure text size is minimum
supportingText = {
- Text(
- text = "Supporting",
- modifier = Modifier.onGloballyPositioned {
- supportingSize.value = it.size
+ Box(Modifier
+ .size(MinSupportingTextLineHeight)
+ .onGloballyPositioned {
supportingPosition.value = it.positionInRoot()
}
)
@@ -1166,11 +1113,9 @@
}
rule.runOnIdleWithDensity {
- assertThat(supportingPosition.value?.x).isEqualTo(
- ExpectedPadding.roundToPx().toFloat()
- )
- assertThat(supportingPosition.value?.y).isEqualTo(
- tfSize.value!!.height - supportingSize.value!!.height
+ assertThat(supportingPosition.value?.x).isWithin(1f).of(ExpectedPadding.toPx())
+ assertThat(supportingPosition.value?.y).isWithin(1f).of(
+ (ExpectedDefaultTextFieldHeight + SupportingTopPadding).toPx()
)
}
}
@@ -1374,7 +1319,7 @@
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
- fun testTransformedTextIsUsed_toDefineLabelPosition() {
+ fun testTextField_transformedTextIsUsed_toDefineLabelPosition() {
// if non-transformed value were used to check if the text input is empty, the label
// wouldn't be aligned to the top, as a result it would be obscured by text
val prefixTransformation = VisualTransformation { text ->
@@ -1410,7 +1355,7 @@
@Test
@LargeTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
- fun testTransformedTextIsUsed_toDefineIfPlaceholderNeeded() {
+ fun testTextField_transformedTextIsUsed_toDefineIfPlaceholderNeeded() {
// if original value were used to check if the text input is empty, the placeholder would be
// displayed on top of the text
val prefixTransformation = VisualTransformation { text ->
@@ -1454,7 +1399,7 @@
}
@Test
- fun testErrorSemantics_defaultMessage() {
+ fun testTextField_errorSemantics_defaultMessage() {
lateinit var errorMessage: String
rule.setMaterialContent(lightColorScheme()) {
TextField(
@@ -1471,10 +1416,10 @@
}
@Test
- fun testErrorSemantics_messageOverridable() {
+ fun testTextField_errorSemantics_messageOverridable() {
val errorMessage = "Special symbols not allowed"
rule.setMaterialContent(lightColorScheme()) {
- var isError = remember { mutableStateOf(true) }
+ val isError = remember { mutableStateOf(true) }
TextField(
value = "test",
onValueChange = {},
@@ -1732,7 +1677,7 @@
TextField(
value = text.value,
onValueChange = { text.value = it },
- placeholder = { Text("placeholder") }
+ textStyle = TextStyle(fontSize = 1.sp), // ensure text size is minimum
)
Divider(Modifier.fillMaxHeight())
}
@@ -1756,7 +1701,7 @@
TextField(
value = text.value,
onValueChange = { text.value = it },
- placeholder = { Text("placeholder") },
+ textStyle = TextStyle(fontSize = 1.sp), // ensure text size is minimum
leadingIcon = { Icon(Icons.Default.Favorite, null) }
)
Divider(Modifier.fillMaxHeight())
@@ -1781,7 +1726,7 @@
TextField(
value = text.value,
onValueChange = { text.value = it },
- placeholder = { Text("placeholder") },
+ textStyle = TextStyle(fontSize = 1.sp), // ensure text size is minimum
trailingIcon = { Icon(Icons.Default.Favorite, null) }
)
Divider(Modifier.fillMaxHeight())
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
index 939ade7..0f76f4d 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
@@ -23,7 +23,9 @@
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@@ -61,6 +63,7 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.coerceAtLeast
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.lerp
import androidx.compose.ui.unit.offset
import androidx.compose.ui.util.lerp
import kotlin.math.max
@@ -562,6 +565,8 @@
Box(
Modifier
.layoutId(PrefixId)
+ .heightIn(min = MinTextLineHeight)
+ .wrapContentHeight()
.padding(start = startPadding, end = PrefixSuffixTextPadding)
) {
prefix()
@@ -571,36 +576,51 @@
Box(
Modifier
.layoutId(SuffixId)
+ .heightIn(min = MinTextLineHeight)
+ .wrapContentHeight()
.padding(start = PrefixSuffixTextPadding, end = endPadding)
) {
suffix()
}
}
- val textPadding = Modifier.padding(
- start = if (prefix == null) startPadding else 0.dp,
- end = if (suffix == null) endPadding else 0.dp,
- )
+ val textPadding = Modifier
+ .heightIn(min = MinTextLineHeight)
+ .wrapContentHeight()
+ .padding(
+ start = if (prefix == null) startPadding else 0.dp,
+ end = if (suffix == null) endPadding else 0.dp,
+ )
if (placeholder != null) {
- placeholder(Modifier.layoutId(PlaceholderId).then(textPadding))
+ placeholder(Modifier
+ .layoutId(PlaceholderId)
+ .then(textPadding))
}
Box(
- modifier = Modifier.layoutId(TextFieldId).then(textPadding),
+ modifier = Modifier
+ .layoutId(TextFieldId)
+ .then(textPadding),
propagateMinConstraints = true
) {
textField()
}
if (label != null) {
- Box(modifier = Modifier.layoutId(LabelId)) { label() }
+ Box(Modifier
+ .heightIn(min = lerp(
+ MinTextLineHeight, MinFocusedLabelLineHeight, animationProgress))
+ .wrapContentHeight()
+ .layoutId(LabelId)) { label() }
}
if (supporting != null) {
@OptIn(ExperimentalMaterial3Api::class)
Box(Modifier
.layoutId(SupportingId)
+ .heightIn(min = MinSupportingTextLineHeight)
+ .wrapContentHeight()
.padding(TextFieldDefaults.supportingTextPadding())
) { supporting() }
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
index ddb51f2e..7010bcf 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
@@ -24,7 +24,9 @@
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@@ -59,6 +61,7 @@
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.coerceAtLeast
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.lerp
import androidx.compose.ui.unit.offset
import kotlin.math.max
import kotlin.math.roundToInt
@@ -558,6 +561,8 @@
Box(
Modifier
.layoutId(PrefixId)
+ .heightIn(min = MinTextLineHeight)
+ .wrapContentHeight()
.padding(start = startPadding, end = PrefixSuffixTextPadding)
) {
prefix()
@@ -567,6 +572,8 @@
Box(
Modifier
.layoutId(SuffixId)
+ .heightIn(min = MinTextLineHeight)
+ .wrapContentHeight()
.padding(start = PrefixSuffixTextPadding, end = endPadding)
) {
suffix()
@@ -577,19 +584,29 @@
Box(
Modifier
.layoutId(LabelId)
+ .heightIn(min = lerp(
+ MinTextLineHeight, MinFocusedLabelLineHeight, animationProgress))
+ .wrapContentHeight()
.padding(start = startPadding, end = endPadding)) { label() }
}
- val textPadding = Modifier.padding(
- start = if (prefix == null) startPadding else 0.dp,
- end = if (suffix == null) endPadding else 0.dp,
- )
+ val textPadding = Modifier
+ .heightIn(min = MinTextLineHeight)
+ .wrapContentHeight()
+ .padding(
+ start = if (prefix == null) startPadding else 0.dp,
+ end = if (suffix == null) endPadding else 0.dp,
+ )
if (placeholder != null) {
- placeholder(Modifier.layoutId(PlaceholderId).then(textPadding))
+ placeholder(Modifier
+ .layoutId(PlaceholderId)
+ .then(textPadding))
}
Box(
- modifier = Modifier.layoutId(TextFieldId).then(textPadding),
+ modifier = Modifier
+ .layoutId(TextFieldId)
+ .then(textPadding),
propagateMinConstraints = true,
) {
textField()
@@ -599,6 +616,8 @@
@OptIn(ExperimentalMaterial3Api::class)
Box(Modifier
.layoutId(SupportingId)
+ .heightIn(min = MinSupportingTextLineHeight)
+ .wrapContentHeight()
.padding(TextFieldDefaults.supportingTextPadding())
) { supporting() }
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldImpl.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldImpl.kt
index e90d4c1..6cfb01f 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldImpl.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldImpl.kt
@@ -422,5 +422,8 @@
internal val HorizontalIconPadding = 12.dp
internal val SupportingTopPadding = 4.dp
internal val PrefixSuffixTextPadding = 2.dp
+internal val MinTextLineHeight = 24.dp
+internal val MinFocusedLabelLineHeight = 16.dp
+internal val MinSupportingTextLineHeight = 16.dp
internal val IconDefaultSizeModifier = Modifier.defaultMinSize(48.dp, 48.dp)
\ No newline at end of file