Use TextFieldState for room list search (#5975)
* Add new `FilledTextField` variant using `TextFieldState` * Use `TextFieldState` for `RoomListSearchState.query` - it seems like this is the best practice for this kind of data * Bonus: fix the clear button being misaligned
This commit is contained in:
parent
8a290c339d
commit
ce85ed16f6
7 changed files with 75 additions and 41 deletions
|
|
@ -10,6 +10,5 @@ package io.element.android.features.home.impl.search
|
|||
|
||||
sealed interface RoomListSearchEvents {
|
||||
data object ToggleSearchVisibility : RoomListSearchEvents
|
||||
data class QueryChanged(val query: String) : RoomListSearchEvents
|
||||
data object ClearQuery : RoomListSearchEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
package io.element.android.features.home.impl.search
|
||||
|
||||
import androidx.compose.foundation.text.input.clearText
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
|
|
@ -29,29 +31,24 @@ class RoomListSearchPresenter(
|
|||
var isSearchActive by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var searchQuery by remember {
|
||||
mutableStateOf("")
|
||||
}
|
||||
val searchQuery = rememberTextFieldState()
|
||||
|
||||
LaunchedEffect(isSearchActive) {
|
||||
dataSource.setIsActive(isSearchActive)
|
||||
}
|
||||
|
||||
LaunchedEffect(searchQuery) {
|
||||
dataSource.setSearchQuery(searchQuery)
|
||||
LaunchedEffect(searchQuery.text) {
|
||||
dataSource.setSearchQuery(searchQuery.text.toString())
|
||||
}
|
||||
|
||||
fun handleEvent(event: RoomListSearchEvents) {
|
||||
when (event) {
|
||||
RoomListSearchEvents.ClearQuery -> {
|
||||
searchQuery = ""
|
||||
}
|
||||
is RoomListSearchEvents.QueryChanged -> {
|
||||
searchQuery = event.query
|
||||
searchQuery.clearText()
|
||||
}
|
||||
RoomListSearchEvents.ToggleSearchVisibility -> {
|
||||
isSearchActive = !isSearchActive
|
||||
searchQuery = ""
|
||||
searchQuery.clearText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,13 @@
|
|||
|
||||
package io.element.android.features.home.impl.search
|
||||
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class RoomListSearchState(
|
||||
val isSearchActive: Boolean,
|
||||
val query: String,
|
||||
val query: TextFieldState,
|
||||
val results: ImmutableList<RoomListRoomSummary>,
|
||||
val eventSink: (RoomListSearchEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
package io.element.android.features.home.impl.search
|
||||
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.home.impl.roomlist.aRoomListRoomSummaryList
|
||||
|
|
@ -33,7 +34,7 @@ fun aRoomListSearchState(
|
|||
eventSink: (RoomListSearchEvents) -> Unit = { },
|
||||
) = RoomListSearchState(
|
||||
isSearchActive = isSearchActive,
|
||||
query = query,
|
||||
query = TextFieldState(initialText = query),
|
||||
results = results,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,16 +18,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
|
|
@ -35,7 +32,6 @@ import androidx.compose.ui.focus.focusRequester
|
|||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
|
|
@ -112,23 +108,14 @@ private fun RoomListSearchContent(
|
|||
},
|
||||
navigationIcon = { BackButton(onClick = ::onBackButtonClick) },
|
||||
title = {
|
||||
// TODO replace `state.query` with TextFieldState when it's available for M3 TextField
|
||||
// The stateSaver will keep the selection state when returning to this UI
|
||||
var value by rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(state.query))
|
||||
}
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
FilledTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
value = value,
|
||||
singleLine = true,
|
||||
onValueChange = {
|
||||
value = it
|
||||
state.eventSink(RoomListSearchEvents.QueryChanged(it.text))
|
||||
},
|
||||
state = state.query,
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
|
|
@ -138,20 +125,18 @@ private fun RoomListSearchContent(
|
|||
disabledIndicatorColor = Color.Transparent,
|
||||
errorIndicatorColor = Color.Transparent,
|
||||
),
|
||||
trailingIcon = {
|
||||
if (value.text.isNotEmpty()) {
|
||||
IconButton(onClick = {
|
||||
state.eventSink(RoomListSearchEvents.ClearQuery)
|
||||
// Clear local state too
|
||||
value = value.copy(text = "")
|
||||
}) {
|
||||
trailingIcon = if (state.query.text.isNotEmpty()) {
|
||||
@Composable {
|
||||
IconButton(onClick = { state.eventSink(RoomListSearchEvents.ClearQuery) }) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Close(),
|
||||
contentDescription = stringResource(CommonStrings.action_cancel)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class RoomListSearchPresenterTest {
|
|||
}.test {
|
||||
awaitItem().let { state ->
|
||||
assertThat(state.isSearchActive).isFalse()
|
||||
assertThat(state.query).isEmpty()
|
||||
assertThat(state.query.text.toString()).isEmpty()
|
||||
assertThat(state.results).isEmpty()
|
||||
}
|
||||
}
|
||||
|
|
@ -72,10 +72,10 @@ class RoomListSearchPresenterTest {
|
|||
).isEqualTo(
|
||||
RoomListFilter.None
|
||||
)
|
||||
state.eventSink(RoomListSearchEvents.QueryChanged("Search"))
|
||||
state.query.edit { append("Search") }
|
||||
}
|
||||
awaitItem().let { state ->
|
||||
assertThat(state.query).isEqualTo("Search")
|
||||
assertThat(state.query.text).isEqualTo("Search")
|
||||
assertThat(
|
||||
roomListService.allRooms.currentFilter.value
|
||||
).isEqualTo(
|
||||
|
|
@ -84,7 +84,7 @@ class RoomListSearchPresenterTest {
|
|||
state.eventSink(RoomListSearchEvents.ClearQuery)
|
||||
}
|
||||
awaitItem().let { state ->
|
||||
assertThat(state.query).isEmpty()
|
||||
assertThat(state.query.text.toString()).isEmpty()
|
||||
assertThat(
|
||||
roomListService.allRooms.currentFilter.value
|
||||
).isEqualTo(
|
||||
|
|
|
|||
|
|
@ -15,9 +15,15 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.input.InputTransformation
|
||||
import androidx.compose.foundation.text.input.KeyboardActionHandler
|
||||
import androidx.compose.foundation.text.input.OutputTransformation
|
||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.TextFieldColors
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TextFieldLabelScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -135,6 +141,51 @@ fun FilledTextField(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FilledTextField(
|
||||
state: TextFieldState,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
textStyle: TextStyle = LocalTextStyle.current,
|
||||
label: @Composable (TextFieldLabelScope.() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
supportingText: @Composable (() -> Unit)? = null,
|
||||
isError: Boolean = false,
|
||||
inputTransformation: InputTransformation? = null,
|
||||
outputTransformation: OutputTransformation? = null,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActionHandler? = null,
|
||||
lineLimits: TextFieldLineLimits = TextFieldLineLimits.Default,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
shape: Shape = TextFieldDefaults.shape,
|
||||
colors: TextFieldColors = TextFieldDefaults.colors()
|
||||
) {
|
||||
androidx.compose.material3.TextField(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
textStyle = textStyle,
|
||||
label = label,
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
supportingText = supportingText,
|
||||
isError = isError,
|
||||
inputTransformation = inputTransformation,
|
||||
outputTransformation = outputTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
onKeyboardAction = keyboardActions,
|
||||
lineLimits = lineLimits,
|
||||
interactionSource = interactionSource,
|
||||
shape = shape,
|
||||
colors = colors,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(group = PreviewGroup.TextFields)
|
||||
@Composable
|
||||
internal fun FilledTextFieldLightPreview() =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue