Improve TextFieldDialog (#4512)

* Extract TextFieldDialog to its own file (no other change).

* Add TextFieldDialogPreview

Enhance TextFieldDialog

* Let RoomMembersModerationView use TextFieldDialog

* Update screenshots

* Konsist.

* Add modifier parameter.

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Benoit Marty 2025-04-02 10:05:56 +02:00 committed by GitHub
parent 5c2a069c95
commit 1fdb590ece
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 291 additions and 264 deletions

View file

@ -1,14 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.roomdetails.impl.members.moderation
import io.element.android.libraries.architecture.AsyncAction
data class ConfirmingWithReason(
val reason: String,
) : AsyncAction.Confirming

View file

@ -12,8 +12,10 @@ import io.element.android.libraries.matrix.api.room.RoomMember
sealed interface RoomMembersModerationEvents {
data class SelectRoomMember(val roomMember: RoomMember) : RoomMembersModerationEvents
data class KickUser(val reason: String, val needsConfirmation: Boolean) : RoomMembersModerationEvents
data class BanUser(val reason: String, val needsConfirmation: Boolean) : RoomMembersModerationEvents
data object KickUser : RoomMembersModerationEvents
data class DoKickUser(val reason: String) : RoomMembersModerationEvents
data object BanUser : RoomMembersModerationEvents
data class DoBanUser(val reason: String) : RoomMembersModerationEvents
data class UnbanUser(val userId: UserId) : RoomMembersModerationEvents
data object Reset : RoomMembersModerationEvents
}

View file

@ -96,24 +96,22 @@ class RoomMembersModerationPresenter @Inject constructor(
}
}
is RoomMembersModerationEvents.KickUser -> {
if (event.needsConfirmation) {
kickUserAsyncAction.value = ConfirmingWithReason(event.reason)
} else {
selectedMember?.let {
coroutineScope.kickUser(it.userId, event.reason, kickUserAsyncAction)
}
selectedMember = null
kickUserAsyncAction.value = AsyncAction.ConfirmingNoParams
}
is RoomMembersModerationEvents.DoKickUser -> {
selectedMember?.let {
coroutineScope.kickUser(it.userId, event.reason, kickUserAsyncAction)
}
selectedMember = null
}
is RoomMembersModerationEvents.BanUser -> {
if (event.needsConfirmation) {
banUserAsyncAction.value = ConfirmingWithReason(event.reason)
} else {
selectedMember?.let {
coroutineScope.banUser(it.userId, event.reason, banUserAsyncAction)
}
selectedMember = null
banUserAsyncAction.value = AsyncAction.ConfirmingNoParams
}
is RoomMembersModerationEvents.DoBanUser -> {
selectedMember?.let {
coroutineScope.banUser(it.userId, event.reason, banUserAsyncAction)
}
selectedMember = null
}
is RoomMembersModerationEvents.UnbanUser -> {
// We are already confirming when we are reaching this point

View file

@ -39,11 +39,7 @@ class RoomMembersModerationStateProvider : PreviewParameterProvider<RoomMembersM
),
aRoomMembersModerationState(
selectedRoomMember = anAlice(),
kickUserAsyncAction = ConfirmingWithReason(""),
),
aRoomMembersModerationState(
selectedRoomMember = anAlice(),
kickUserAsyncAction = ConfirmingWithReason("A reason"),
kickUserAsyncAction = AsyncAction.ConfirmingNoParams,
),
aRoomMembersModerationState(
selectedRoomMember = anAlice(),
@ -51,11 +47,7 @@ class RoomMembersModerationStateProvider : PreviewParameterProvider<RoomMembersM
),
aRoomMembersModerationState(
selectedRoomMember = anAlice(),
banUserAsyncAction = ConfirmingWithReason(""),
),
aRoomMembersModerationState(
selectedRoomMember = anAlice(),
banUserAsyncAction = ConfirmingWithReason("A reason"),
banUserAsyncAction = AsyncAction.ConfirmingNoParams,
),
aRoomMembersModerationState(
selectedRoomMember = anAlice(),

View file

@ -38,9 +38,8 @@ import io.element.android.libraries.designsystem.components.async.rememberAsyncI
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.ListDialog
import io.element.android.libraries.designsystem.components.dialogs.TextFieldDialog
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.list.TextFieldListItem
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.IconSource
@ -74,10 +73,10 @@ fun RoomMembersModerationView(
onDisplayMemberProfile(action.userId)
}
is ModerationAction.KickUser -> {
state.eventSink(RoomMembersModerationEvents.KickUser(reason = "", needsConfirmation = true))
state.eventSink(RoomMembersModerationEvents.KickUser)
}
is ModerationAction.BanUser -> {
state.eventSink(RoomMembersModerationEvents.BanUser(reason = "", needsConfirmation = true))
state.eventSink(RoomMembersModerationEvents.BanUser)
}
}
},
@ -90,45 +89,19 @@ fun RoomMembersModerationView(
when (val action = state.kickUserAsyncAction) {
is AsyncAction.Confirming -> {
if (action is ConfirmingWithReason) {
ListDialog(
title = stringResource(R.string.screen_room_member_list_kick_member_confirmation_title),
submitText = stringResource(R.string.screen_room_member_list_kick_member_confirmation_action),
onSubmit = {
state.eventSink(
RoomMembersModerationEvents.KickUser(
reason = action.reason,
needsConfirmation = false,
)
)
},
applyPaddingToContents = true,
onDismissRequest = { state.eventSink(RoomMembersModerationEvents.Reset) },
) {
item {
Text(
text = stringResource(R.string.screen_room_member_list_kick_member_confirmation_description),
style = ElementTheme.materialTypography.bodyMedium,
)
}
item {
TextFieldListItem(
placeholder = stringResource(id = CommonStrings.common_reason),
label = stringResource(id = CommonStrings.common_reason),
withBorder = true,
text = action.reason,
onTextChange = { newText ->
state.eventSink(
RoomMembersModerationEvents.KickUser(
reason = newText,
needsConfirmation = true,
)
)
},
)
}
}
}
TextFieldDialog(
title = stringResource(R.string.screen_room_member_list_kick_member_confirmation_title),
submitText = stringResource(R.string.screen_room_member_list_kick_member_confirmation_action),
onSubmit = { reason ->
state.eventSink(RoomMembersModerationEvents.DoKickUser(reason = reason))
},
onDismissRequest = { state.eventSink(RoomMembersModerationEvents.Reset) },
placeholder = stringResource(id = CommonStrings.common_reason),
label = stringResource(id = CommonStrings.common_reason),
withBorder = true,
content = stringResource(R.string.screen_room_member_list_kick_member_confirmation_description),
value = "",
)
}
is AsyncAction.Loading -> {
LaunchedEffect(action) {
@ -156,45 +129,19 @@ fun RoomMembersModerationView(
when (val action = state.banUserAsyncAction) {
is AsyncAction.Confirming -> {
if (action is ConfirmingWithReason) {
ListDialog(
title = stringResource(R.string.screen_room_member_list_ban_member_confirmation_title),
submitText = stringResource(R.string.screen_room_member_list_ban_member_confirmation_action),
onSubmit = {
state.eventSink(
RoomMembersModerationEvents.BanUser(
reason = action.reason,
needsConfirmation = false,
)
)
},
applyPaddingToContents = true,
onDismissRequest = { state.eventSink(RoomMembersModerationEvents.Reset) },
) {
item {
Text(
text = stringResource(R.string.screen_room_member_list_ban_member_confirmation_description),
style = ElementTheme.materialTypography.bodyMedium,
)
}
item {
TextFieldListItem(
placeholder = stringResource(id = CommonStrings.common_reason),
label = stringResource(id = CommonStrings.common_reason),
withBorder = true,
text = action.reason,
onTextChange = { newText ->
state.eventSink(
RoomMembersModerationEvents.BanUser(
reason = newText,
needsConfirmation = true,
)
)
},
)
}
}
}
TextFieldDialog(
title = stringResource(R.string.screen_room_member_list_ban_member_confirmation_title),
submitText = stringResource(R.string.screen_room_member_list_ban_member_confirmation_action),
onSubmit = { reason ->
state.eventSink(RoomMembersModerationEvents.DoBanUser(reason = reason))
},
onDismissRequest = { state.eventSink(RoomMembersModerationEvents.Reset) },
placeholder = stringResource(id = CommonStrings.common_reason),
label = stringResource(id = CommonStrings.common_reason),
withBorder = true,
content = stringResource(R.string.screen_room_member_list_ban_member_confirmation_description),
value = "",
)
}
is AsyncAction.Loading -> {
LaunchedEffect(action) {

View file

@ -173,15 +173,11 @@ class RoomMembersModerationPresenterTest {
}.test {
skipItems(1)
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
awaitItem().eventSink(RoomMembersModerationEvents.KickUser(reason = "", needsConfirmation = true))
awaitItem().eventSink(RoomMembersModerationEvents.KickUser)
val confirmingState = awaitItem()
assertThat(confirmingState.kickUserAsyncAction).isEqualTo(ConfirmingWithReason(""))
// Change the reason
confirmingState.eventSink(RoomMembersModerationEvents.KickUser(reason = A_REASON, needsConfirmation = true))
val confirmingWithReasonState = awaitItem()
assertThat(confirmingWithReasonState.kickUserAsyncAction).isEqualTo(ConfirmingWithReason(A_REASON))
assertThat(confirmingState.kickUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams)
// Confirm
confirmingState.eventSink(RoomMembersModerationEvents.KickUser(reason = A_REASON, needsConfirmation = false))
confirmingState.eventSink(RoomMembersModerationEvents.DoKickUser(reason = A_REASON))
skipItems(1)
val loadingState = awaitItem()
assertThat(loadingState.actions).isEmpty()
@ -215,15 +211,11 @@ class RoomMembersModerationPresenterTest {
}.test {
skipItems(1)
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(selectedMember))
awaitItem().eventSink(RoomMembersModerationEvents.BanUser(reason = "", needsConfirmation = true))
awaitItem().eventSink(RoomMembersModerationEvents.BanUser)
val confirmingState = awaitItem()
assertThat(confirmingState.banUserAsyncAction).isEqualTo(ConfirmingWithReason(""))
// Change the reason
confirmingState.eventSink(RoomMembersModerationEvents.BanUser(reason = A_REASON, needsConfirmation = true))
val confirmingWithReasonState = awaitItem()
assertThat(confirmingWithReasonState.banUserAsyncAction).isEqualTo(ConfirmingWithReason(A_REASON))
assertThat(confirmingState.banUserAsyncAction).isEqualTo(AsyncAction.ConfirmingNoParams)
// Confirm
confirmingState.eventSink(RoomMembersModerationEvents.BanUser(reason = A_REASON, needsConfirmation = false))
confirmingState.eventSink(RoomMembersModerationEvents.DoBanUser(reason = A_REASON))
skipItems(1)
val loadingItem = awaitItem()
assertThat(loadingItem.actions).isEmpty()
@ -314,7 +306,7 @@ class RoomMembersModerationPresenterTest {
val initialItem = awaitItem()
// Kick user and fail
awaitItem().eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
awaitItem().eventSink(RoomMembersModerationEvents.KickUser(reason = "", needsConfirmation = false))
awaitItem().eventSink(RoomMembersModerationEvents.DoKickUser(reason = ""))
skipItems(1)
assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(awaitItem().kickUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)
@ -324,7 +316,7 @@ class RoomMembersModerationPresenterTest {
// Ban user and fail
initialItem.eventSink(RoomMembersModerationEvents.SelectRoomMember(aVictor()))
awaitItem().eventSink(RoomMembersModerationEvents.BanUser(reason = "", needsConfirmation = false))
awaitItem().eventSink(RoomMembersModerationEvents.DoBanUser(reason = ""))
skipItems(1)
assertThat(awaitItem().banUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(awaitItem().banUserAsyncAction).isInstanceOf(AsyncAction.Failure::class.java)

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.roomdetails.impl.R
import io.element.android.features.roomdetails.impl.members.anAlice
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.test.A_REASON
import io.element.android.libraries.ui.strings.CommonStrings
@ -94,7 +95,7 @@ class RoomMembersModerationViewTest {
rule.clickOn(R.string.screen_room_member_list_manage_member_remove)
// Give time for the bottom sheet to animate
rule.mainClock.advanceTimeBy(1_000)
eventsRecorder.assertSingle(RoomMembersModerationEvents.KickUser(reason = "", needsConfirmation = true))
eventsRecorder.assertSingle(RoomMembersModerationEvents.KickUser)
}
@Test
@ -103,7 +104,7 @@ class RoomMembersModerationViewTest {
val roomMember = anAlice()
val state = aRoomMembersModerationState(
selectedRoomMember = roomMember,
kickUserAsyncAction = ConfirmingWithReason(A_REASON),
kickUserAsyncAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder
)
rule.setRoomMembersModerationView(
@ -115,19 +116,21 @@ class RoomMembersModerationViewTest {
}
@Test
fun `confirming 'Remove member' reason edition emits the expected event`() {
fun `confirming 'Remove member' reason edition then validation emits the expected event`() {
val eventsRecorder = EventsRecorder<RoomMembersModerationEvents>()
val roomMember = anAlice()
val state = aRoomMembersModerationState(
selectedRoomMember = roomMember,
kickUserAsyncAction = ConfirmingWithReason(A_REASON),
kickUserAsyncAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder
)
rule.setRoomMembersModerationView(
state = state,
)
rule.onNodeWithText(A_REASON).performTextInput("z")
eventsRecorder.assertSingle(RoomMembersModerationEvents.KickUser(reason = "z$A_REASON", needsConfirmation = true))
val reason = rule.activity.getString(CommonStrings.common_reason)
rule.onNodeWithText(reason).performTextInput(A_REASON)
rule.clickOn(R.string.screen_room_member_list_kick_member_confirmation_action)
eventsRecorder.assertSingle(RoomMembersModerationEvents.DoKickUser(reason = A_REASON))
}
@Test
@ -136,7 +139,7 @@ class RoomMembersModerationViewTest {
val roomMember = anAlice()
val state = aRoomMembersModerationState(
selectedRoomMember = roomMember,
kickUserAsyncAction = ConfirmingWithReason(A_REASON),
kickUserAsyncAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder
)
rule.setRoomMembersModerationView(
@ -144,7 +147,7 @@ class RoomMembersModerationViewTest {
)
// Note: the string key semantics is not perfect here :/
rule.clickOn(R.string.screen_room_member_list_kick_member_confirmation_action)
eventsRecorder.assertSingle(RoomMembersModerationEvents.KickUser(reason = A_REASON, needsConfirmation = false))
eventsRecorder.assertSingle(RoomMembersModerationEvents.DoKickUser(reason = ""))
}
@Config(qualifiers = "h1024dp")
@ -168,7 +171,7 @@ class RoomMembersModerationViewTest {
rule.clickOn(R.string.screen_room_member_list_manage_member_remove_confirmation_ban)
// Give time for the bottom sheet to animate
rule.mainClock.advanceTimeBy(1_000)
eventsRecorder.assertSingle(RoomMembersModerationEvents.BanUser(reason = "", needsConfirmation = true))
eventsRecorder.assertSingle(RoomMembersModerationEvents.BanUser)
}
@Test
@ -177,7 +180,7 @@ class RoomMembersModerationViewTest {
val roomMember = anAlice()
val state = aRoomMembersModerationState(
selectedRoomMember = roomMember,
banUserAsyncAction = ConfirmingWithReason(A_REASON),
banUserAsyncAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder
)
rule.setRoomMembersModerationView(
@ -194,14 +197,16 @@ class RoomMembersModerationViewTest {
val roomMember = anAlice()
val state = aRoomMembersModerationState(
selectedRoomMember = roomMember,
banUserAsyncAction = ConfirmingWithReason(A_REASON),
banUserAsyncAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder
)
rule.setRoomMembersModerationView(
state = state,
)
rule.onNodeWithText(A_REASON).performTextInput("z")
eventsRecorder.assertSingle(RoomMembersModerationEvents.BanUser(reason = "z$A_REASON", needsConfirmation = true))
val reason = rule.activity.getString(CommonStrings.common_reason)
rule.onNodeWithText(reason).performTextInput(A_REASON)
rule.clickOn(R.string.screen_room_member_list_ban_member_confirmation_action)
eventsRecorder.assertSingle(RoomMembersModerationEvents.DoBanUser(reason = A_REASON))
}
@Test
@ -210,7 +215,7 @@ class RoomMembersModerationViewTest {
val roomMember = anAlice()
val state = aRoomMembersModerationState(
selectedRoomMember = roomMember,
banUserAsyncAction = ConfirmingWithReason(A_REASON),
banUserAsyncAction = AsyncAction.ConfirmingNoParams,
eventSink = eventsRecorder
)
rule.setRoomMembersModerationView(
@ -218,7 +223,7 @@ class RoomMembersModerationViewTest {
)
// Note: the string key semantics is not perfect here :/
rule.clickOn(R.string.screen_room_member_list_ban_member_confirmation_action)
eventsRecorder.assertSingle(RoomMembersModerationEvents.BanUser(reason = A_REASON, needsConfirmation = false))
eventsRecorder.assertSingle(RoomMembersModerationEvents.DoBanUser(reason = ""))
}
@Test

View file

@ -0,0 +1,154 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.designsystem.components.dialogs
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
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.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.components.list.TextFieldListItem
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun TextFieldDialog(
title: String,
onSubmit: (String) -> Unit,
onDismissRequest: () -> Unit,
value: String?,
placeholder: String?,
modifier: Modifier = Modifier,
validation: (String?) -> Boolean = { true },
onValidationErrorMessage: String? = null,
autoSelectOnDisplay: Boolean = true,
maxLines: Int = 1,
content: String? = null,
label: String? = null,
withBorder: Boolean = false,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
submitText: String = stringResource(CommonStrings.action_ok),
) {
val focusRequester = remember { FocusRequester() }
var textFieldContents by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(
TextFieldValue(
value.orEmpty(),
selection = TextRange(value.orEmpty().length)
)
)
}
var error by rememberSaveable { mutableStateOf(if (!validation(value.orEmpty())) onValidationErrorMessage else null) }
var canRequestFocus by rememberSaveable { mutableStateOf(false) }
val canSubmit by remember { derivedStateOf { validation(textFieldContents.text) } }
ListDialog(
title = title,
onSubmit = { onSubmit(textFieldContents.text) },
onDismissRequest = onDismissRequest,
enabled = canSubmit,
applyPaddingToContents = content.isNullOrEmpty().not(),
submitText = submitText,
modifier = modifier,
) {
if (content != null) {
item {
Text(
text = content,
style = ElementTheme.materialTypography.bodyMedium,
)
}
}
item {
TextFieldListItem(
placeholder = placeholder.orEmpty(),
label = label,
withBorder = withBorder,
text = textFieldContents,
onTextChange = {
error = if (!validation(it.text)) onValidationErrorMessage else null
textFieldContents = it
},
error = error,
keyboardOptions = keyboardOptions,
keyboardActions = KeyboardActions(onAny = {
if (validation(textFieldContents.text)) {
onSubmit(textFieldContents.text)
}
}),
maxLines = maxLines,
modifier = Modifier.focusRequester(focusRequester),
)
canRequestFocus = true
}
}
if (autoSelectOnDisplay && canRequestFocus) {
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
}
@PreviewsDayNight
@Composable
internal fun TextFieldDialogPreview() = ElementPreview {
TextFieldDialog(
title = "Title",
value = "",
placeholder = "Placeholder",
onSubmit = {},
onDismissRequest = {},
)
}
@PreviewsDayNight
@Composable
internal fun TextFieldDialogWithBorderPreview() = ElementPreview {
TextFieldDialog(
title = "Title",
content = "Some content",
onSubmit = {},
onDismissRequest = {},
value = "Value",
placeholder = "Placeholder",
label = "Label",
withBorder = true,
onValidationErrorMessage = "Error message",
)
}
@PreviewsDayNight
@Composable
internal fun TextFieldDialogWithErrorPreview() = ElementPreview {
TextFieldDialog(
title = "Title",
content = "Some content",
onSubmit = {},
validation = { false },
onDismissRequest = {},
value = "Value",
placeholder = "Placeholder",
label = "Label",
withBorder = true,
onValidationErrorMessage = "Error message",
)
}

View file

@ -70,6 +70,8 @@ fun TextFieldListItem(
modifier: Modifier = Modifier,
error: String? = null,
maxLines: Int = 1,
withBorder: Boolean = false,
label: String? = null,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
) {
@ -79,12 +81,17 @@ fun TextFieldListItem(
value = text,
onValueChange = { onTextChange(it) },
placeholder = placeholder?.let { @Composable { Text(it) } },
colors = OutlinedTextFieldDefaults.colors(
disabledBorderColor = Color.Transparent,
errorBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
),
label = label?.let { @Composable { Text(it) } },
colors = if (withBorder) {
OutlinedTextFieldDefaults.colors()
} else {
OutlinedTextFieldDefaults.colors(
disabledBorderColor = Color.Transparent,
errorBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
)
},
isError = error != null,
supportingText = error?.let { @Composable { Text(it) } },
keyboardOptions = keyboardOptions,

View file

@ -7,24 +7,15 @@
package io.element.android.libraries.designsystem.components.preferences
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
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.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import io.element.android.libraries.designsystem.components.dialogs.ListDialog
import io.element.android.libraries.designsystem.components.dialogs.TextFieldDialog
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.list.TextFieldListItem
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
import io.element.android.libraries.designsystem.theme.components.Text
@ -74,58 +65,3 @@ fun PreferenceTextField(
)
}
}
@Composable
private fun TextFieldDialog(
title: String,
onSubmit: (String) -> Unit,
onDismissRequest: () -> Unit,
value: String?,
placeholder: String?,
validation: (String?) -> Boolean = { true },
onValidationErrorMessage: String? = null,
autoSelectOnDisplay: Boolean = true,
maxLines: Int = 1,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
) {
val focusRequester = remember { FocusRequester() }
var textFieldContents by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(value.orEmpty(), selection = TextRange(value.orEmpty().length)))
}
var error by rememberSaveable { mutableStateOf<String?>(null) }
var canRequestFocus by rememberSaveable { mutableStateOf(false) }
val canSubmit by remember { derivedStateOf { validation(textFieldContents.text) } }
ListDialog(
title = title,
onSubmit = { onSubmit(textFieldContents.text) },
onDismissRequest = onDismissRequest,
enabled = canSubmit,
) {
item {
TextFieldListItem(
placeholder = placeholder.orEmpty(),
text = textFieldContents,
onTextChange = {
error = if (!validation(it.text)) onValidationErrorMessage else null
textFieldContents = it
},
error = error,
keyboardOptions = keyboardOptions,
keyboardActions = KeyboardActions(onAny = {
if (validation(textFieldContents.text)) {
onSubmit(textFieldContents.text)
}
}),
maxLines = maxLines,
modifier = Modifier.focusRequester(focusRequester),
)
canRequestFocus = true
}
}
if (autoSelectOnDisplay && canRequestFocus) {
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
}

View file

@ -120,6 +120,8 @@ class KonsistPreviewTest {
"TextComposerSimpleNotEncryptedPreview",
"TextComposerVoicePreview",
"TextComposerVoiceNotEncryptedPreview",
"TextFieldDialogWithBorderPreview",
"TextFieldDialogWithErrorPreview",
"TimelineImageWithCaptionRowPreview",
"TimelineItemEventRowForDirectRoomPreview",
"TimelineItemEventRowShieldPreview",

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f83d60c7e1d963ccef24668c219bd9df36494dc8bd8f57af7aa63a3b73ee6882
size 7545
oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650
size 3642

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b323796ccdf27e5c445aa02e3f7b73f65df961dd88f3ee480efcba14d8038281
size 19426

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650
size 3642

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:501ebaae1e09a4f7853b574b788ddb3a73cd470d37d5dbef1f46b2967761e612
size 31207
oid sha256:2d06a58bb3b3fe7b01740cc04ed3128d24a13717be84cb4a70ced228d2256b41
size 9435

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2d06a58bb3b3fe7b01740cc04ed3128d24a13717be84cb4a70ced228d2256b41
size 9435
oid sha256:39b34e0940f38ab0d66ba68b61c07ba70a51acfb54f995eedce800231ea6a2c5
size 28257

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:39b34e0940f38ab0d66ba68b61c07ba70a51acfb54f995eedce800231ea6a2c5
size 28257
oid sha256:571207325446195a58251e7ece1ecaabc8a782cbb526315abe2a82f21e6441eb
size 9006

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:96e0f2e3c32eedf2c85e243661a7947d28cbfe2eee7df81b239c5331770e7f7c
size 29835
oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650
size 3642

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:571207325446195a58251e7ece1ecaabc8a782cbb526315abe2a82f21e6441eb
size 9006
oid sha256:f83d60c7e1d963ccef24668c219bd9df36494dc8bd8f57af7aa63a3b73ee6882
size 7545

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650
size 3642
oid sha256:b323796ccdf27e5c445aa02e3f7b73f65df961dd88f3ee480efcba14d8038281
size 19426

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ba213b12d181b04695b9271670cb7d96ce83dfccc3c2031ee002ba1cf180cc8d
size 6408
oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd
size 3659

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f9def02bcb5784681dc26327791510a9a25f0772644cdae363d7a4b90e69067c
size 17479

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd
size 3659

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:83e72d7332d7574dd2f4ea42b9fa18995359720b18105f08bfde1f7f14cf8725
size 29080
oid sha256:273043114950e000953fbede780f3ad8adcce9d3d579edea2cad58f8e3c48881
size 8175

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:273043114950e000953fbede780f3ad8adcce9d3d579edea2cad58f8e3c48881
size 8175
oid sha256:19483b4fae4026098273e6e4c941592074f08b3e7f9fa44e759bdab9daf2b27a
size 26233

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:19483b4fae4026098273e6e4c941592074f08b3e7f9fa44e759bdab9daf2b27a
size 26233
oid sha256:2b0d51e2c5b255fb2f94959734293d4b7807f002acb0217c84856f0002297293
size 7636

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0c36a1bfdfdf92ac180436b8c4aef3d017911119f10f02e2a940b5ab242ac324
size 27856
oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd
size 3659

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2b0d51e2c5b255fb2f94959734293d4b7807f002acb0217c84856f0002297293
size 7636
oid sha256:ba213b12d181b04695b9271670cb7d96ce83dfccc3c2031ee002ba1cf180cc8d
size 6408

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd
size 3659
oid sha256:f9def02bcb5784681dc26327791510a9a25f0772644cdae363d7a4b90e69067c
size 17479

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1be0efd7f33ee57088b04aedf6c63cb8092abc0356a31636fe028e31d542e28d
size 15671

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e7609a1d51c1be29b0d117dd968bd44b250d02e82e1630fa062da4cd280bb950
size 13934

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c4697eda8d2fb160b2cade0be583148ec0b2e55f35fe44929f98feb4c8535377
size 17761

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2774d56a1e788567c19df9a714f1399968e75d2df6c3a4f404e3c03a30286567
size 15951

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3257be6487ff2f412564bb8f188860e39b0325156533ad519c9d6852000a173a
size 13119

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:baf396f36cdc0ca5e01842f70fe0b93166fa82c11ad981c54a529f59c90b5111
size 11469