Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions chiptextfield-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,10 @@ kotlin {
api(libs.jetbrains.compose.foundation)
}
}
commonTest {
dependencies {
implementation(kotlin("test"))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -330,10 +330,10 @@ fun <T : Chip> 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()
Expand All @@ -344,6 +344,7 @@ fun <T : Chip> BasicChipTextField(
textFieldFocusRequester.freeFocus()
}
}
state.consumeTextFieldFocusRequest(request)
}
}

Expand Down Expand Up @@ -604,10 +605,8 @@ private fun <T : Chip> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,12 @@ class ChipTextFieldState<T : Chip>(

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)

Expand Down Expand Up @@ -110,12 +113,24 @@ class ChipTextFieldState<T : Chip>(

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.
*/
Expand All @@ -141,17 +156,29 @@ class ChipTextFieldState<T : Chip>(
* 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,
)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
internal enum class TextFieldFocusState {
None,
Focused,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Chip>()

assertFalse(state.isTextFieldFocused)

state.updateTextFieldFocus(true)
assertTrue(state.isTextFieldFocused)

state.updateTextFieldFocus(false)
assertFalse(state.isTextFieldFocused)
}

@Test
fun repeatedTextFieldFocusRequestsAreDistinct() {
val state = ChipTextFieldState<Chip>()

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<Chip>()
state.focusTextField()
val request = state.textFieldFocusRequest

state.consumeTextFieldFocusRequest(request)

assertEquals(TextFieldFocusState.None, state.textFieldFocusRequest.state)
}

@Test
fun consumingOldTextFieldFocusRequestDoesNotClearNewRequest() {
val state = ChipTextFieldState<Chip>()
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)
}
}
Loading