diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchEvents.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchEvents.kt index 20222c144d..d8269fbc04 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchEvents.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchEvents.kt @@ -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 } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenter.kt index ad06b12f5e..49047ffaed 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenter.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenter.kt @@ -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() } } } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchState.kt index 92e8c1ff03..c2d889388b 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchState.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchState.kt @@ -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, val eventSink: (RoomListSearchEvents) -> Unit ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchStateProvider.kt index a5015a5003..645eb791ba 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchStateProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchStateProvider.kt @@ -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, ) diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt index 58d6ba7e00..f013b602dd 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt @@ -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) { diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenterTest.kt index dee0601915..0fd6459057 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenterTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/RoomListSearchPresenterTest.kt @@ -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( diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/FilledTextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/FilledTextField.kt index cbd25c1aeb..d9f4256687 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/FilledTextField.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/FilledTextField.kt @@ -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() =