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
ec43d7652b
commit
d67aee23c3
7 changed files with 75 additions and 41 deletions
|
|
@ -10,6 +10,5 @@ package io.element.android.features.home.impl.search
|
||||||
|
|
||||||
sealed interface RoomListSearchEvents {
|
sealed interface RoomListSearchEvents {
|
||||||
data object ToggleSearchVisibility : RoomListSearchEvents
|
data object ToggleSearchVisibility : RoomListSearchEvents
|
||||||
data class QueryChanged(val query: String) : RoomListSearchEvents
|
|
||||||
data object ClearQuery : RoomListSearchEvents
|
data object ClearQuery : RoomListSearchEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
|
|
||||||
package io.element.android.features.home.impl.search
|
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.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
|
@ -29,29 +31,24 @@ class RoomListSearchPresenter(
|
||||||
var isSearchActive by remember {
|
var isSearchActive by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
var searchQuery by remember {
|
val searchQuery = rememberTextFieldState()
|
||||||
mutableStateOf("")
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(isSearchActive) {
|
LaunchedEffect(isSearchActive) {
|
||||||
dataSource.setIsActive(isSearchActive)
|
dataSource.setIsActive(isSearchActive)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(searchQuery) {
|
LaunchedEffect(searchQuery.text) {
|
||||||
dataSource.setSearchQuery(searchQuery)
|
dataSource.setSearchQuery(searchQuery.text.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleEvent(event: RoomListSearchEvents) {
|
fun handleEvent(event: RoomListSearchEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
RoomListSearchEvents.ClearQuery -> {
|
RoomListSearchEvents.ClearQuery -> {
|
||||||
searchQuery = ""
|
searchQuery.clearText()
|
||||||
}
|
|
||||||
is RoomListSearchEvents.QueryChanged -> {
|
|
||||||
searchQuery = event.query
|
|
||||||
}
|
}
|
||||||
RoomListSearchEvents.ToggleSearchVisibility -> {
|
RoomListSearchEvents.ToggleSearchVisibility -> {
|
||||||
isSearchActive = !isSearchActive
|
isSearchActive = !isSearchActive
|
||||||
searchQuery = ""
|
searchQuery.clearText()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,13 @@
|
||||||
|
|
||||||
package io.element.android.features.home.impl.search
|
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 io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class RoomListSearchState(
|
data class RoomListSearchState(
|
||||||
val isSearchActive: Boolean,
|
val isSearchActive: Boolean,
|
||||||
val query: String,
|
val query: TextFieldState,
|
||||||
val results: ImmutableList<RoomListRoomSummary>,
|
val results: ImmutableList<RoomListRoomSummary>,
|
||||||
val eventSink: (RoomListSearchEvents) -> Unit
|
val eventSink: (RoomListSearchEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
package io.element.android.features.home.impl.search
|
package io.element.android.features.home.impl.search
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
import io.element.android.features.home.impl.model.RoomListRoomSummary
|
||||||
import io.element.android.features.home.impl.roomlist.aRoomListRoomSummaryList
|
import io.element.android.features.home.impl.roomlist.aRoomListRoomSummaryList
|
||||||
|
|
@ -33,7 +34,7 @@ fun aRoomListSearchState(
|
||||||
eventSink: (RoomListSearchEvents) -> Unit = { },
|
eventSink: (RoomListSearchEvents) -> Unit = { },
|
||||||
) = RoomListSearchState(
|
) = RoomListSearchState(
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
query = query,
|
query = TextFieldState(initialText = query),
|
||||||
results = results,
|
results = results,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -18,16 +18,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
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.Modifier
|
||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
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.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
|
|
@ -112,23 +108,14 @@ private fun RoomListSearchContent(
|
||||||
},
|
},
|
||||||
navigationIcon = { BackButton(onClick = ::onBackButtonClick) },
|
navigationIcon = { BackButton(onClick = ::onBackButtonClick) },
|
||||||
title = {
|
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
|
// 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() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
FilledTextField(
|
FilledTextField(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
value = value,
|
state = state.query,
|
||||||
singleLine = true,
|
lineLimits = TextFieldLineLimits.SingleLine,
|
||||||
onValueChange = {
|
|
||||||
value = it
|
|
||||||
state.eventSink(RoomListSearchEvents.QueryChanged(it.text))
|
|
||||||
},
|
|
||||||
colors = TextFieldDefaults.colors(
|
colors = TextFieldDefaults.colors(
|
||||||
focusedContainerColor = Color.Transparent,
|
focusedContainerColor = Color.Transparent,
|
||||||
unfocusedContainerColor = Color.Transparent,
|
unfocusedContainerColor = Color.Transparent,
|
||||||
|
|
@ -138,20 +125,18 @@ private fun RoomListSearchContent(
|
||||||
disabledIndicatorColor = Color.Transparent,
|
disabledIndicatorColor = Color.Transparent,
|
||||||
errorIndicatorColor = Color.Transparent,
|
errorIndicatorColor = Color.Transparent,
|
||||||
),
|
),
|
||||||
trailingIcon = {
|
trailingIcon = if (state.query.text.isNotEmpty()) {
|
||||||
if (value.text.isNotEmpty()) {
|
@Composable {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = { state.eventSink(RoomListSearchEvents.ClearQuery) }) {
|
||||||
state.eventSink(RoomListSearchEvents.ClearQuery)
|
|
||||||
// Clear local state too
|
|
||||||
value = value.copy(text = "")
|
|
||||||
}) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.Close(),
|
imageVector = CompoundIcons.Close(),
|
||||||
contentDescription = stringResource(CommonStrings.action_cancel)
|
contentDescription = stringResource(CommonStrings.action_cancel)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ class RoomListSearchPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.isSearchActive).isFalse()
|
assertThat(state.isSearchActive).isFalse()
|
||||||
assertThat(state.query).isEmpty()
|
assertThat(state.query.text.toString()).isEmpty()
|
||||||
assertThat(state.results).isEmpty()
|
assertThat(state.results).isEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,10 +72,10 @@ class RoomListSearchPresenterTest {
|
||||||
).isEqualTo(
|
).isEqualTo(
|
||||||
RoomListFilter.None
|
RoomListFilter.None
|
||||||
)
|
)
|
||||||
state.eventSink(RoomListSearchEvents.QueryChanged("Search"))
|
state.query.edit { append("Search") }
|
||||||
}
|
}
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.query).isEqualTo("Search")
|
assertThat(state.query.text).isEqualTo("Search")
|
||||||
assertThat(
|
assertThat(
|
||||||
roomListService.allRooms.currentFilter.value
|
roomListService.allRooms.currentFilter.value
|
||||||
).isEqualTo(
|
).isEqualTo(
|
||||||
|
|
@ -84,7 +84,7 @@ class RoomListSearchPresenterTest {
|
||||||
state.eventSink(RoomListSearchEvents.ClearQuery)
|
state.eventSink(RoomListSearchEvents.ClearQuery)
|
||||||
}
|
}
|
||||||
awaitItem().let { state ->
|
awaitItem().let { state ->
|
||||||
assertThat(state.query).isEmpty()
|
assertThat(state.query.text.toString()).isEmpty()
|
||||||
assertThat(
|
assertThat(
|
||||||
roomListService.allRooms.currentFilter.value
|
roomListService.allRooms.currentFilter.value
|
||||||
).isEqualTo(
|
).isEqualTo(
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,15 @@ import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
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.LocalTextStyle
|
||||||
import androidx.compose.material3.TextFieldColors
|
import androidx.compose.material3.TextFieldColors
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.material3.TextFieldLabelScope
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
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)
|
@Preview(group = PreviewGroup.TextFields)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun FilledTextFieldLightPreview() =
|
internal fun FilledTextFieldLightPreview() =
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue