From 465274abaab734712ad90c089996d0f7639910db Mon Sep 17 00:00:00 2001 From: dokar3 <68095777+dokar3@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:44:27 +0800 Subject: [PATCH 1/3] Fix chip text field focus synchronization --- chiptextfield-core/build.gradle | 5 +++ .../dokar/chiptextfield/BasicChipTextField.kt | 10 ++--- .../dokar/chiptextfield/ChipTextFieldState.kt | 30 +++++++++++---- .../chiptextfield/ChipTextFieldStateTest.kt | 38 +++++++++++++++++++ 4 files changed, 70 insertions(+), 13 deletions(-) create mode 100644 chiptextfield-core/src/commonTest/kotlin/com/dokar/chiptextfield/ChipTextFieldStateTest.kt diff --git a/chiptextfield-core/build.gradle b/chiptextfield-core/build.gradle index 2099d49..268ebd2 100644 --- a/chiptextfield-core/build.gradle +++ b/chiptextfield-core/build.gradle @@ -53,5 +53,10 @@ kotlin { api(libs.jetbrains.compose.foundation) } } + commonTest { + dependencies { + implementation(kotlin("test")) + } + } } } diff --git a/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/BasicChipTextField.kt b/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/BasicChipTextField.kt index 4d25271..ea3081f 100644 --- a/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/BasicChipTextField.kt +++ b/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/BasicChipTextField.kt @@ -330,10 +330,10 @@ fun BasicChipTextField( } LaunchedEffect(focusManager, state, textFieldFocusRequester) { - snapshotFlow { state.textFieldFocusState } + snapshotFlow { state.textFieldFocusRequest } .distinctUntilChanged() - .collect { - when (it) { + .collect { request -> + when (request.state) { TextFieldFocusState.None -> {} TextFieldFocusState.Focused -> runCatching { textFieldFocusRequester.requestFocus() @@ -604,10 +604,8 @@ private fun Input( modifier = modifier .focusRequester(focusRequester) .onFocusChanged { + state.updateTextFieldFocus(it.isFocused) onFocusChange(it.isFocused) - if (it.isFocused) { - state.focusTextField() - } } .onPreviewKeyEvent { if (it.type == KeyEventType.KeyDown && it.key == Key.Backspace) { diff --git a/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/ChipTextFieldState.kt b/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/ChipTextFieldState.kt index a672af8..3488e8e 100644 --- a/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/ChipTextFieldState.kt +++ b/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/ChipTextFieldState.kt @@ -58,9 +58,12 @@ class ChipTextFieldState( internal var recordFocusedChip = true - internal var textFieldFocusState by mutableStateOf(TextFieldFocusState.None) + private var nextTextFieldFocusRequestId = 0 - val isTextFieldFocused get() = textFieldFocusState == TextFieldFocusState.Focused + internal var textFieldFocusRequest by mutableStateOf(TextFieldFocusRequest()) + + private var _isTextFieldFocused by mutableStateOf(false) + val isTextFieldFocused get() = _isTextFieldFocused var chips by mutableStateOf(chips) @@ -109,13 +112,14 @@ class ChipTextFieldState( } internal fun updateFocusedChip(chip: T?) { - if (chip != null) { - textFieldFocusState = TextFieldFocusState.None - } this._focusedChip = chip this._focusedChipIndex = chips.indexOf(chip) } + internal fun updateTextFieldFocus(isFocused: Boolean) { + _isTextFieldFocused = isFocused + } + /** * Focus a chip by index. */ @@ -141,17 +145,29 @@ class ChipTextFieldState( * Focus the text field at the end of chips. */ fun focusTextField() { - textFieldFocusState = TextFieldFocusState.Focused + updateTextFieldFocusRequest(TextFieldFocusState.Focused) } /** * Clear focus from the focused text field at the end chips. */ fun clearTextFieldFocus() { - textFieldFocusState = TextFieldFocusState.Unfocused + updateTextFieldFocusRequest(TextFieldFocusState.Unfocused) + } + + private fun updateTextFieldFocusRequest(state: TextFieldFocusState) { + textFieldFocusRequest = TextFieldFocusRequest( + id = ++nextTextFieldFocusRequestId, + state = state, + ) } } +internal data class TextFieldFocusRequest( + val id: Int = 0, + val state: TextFieldFocusState = TextFieldFocusState.None, +) + internal enum class TextFieldFocusState { None, Focused, diff --git a/chiptextfield-core/src/commonTest/kotlin/com/dokar/chiptextfield/ChipTextFieldStateTest.kt b/chiptextfield-core/src/commonTest/kotlin/com/dokar/chiptextfield/ChipTextFieldStateTest.kt new file mode 100644 index 0000000..55f447f --- /dev/null +++ b/chiptextfield-core/src/commonTest/kotlin/com/dokar/chiptextfield/ChipTextFieldStateTest.kt @@ -0,0 +1,38 @@ +package com.dokar.chiptextfield + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +class ChipTextFieldStateTest { + @Test + fun textFieldFocusReflectsUiFocus() { + val state = ChipTextFieldState() + + assertFalse(state.isTextFieldFocused) + + state.updateTextFieldFocus(true) + assertTrue(state.isTextFieldFocused) + + state.updateTextFieldFocus(false) + assertFalse(state.isTextFieldFocused) + } + + @Test + fun repeatedTextFieldFocusRequestsAreDistinct() { + val state = ChipTextFieldState() + + state.focusTextField() + val firstRequest = state.textFieldFocusRequest + assertFalse(state.isTextFieldFocused) + + state.focusTextField() + val secondRequest = state.textFieldFocusRequest + + assertEquals(TextFieldFocusState.Focused, firstRequest.state) + assertEquals(TextFieldFocusState.Focused, secondRequest.state) + assertNotEquals(firstRequest, secondRequest) + } +} From 79f9e729a110527d7b7d9c38bad440fd4f3a2c49 Mon Sep 17 00:00:00 2001 From: dokar3 <68095777+dokar3@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:54:23 +0800 Subject: [PATCH 2/3] Consume text field focus requests --- .../dokar/chiptextfield/BasicChipTextField.kt | 1 + .../dokar/chiptextfield/ChipTextFieldState.kt | 6 +++++ .../chiptextfield/ChipTextFieldStateTest.kt | 25 +++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/BasicChipTextField.kt b/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/BasicChipTextField.kt index ea3081f..14c11fa 100644 --- a/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/BasicChipTextField.kt +++ b/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/BasicChipTextField.kt @@ -344,6 +344,7 @@ fun BasicChipTextField( textFieldFocusRequester.freeFocus() } } + state.consumeTextFieldFocusRequest(request) } } diff --git a/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/ChipTextFieldState.kt b/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/ChipTextFieldState.kt index 3488e8e..640482c 100644 --- a/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/ChipTextFieldState.kt +++ b/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/ChipTextFieldState.kt @@ -120,6 +120,12 @@ class ChipTextFieldState( _isTextFieldFocused = isFocused } + internal fun consumeTextFieldFocusRequest(request: TextFieldFocusRequest) { + if (textFieldFocusRequest == request) { + textFieldFocusRequest = request.copy(state = TextFieldFocusState.None) + } + } + /** * Focus a chip by index. */ diff --git a/chiptextfield-core/src/commonTest/kotlin/com/dokar/chiptextfield/ChipTextFieldStateTest.kt b/chiptextfield-core/src/commonTest/kotlin/com/dokar/chiptextfield/ChipTextFieldStateTest.kt index 55f447f..e482bff 100644 --- a/chiptextfield-core/src/commonTest/kotlin/com/dokar/chiptextfield/ChipTextFieldStateTest.kt +++ b/chiptextfield-core/src/commonTest/kotlin/com/dokar/chiptextfield/ChipTextFieldStateTest.kt @@ -35,4 +35,29 @@ class ChipTextFieldStateTest { assertEquals(TextFieldFocusState.Focused, secondRequest.state) assertNotEquals(firstRequest, secondRequest) } + + @Test + fun consumedTextFieldFocusRequestBecomesInert() { + val state = ChipTextFieldState() + state.focusTextField() + val request = state.textFieldFocusRequest + + state.consumeTextFieldFocusRequest(request) + + assertEquals(TextFieldFocusState.None, state.textFieldFocusRequest.state) + } + + @Test + fun consumingOldTextFieldFocusRequestDoesNotClearNewRequest() { + val state = ChipTextFieldState() + state.focusTextField() + val oldRequest = state.textFieldFocusRequest + state.clearTextFieldFocus() + val newRequest = state.textFieldFocusRequest + + state.consumeTextFieldFocusRequest(oldRequest) + + assertEquals(newRequest, state.textFieldFocusRequest) + assertEquals(TextFieldFocusState.Unfocused, state.textFieldFocusRequest.state) + } } From b55bfea9fdfa88e6ef1998443b91bd3c69f67480 Mon Sep 17 00:00:00 2001 From: dokar3 <68095777+dokar3@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:00:24 +0800 Subject: [PATCH 3/3] Cancel pending text field focus for chips --- .../com/dokar/chiptextfield/ChipTextFieldState.kt | 5 +++++ .../dokar/chiptextfield/ChipTextFieldStateTest.kt | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/ChipTextFieldState.kt b/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/ChipTextFieldState.kt index 640482c..6a20789 100644 --- a/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/ChipTextFieldState.kt +++ b/chiptextfield-core/src/commonMain/kotlin/com/dokar/chiptextfield/ChipTextFieldState.kt @@ -112,6 +112,11 @@ class ChipTextFieldState( } internal fun updateFocusedChip(chip: T?) { + if (chip != null) { + textFieldFocusRequest = textFieldFocusRequest.copy( + state = TextFieldFocusState.None, + ) + } this._focusedChip = chip this._focusedChipIndex = chips.indexOf(chip) } diff --git a/chiptextfield-core/src/commonTest/kotlin/com/dokar/chiptextfield/ChipTextFieldStateTest.kt b/chiptextfield-core/src/commonTest/kotlin/com/dokar/chiptextfield/ChipTextFieldStateTest.kt index e482bff..d451fca 100644 --- a/chiptextfield-core/src/commonTest/kotlin/com/dokar/chiptextfield/ChipTextFieldStateTest.kt +++ b/chiptextfield-core/src/commonTest/kotlin/com/dokar/chiptextfield/ChipTextFieldStateTest.kt @@ -60,4 +60,18 @@ class ChipTextFieldStateTest { assertEquals(newRequest, state.textFieldFocusRequest) assertEquals(TextFieldFocusState.Unfocused, state.textFieldFocusRequest.state) } + + @Test + fun focusingChipCancelsPendingTextFieldFocusRequest() { + val chip = Chip("Chip") + val state = ChipTextFieldState(chips = listOf(chip)) + state.focusTextField() + val pendingRequest = state.textFieldFocusRequest + + state.focusChip(0) + + assertEquals(pendingRequest.id, state.textFieldFocusRequest.id) + assertEquals(TextFieldFocusState.None, state.textFieldFocusRequest.state) + assertEquals(0, state.focusedChipIndex) + } }