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..14c11fa 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() @@ -344,6 +344,7 @@ fun BasicChipTextField( textFieldFocusRequester.freeFocus() } } + state.consumeTextFieldFocusRequest(request) } } @@ -604,10 +605,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..6a20789 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) @@ -110,12 +113,24 @@ class ChipTextFieldState( internal fun updateFocusedChip(chip: T?) { if (chip != null) { - textFieldFocusState = TextFieldFocusState.None + textFieldFocusRequest = textFieldFocusRequest.copy( + state = TextFieldFocusState.None, + ) } this._focusedChip = chip this._focusedChipIndex = chips.indexOf(chip) } + internal fun updateTextFieldFocus(isFocused: Boolean) { + _isTextFieldFocused = isFocused + } + + internal fun consumeTextFieldFocusRequest(request: TextFieldFocusRequest) { + if (textFieldFocusRequest == request) { + textFieldFocusRequest = request.copy(state = TextFieldFocusState.None) + } + } + /** * Focus a chip by index. */ @@ -141,17 +156,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..d451fca --- /dev/null +++ b/chiptextfield-core/src/commonTest/kotlin/com/dokar/chiptextfield/ChipTextFieldStateTest.kt @@ -0,0 +1,77 @@ +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) + } + + @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) + } + + @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) + } +}