Merge "BTF2 filtered text change should remove composition" into androidx-main
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index 21afe36..44cc5f7 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -19,8 +19,8 @@
import androidx.compose.foundation.demos.text2.BasicSecureTextFieldDemos
import androidx.compose.foundation.demos.text2.BasicTextFieldCustomPinFieldDemo
import androidx.compose.foundation.demos.text2.BasicTextFieldDemos
-import androidx.compose.foundation.demos.text2.BasicTextFieldFilterDemos
import androidx.compose.foundation.demos.text2.BasicTextFieldInScrollableDemo
+import androidx.compose.foundation.demos.text2.BasicTextFieldInputTransformationDemos
import androidx.compose.foundation.demos.text2.BasicTextFieldLongTextDemo
import androidx.compose.foundation.demos.text2.BasicTextFieldOutputTransformationDemos
import androidx.compose.foundation.demos.text2.BasicTextFieldValueCallbackDemo
@@ -169,12 +169,14 @@
ComposableDemo("Rtl") { ScrollableDemosRtl() },
)),
ComposableDemo("Inside Scrollable") { BasicTextFieldInScrollableDemo() },
- ComposableDemo("Filters") { BasicTextFieldFilterDemos() },
+ ComposableDemo("Input Transformation") {
+ BasicTextFieldInputTransformationDemos()
+ },
DemoCategory("Receive Content", listOf(
ComposableDemo("Basic") { TextFieldReceiveContentDemo() },
ComposableDemo("Nested") { NestedReceiveContentDemo() },
)),
- ComposableDemo("Output transformation") {
+ ComposableDemo("Output Transformation") {
BasicTextFieldOutputTransformationDemos()
},
ComposableDemo("Secure Field") { BasicSecureTextFieldDemos() },
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldFilterDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldInputTransformationDemos.kt
similarity index 90%
rename from compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldFilterDemos.kt
rename to compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldInputTransformationDemos.kt
index efdff20..1b5b733 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldFilterDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicTextFieldInputTransformationDemos.kt
@@ -37,6 +37,7 @@
import androidx.compose.foundation.text.input.TextFieldBuffer
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.allCaps
+import androidx.compose.foundation.text.input.forEachChange
import androidx.compose.foundation.text.input.maxLength
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Switch
@@ -52,7 +53,7 @@
import androidx.core.text.isDigitsOnly
@Composable
-fun BasicTextFieldFilterDemos() {
+fun BasicTextFieldInputTransformationDemos() {
Column(
Modifier
.imePadding()
@@ -67,6 +68,9 @@
TagLine(tag = "Digits Only BasicTextField")
DigitsOnlyDemo()
+ TagLine(tag = "Additive InputTransformation")
+ AdditiveInputTransformationDemo()
+
TagLine(tag = "Change filter")
ChangeFilterDemo()
@@ -97,7 +101,6 @@
}
}
-@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun DigitsOnlyDemo() {
FilterDemo(filter = object : InputTransformation {
@@ -114,6 +117,18 @@
}
@Composable
+private fun AdditiveInputTransformationDemo() {
+ FilterDemo(filter = {
+ changes.forEachChange { range, originalRange ->
+ // only extend the insertions
+ if (!range.collapsed && originalRange.collapsed) {
+ replace(range.end, range.end, "a")
+ }
+ }
+ })
+}
+
+@Composable
private fun FilterDemo(filter: InputTransformation) {
val state = remember { TextFieldState() }
BasicTextField(
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
index 6a23981..2717eb0 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
@@ -552,6 +552,25 @@
}
@Test
+ fun textField_appliesFilter_toInputConnection_changingComposition() {
+ val state = TextFieldState()
+ inputMethodInterceptor.setTextFieldTestContent {
+ BasicTextField(
+ state = state,
+ inputTransformation = RejectAllTextFilter,
+ modifier = Modifier.testTag(Tag)
+ )
+ }
+ requestFocus(Tag)
+
+ inputMethodInterceptor.withInputConnection {
+ setComposingText("hello", 1)
+ }
+ rule.onNodeWithTag(Tag).assertTextEquals("")
+ assertThat(state.composition).isNull()
+ }
+
+ @Test
fun textField_appliesFilter_toSetTextSemanticsAction() {
val state = TextFieldState()
inputMethodInterceptor.setTextFieldTestContent {
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt
index 2a4f9b2..6b053e2 100644
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt
+++ b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/input/TextFieldStateTest.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation.text.input
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.text.input.internal.setComposingText
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.snapshots.SnapshotStateObserver
@@ -699,6 +700,18 @@
assertThat(transformationCalled).isEqualTo(0)
}
+ @Test
+ fun inputTransformationRejectsChanges_removesComposition() {
+ val state = TextFieldState()
+ val inputTransformation = InputTransformation { revertAllChanges() }
+ state.editAsUser(inputTransformation) {
+ setComposingText("hello", 1)
+ }
+ assertThat(state.text).isEqualTo("")
+ assertThat(state.selection).isEqualTo(TextRange.Zero)
+ assertThat(state.composition).isNull()
+ }
+
private fun runTestWithSnapshotsThenCancelChildren(testBody: suspend TestScope.() -> Unit) {
val globalWriteObserverHandle = Snapshot.registerGlobalWriteObserver {
// This is normally done by the compose runtime.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
index 20cfe61..49f0790 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/TextFieldState.kt
@@ -377,7 +377,9 @@
if (textChangedByFilter || selectionChangedByFilter) {
syncMainBufferToTemporaryBuffer(
textFieldBuffer = textFieldBuffer,
- newComposition = afterEditValue.composition,
+ // Composition should be decided by the IME after the content or selection has been
+ // changed programmatically, outside the knowledge of IME.
+ newComposition = null,
textChanged = textChangedByFilter,
selectionChanged = selectionChangedByFilter
)