diff --git a/.cla-accepted b/.cla-accepted index 722b0d023debe2a17be57a7b4e81d012356b0b41..1669034493f181e528e1f3c688f4f3cfc307ad0d 100644 --- a/.cla-accepted +++ b/.cla-accepted @@ -4,3 +4,4 @@ @akwizgran (Michael Rogers) @paul-lorenc (Paul Lorenc) @ialokim (Mikolai Gütschow) +@altynbek.nurtaza (Altynbek Nurtaza) diff --git a/src/main/kotlin/androidx/compose/material/OutlinedTextFieldExt.kt b/src/main/kotlin/androidx/compose/material/OutlinedTextFieldExt.kt index e5ad6c096ac7f0536da60f4f4068a38363d219f5..d2defb118f2a555ff0f74e30b06fb92be6f03e3f 100644 --- a/src/main/kotlin/androidx/compose/material/OutlinedTextFieldExt.kt +++ b/src/main/kotlin/androidx/compose/material/OutlinedTextFieldExt.kt @@ -27,6 +27,9 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.InitialFocusState.AFTER_FIRST_FOCUSSED import androidx.compose.material.InitialFocusState.AFTER_FOCUS_LOST_ONCE import androidx.compose.material.InitialFocusState.FROM_START +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -43,9 +46,11 @@ import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n /** * Material Design outlined text field with extended support for error and helper messages, @@ -142,3 +147,101 @@ fun OutlinedTextField( } enum class InitialFocusState { FROM_START, AFTER_FIRST_FOCUSSED, AFTER_FOCUS_LOST_ONCE } + +/** + * Material Design outlined text field with support for error and helper messages, + * as well as for handling the Enter key. The difference with [OutlinedTextField] is + * that it shows the visibility icons instead of error icons in the error state. + * All parameters not specified here are the same as on the original [OutlinedTextField]. + * + * @param visualTransformation Visual transformation is only applied when password is hidden + */ +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun OutlinedPasswordTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + onEnter: () -> Unit = {}, + enabled: Boolean = true, + readOnly: Boolean = false, + textStyle: TextStyle = LocalTextStyle.current, + label: @Composable (() -> Unit)? = null, + placeholder: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + helperMessage: String? = null, + errorMessage: String? = null, + isError: Boolean = false, + showErrorWhen: InitialFocusState = FROM_START, + visualTransformation: VisualTransformation = PasswordVisualTransformation(), + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + singleLine: Boolean = false, + maxLines: Int = Int.MAX_VALUE, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + shape: Shape = MaterialTheme.shapes.small, + colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors() +) { + var isPasswordVisible by remember { mutableStateOf(false) } + + OutlinedTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier, + onEnter = onEnter, + enabled = enabled, + readOnly = readOnly, + textStyle = textStyle, + label = label, + placeholder = placeholder, + leadingIcon = leadingIcon, + trailingIcon = { + ShowHidePasswordIcon( + isVisible = isPasswordVisible, + toggleIsVisible = { + isPasswordVisible = !isPasswordVisible + }, + ) + }, + errorIcon = { + ShowHidePasswordIcon( + isVisible = isPasswordVisible, + toggleIsVisible = { + isPasswordVisible = !isPasswordVisible + }, + ) + }, + helperMessage = helperMessage, + errorMessage = errorMessage, + isError = isError, + showErrorWhen = showErrorWhen, + visualTransformation = if (!isPasswordVisible) visualTransformation else VisualTransformation.None, + keyboardOptions = keyboardOptions, + singleLine = singleLine, + maxLines = maxLines, + interactionSource = interactionSource, + shape = shape, + colors = colors, + ) +} + +@Composable +private fun ShowHidePasswordIcon( + isVisible: Boolean, + toggleIsVisible: () -> Unit, +) { + IconButton( + onClick = toggleIsVisible + ) { + if (isVisible) { + Icon( + imageVector = Icons.Filled.VisibilityOff, + contentDescription = i18n("access.password.show"), + ) + } else { + Icon( + imageVector = Icons.Filled.Visibility, + contentDescription = i18n("access.password.hide"), + ) + } + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt index f1413a50ad4354c9688b4dff5fadecda576ff912..abdb0fd68a26b29a8da868df8622c48d646ee194 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt @@ -26,7 +26,7 @@ import androidx.compose.material.ButtonType.DESTRUCTIVE import androidx.compose.material.ButtonType.NEUTRAL import androidx.compose.material.DialogButton import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.OutlinedTextField +import androidx.compose.material.OutlinedPasswordTextField import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.Composable @@ -38,7 +38,6 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import org.briarproject.briar.desktop.login.LoginSubViewModel.State.COMPACTING import org.briarproject.briar.desktop.login.LoginSubViewModel.State.MIGRATING @@ -110,14 +109,13 @@ fun LoginForm( val initialFocusRequester = remember { FocusRequester() } val passwordForgotten = remember { mutableStateOf(false) } - OutlinedTextField( + OutlinedPasswordTextField( value = password, onValueChange = setPassword, label = { Text(i18n("startup.field.password")) }, singleLine = true, isError = passwordInvalidError, errorMessage = i18n("startup.error.password_wrong"), - visualTransformation = PasswordVisualTransformation(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Next), modifier = Modifier.fillMaxWidth().focusRequester(initialFocusRequester), onEnter = onEnter diff --git a/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationScreen.kt b/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationScreen.kt index d223cb9b17996e26bb25c14dc292d13f2a517c9b..0397637debd567e2987a3f37a0dbf2790fffcfba 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationScreen.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.InitialFocusState.AFTER_FIRST_FOCUSSED import androidx.compose.material.InitialFocusState.AFTER_FOCUS_LOST_ONCE +import androidx.compose.material.OutlinedPasswordTextField import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -37,7 +38,6 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import org.briarproject.briar.desktop.login.RegistrationSubViewModel.State.CREATED import org.briarproject.briar.desktop.login.RegistrationSubViewModel.State.CREATING @@ -140,7 +140,7 @@ fun PasswordForm( if (password.isNotEmpty()) StrengthMeter(passwordStrength, Modifier.fillMaxWidth()) } - OutlinedTextField( + OutlinedPasswordTextField( value = password, onValueChange = setPassword, label = { Text(i18n("startup.field.password")) }, @@ -148,12 +148,11 @@ fun PasswordForm( isError = passwordTooWeakError, showErrorWhen = AFTER_FOCUS_LOST_ONCE, errorMessage = i18n("startup.error.password_too_weak"), - visualTransformation = PasswordVisualTransformation(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Next), modifier = Modifier.fillMaxWidth().focusRequester(initialFocusRequester), - onEnter = { focusManager.moveFocus(FocusDirection.Next) } + onEnter = { focusManager.moveFocus(FocusDirection.Next) }, ) - OutlinedTextField( + OutlinedPasswordTextField( value = passwordConfirmation, onValueChange = setPasswordConfirmation, label = { Text(i18n("startup.field.password_confirmation")) }, @@ -161,7 +160,6 @@ fun PasswordForm( isError = passwordsDontMatchError, showErrorWhen = AFTER_FIRST_FOCUSSED, errorMessage = i18n("startup.error.passwords_not_match"), - visualTransformation = PasswordVisualTransformation(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done), modifier = Modifier.fillMaxWidth(), onEnter = onEnter,