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:
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@ class KonsistPreviewTest {
|
||||
"TextComposerSimpleNotEncryptedPreview",
|
||||
"TextComposerVoicePreview",
|
||||
"TextComposerVoiceNotEncryptedPreview",
|
||||
"TextFieldDialogWithBorderPreview",
|
||||
"TextFieldDialogWithErrorPreview",
|
||||
"TimelineImageWithCaptionRowPreview",
|
||||
"TimelineItemEventRowForDirectRoomPreview",
|
||||
"TimelineItemEventRowShieldPreview",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user