From d2d06c5d5a3dbc1c38b56b2bac8a6e5226950287 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Wed, 6 May 2026 16:21:35 +0300 Subject: [PATCH] Fix moving textfield cursor, refactor textfields --- .../EncryptionServicesSettingsScreen.kt | 328 +++-------- .../screen/ProxyServicesSettingsScreen.kt | 346 +++-------- .../ValidationServicesSettingsScreen.kt | 104 +--- .../myeid/pinandcertificate/MyEidPinScreen.kt | 59 +- .../ui/component/settings/EditValueDialog.kt | 107 +--- .../MobileIdAndSmartIdServicesComponent.kt | 58 +- .../TimestampServicesComponent.kt | 103 +--- .../ui/component/shared/PrimaryTextField.kt | 255 +++++++++ .../ui/component/shared/RoleDataView.kt | 538 ++++-------------- .../ui/component/shared/SecurePinTextField.kt | 221 ++++--- .../ui/component/signing/IdCardView.kt | 100 +--- .../ui/component/signing/MobileIdView.kt | 323 +++-------- .../DigiDoc/ui/component/signing/NFCView.kt | 292 +++------- .../ui/component/signing/SmartIdView.kt | 254 +++------ 14 files changed, 1022 insertions(+), 2066 deletions(-) create mode 100644 app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/PrimaryTextField.kt diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/EncryptionServicesSettingsScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/EncryptionServicesSettingsScreen.kt index 10057617a..204ef50ae 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/EncryptionServicesSettingsScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/EncryptionServicesSettingsScreen.kt @@ -47,10 +47,7 @@ import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost @@ -72,9 +69,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager @@ -98,6 +93,7 @@ import ee.ria.DigiDoc.domain.model.settings.CDOCSetting import ee.ria.DigiDoc.ui.component.menu.SettingsMenuBottomSheet import ee.ria.DigiDoc.ui.component.settings.SettingsSwitchItem import ee.ria.DigiDoc.ui.component.shared.InvisibleElement +import ee.ria.DigiDoc.ui.component.shared.PrimaryTextField import ee.ria.DigiDoc.ui.component.shared.TopBar import ee.ria.DigiDoc.ui.component.shared.dialog.OptionChooserDialog import ee.ria.DigiDoc.ui.component.support.textFieldValueSaver @@ -108,7 +104,6 @@ import ee.ria.DigiDoc.ui.theme.Dimensions.XSBorder import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding import ee.ria.DigiDoc.ui.theme.buttonRoundedCornerShape import ee.ria.DigiDoc.utils.Route -import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.isTalkBackEnabled import ee.ria.DigiDoc.utils.extensions.notAccessible import ee.ria.DigiDoc.utils.snackbar.SnackBarManager import ee.ria.DigiDoc.viewmodel.shared.SharedCertificateViewModel @@ -117,7 +112,6 @@ import ee.ria.DigiDoc.viewmodel.shared.SharedSettingsViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -516,18 +510,12 @@ fun EncryptionServicesSettingsScreen( contentDescription = serverLabel }, ) { - OutlinedTextField( - label = { - Text(serverLabel) - }, - value = nameChoices[settingsCdocNameChoiceInt.intValue], + PrimaryTextField( + value = TextFieldValue(text = nameChoices[settingsCdocNameChoiceInt.intValue]), onValueChange = {}, readOnly = true, singleLine = true, - modifier = - modifier - .fillMaxWidth() - .focusRequester(nameFocusRequester), + label = serverLabel, trailingIcon = { Icon( imageVector = @@ -541,6 +529,7 @@ fun EncryptionServicesSettingsScreen( }, ) }, + testTag = "encryptionServicesUuidTextField", ) if (!openOptionChooserDialog) { @@ -555,7 +544,8 @@ fun EncryptionServicesSettingsScreen( interactionSource = interactionSource, indication = null, ).semantics { - contentDescription = serverLabel + contentDescription = + "$serverLabel: ${nameChoices[settingsCdocNameChoiceInt.intValue]}" }, ) } else { @@ -600,246 +590,72 @@ fun EncryptionServicesSettingsScreen( } } - Spacer(modifier = modifier.padding(MSPadding)) - - Row( - modifier = - modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - enabled = - settingsCdocServiceChoice.value == CDOCSetting.CDOC2 && - useKeyTransfer.value && - !useDefaultKeyTransferServer.value, - value = uuidText, - singleLine = true, - onValueChange = { - uuidText = it.copy(selection = TextRange(it.text.length)) - setCDOC2UUID(it.text) - }, - shape = RectangleShape, - label = { Text(uuidLabel) }, - modifier = - modifier - .focusRequester(uuidFocusRequester) - .weight(1f) - .fillMaxWidth() - .semantics { - testTagsAsResourceId = true - }.testTag("encryptionServicesUuidTextField"), - trailingIcon = { - if (!isTalkBackEnabled(context) && uuidText.text.isNotEmpty()) { - IconButton(onClick = { - uuidText = TextFieldValue("") - }) { - Icon( - imageVector = - ImageVector.vectorResource( - R.drawable.ic_icon_remove, - ), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - }, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Text, - ), - ) - - if (isTalkBackEnabled(context) && uuidText.text.isNotEmpty()) { - IconButton(onClick = { - uuidText = TextFieldValue("") - scope.launch(Main) { - uuidFocusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - uuidFocusRequester.requestFocus() - } - }) { - Icon( - modifier = - modifier - .semantics { - testTagsAsResourceId = true - }.testTag("encryptionServicesUuidRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - } - - Spacer(modifier = modifier.height(MSPadding)) - - Row( - modifier = - modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - enabled = - settingsCdocServiceChoice.value == CDOCSetting.CDOC2 && - useKeyTransfer.value && - !useDefaultKeyTransferServer.value, - value = fetchUrlText, - singleLine = true, - onValueChange = { - fetchUrlText = - it.copy(selection = TextRange(it.text.length)) - setCDOC2FetchURL(it.text) - }, - shape = RectangleShape, - label = { Text(fetchUrlLabel) }, - modifier = - modifier - .focusRequester(fetchUrlFocusRequester) - .weight(1f) - .fillMaxWidth() - .semantics { - testTagsAsResourceId = true - }.testTag("encryptionServicesFetchUrlTextField"), - trailingIcon = { - if (!isTalkBackEnabled(context) && fetchUrlText.text.isNotEmpty()) { - IconButton(onClick = { - fetchUrlText = TextFieldValue("") - }) { - Icon( - imageVector = - ImageVector.vectorResource( - R.drawable.ic_icon_remove, - ), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - }, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Uri, - ), - ) - - if (isTalkBackEnabled(context) && fetchUrlText.text.isNotEmpty()) { - IconButton(onClick = { - fetchUrlText = TextFieldValue("") - scope.launch(Main) { - fetchUrlFocusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - fetchUrlFocusRequester.requestFocus() - } - }) { - Icon( - modifier = - modifier - .semantics { - testTagsAsResourceId = true - }.testTag("encryptionServicesFetchUrlRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - } - - Spacer(modifier = modifier.height(MSPadding)) - - Row( - modifier = - modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - enabled = - settingsCdocServiceChoice.value == CDOCSetting.CDOC2 && - useKeyTransfer.value && - !useDefaultKeyTransferServer.value, - value = postUrlText, - singleLine = true, - onValueChange = { - postUrlText = it.copy(selection = TextRange(it.text.length)) - setCDOC2PostURL(it.text) - }, - shape = RectangleShape, - label = { Text(postUrlLabel) }, - modifier = - modifier - .focusRequester(postUrlFocusRequester) - .weight(1f) - .fillMaxWidth() - .semantics { - testTagsAsResourceId = true - }.testTag("encryptionServicesPostUrlTextField"), - trailingIcon = { - if (!isTalkBackEnabled(context) && postUrlText.text.isNotEmpty()) { - IconButton(onClick = { - postUrlText = TextFieldValue("") - }) { - Icon( - imageVector = - ImageVector.vectorResource( - R.drawable.ic_icon_remove, - ), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - }, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Uri, - ), - ) + PrimaryTextField( + modifier = Modifier.padding(vertical = MSPadding), + value = uuidText, + onValueChange = { + uuidText = it + setCDOC2UUID(it.text) + }, + singleLine = true, + label = uuidLabel, + enabled = + settingsCdocServiceChoice.value == CDOCSetting.CDOC2 && + useKeyTransfer.value && + !useDefaultKeyTransferServer.value, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Text, + ), + testTag = "encryptionServicesUuidTextField", + removeIconTestTag = "encryptionServicesUuidRemoveIconButton", + ) + + PrimaryTextField( + modifier = Modifier.padding(vertical = MSPadding), + value = fetchUrlText, + onValueChange = { + fetchUrlText = it + setCDOC2FetchURL(it.text) + }, + singleLine = true, + label = fetchUrlLabel, + enabled = + settingsCdocServiceChoice.value == CDOCSetting.CDOC2 && + useKeyTransfer.value && + !useDefaultKeyTransferServer.value, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Uri, + ), + testTag = "encryptionServicesFetchUrlTextField", + removeIconTestTag = "encryptionServicesFetchUrlRemoveIconButton", + ) + + PrimaryTextField( + modifier = Modifier.padding(vertical = MSPadding), + value = postUrlText, + onValueChange = { + postUrlText = it + setCDOC2PostURL(it.text) + }, + singleLine = true, + label = postUrlLabel, + enabled = + settingsCdocServiceChoice.value == CDOCSetting.CDOC2 && + useKeyTransfer.value && + !useDefaultKeyTransferServer.value, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Uri, + ), + testTag = "encryptionServicesPostUrlTextField", + removeIconTestTag = "encryptionServicesPostUrlRemoveIconButton", + ) - if (isTalkBackEnabled(context) && postUrlText.text.isNotEmpty()) { - IconButton(onClick = { - postUrlText = TextFieldValue("") - scope.launch(Main) { - postUrlFocusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - postUrlFocusRequester.requestFocus() - } - }) { - Icon( - modifier = - modifier - .semantics { - testTagsAsResourceId = true - }.testTag("encryptionServicesPostUrlRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - } if (settingsCdocServiceChoice.value == CDOCSetting.CDOC2 && useKeyTransfer.value && !useDefaultKeyTransferServer.value diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ProxyServicesSettingsScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ProxyServicesSettingsScreen.kt index 4f3fbfc8f..3a296d77d 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ProxyServicesSettingsScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ProxyServicesSettingsScreen.kt @@ -23,15 +23,12 @@ package ee.ria.DigiDoc.fragment.screen import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable -import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions @@ -41,8 +38,6 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost @@ -64,7 +59,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager @@ -79,9 +73,7 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontWeight 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.text.input.TextFieldValue -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.lifecycle.asFlow import androidx.navigation.NavHostController @@ -90,6 +82,7 @@ import ee.ria.DigiDoc.network.proxy.ManualProxy import ee.ria.DigiDoc.network.proxy.ProxySetting import ee.ria.DigiDoc.ui.component.menu.SettingsMenuBottomSheet import ee.ria.DigiDoc.ui.component.shared.InvisibleElement +import ee.ria.DigiDoc.ui.component.shared.PrimaryTextField import ee.ria.DigiDoc.ui.component.shared.TopBar import ee.ria.DigiDoc.ui.component.support.textFieldValueSaver import ee.ria.DigiDoc.ui.theme.Dimensions.LPadding @@ -407,167 +400,84 @@ fun ProxyServicesSettingsScreen( .padding(horizontal = SPadding) .padding(bottom = LPadding), ) { - Row( + PrimaryTextField( modifier = - modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - enabled = settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name, - value = proxyHost, - singleLine = true, - onValueChange = { - proxyHost = it.copy(selection = TextRange(it.text.length)) - setProxyHost(it.text) - }, - shape = RectangleShape, - label = { Text(stringResource(R.string.main_settings_proxy_host)) }, - modifier = - modifier - .focusRequester(hostFocusRequester) - .weight(1f) - .fillMaxWidth() - .semantics { - testTagsAsResourceId = true - }.testTag("proxyServicesHostTextField"), - trailingIcon = { - if (!isTalkBackEnabled(context) && proxyHost.text.isNotEmpty()) { - IconButton(onClick = { - proxyHost = TextFieldValue("") - }) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - }, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Uri, - ), - ) - - if (isTalkBackEnabled(context) && proxyHost.text.isNotEmpty()) { - IconButton(onClick = { - proxyHost = TextFieldValue("") - scope.launch(Main) { - hostFocusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - hostFocusRequester.requestFocus() - } - }) { - Icon( - modifier = - modifier - .semantics { - testTagsAsResourceId = true - }.testTag("proxyServicesHostRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - } - - Spacer(modifier = modifier.height(XSPadding)) + Modifier + .focusRequester(hostFocusRequester) + .padding(vertical = XSPadding), + value = proxyHost, + onValueChange = { + proxyHost = it + setProxyHost(it.text) + }, + singleLine = true, + label = stringResource(R.string.main_settings_proxy_host), + enabled = settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Uri, + ), + onDone = { + portFocusRequester.requestFocus() + }, + testTag = "proxyServicesHostTextField", + removeIconTestTag = "proxyServicesHostRemoveIconButton", + ) - Row( + PrimaryTextField( modifier = - modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - enabled = settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name, - value = proxyPort, - singleLine = true, - onValueChange = { - proxyPort = it.copy(selection = TextRange(it.text.length)) - if (isValidPortNumber(it.text)) { - setProxyPort(it.text.toInt()) - } - }, - shape = RectangleShape, - label = { Text(stringResource(R.string.main_settings_proxy_port)) }, - modifier = - modifier - .focusRequester(portFocusRequester) - .weight(1f) - .fillMaxWidth() - .semantics { - testTagsAsResourceId = true - }.testTag("proxyServicesPortTextField"), - trailingIcon = { - if (!isTalkBackEnabled(context) && proxyPort.text.isNotEmpty()) { - IconButton(onClick = { - proxyPort = TextFieldValue("") - }) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - }, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Number, - ), - ) - - if (isTalkBackEnabled(context) && proxyPort.text.isNotEmpty()) { - IconButton(onClick = { - proxyPort = TextFieldValue("") - scope.launch(Main) { - portFocusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - portFocusRequester.requestFocus() - } - }) { - Icon( - modifier = - modifier - .semantics { - testTagsAsResourceId = true - }.testTag("proxyServicesPortRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) + Modifier + .focusRequester(portFocusRequester) + .padding(vertical = XSPadding), + value = proxyPort, + onValueChange = { + proxyPort = it + if (isValidPortNumber(it.text)) { + setProxyPort(it.text.toInt()) } - } - } - if (proxyPortErrorText.isNotEmpty()) { - Text( - modifier = - modifier - .fillMaxWidth() - .focusable(enabled = true) - .semantics { contentDescription = proxyPortErrorText } - .testTag("proxyServicesPortErrorText"), - text = proxyPortErrorText, - color = MaterialTheme.colorScheme.error, - ) - } + }, + singleLine = true, + label = stringResource(R.string.main_settings_proxy_port), + enabled = settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Number, + ), + onDone = { + usernameFocusRequester.requestFocus() + }, + isError = proxyPortErrorText.isNotEmpty(), + errorText = proxyPortErrorText, + testTag = "proxyServicesPortTextField", + removeIconTestTag = "proxyServicesPortRemoveIconButton", + ) - Spacer(modifier = modifier.height(XSPadding)) + PrimaryTextField( + modifier = + Modifier + .focusRequester(usernameFocusRequester) + .padding(vertical = XSPadding), + value = proxyUsername, + onValueChange = { + proxyUsername = it + setProxyUsername(it.text) + }, + singleLine = true, + label = stringResource(R.string.main_settings_proxy_username), + enabled = settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Text, + ), + onDone = { + passwordFocusRequester.requestFocus() + }, + testTag = "proxyServicesUsernameTextField", + removeIconTestTag = "proxyServicesUsernameRemoveIconButton", + ) Row( modifier = @@ -576,98 +486,21 @@ fun ProxyServicesSettingsScreen( horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, ) { - OutlinedTextField( - enabled = settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name, - value = proxyUsername, - singleLine = true, - onValueChange = { - proxyUsername = it.copy(selection = TextRange(it.text.length)) - setProxyUsername(it.text) - }, - shape = RectangleShape, - label = { Text(stringResource(R.string.main_settings_proxy_username)) }, + PrimaryTextField( modifier = - modifier - .focusRequester(usernameFocusRequester) + Modifier + .focusRequester(passwordFocusRequester) .weight(1f) - .fillMaxWidth() - .semantics { - testTagsAsResourceId = true - }.testTag("proxyServicesUsernameTextField"), - trailingIcon = { - if (!isTalkBackEnabled(context) && proxyUsername.text.isNotEmpty()) { - IconButton(onClick = { - proxyUsername = TextFieldValue("") - }) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - }, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Text, - ), - ) - - if (isTalkBackEnabled(context) && proxyUsername.text.isNotEmpty()) { - IconButton(onClick = { - proxyUsername = TextFieldValue("") - scope.launch(Main) { - usernameFocusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - usernameFocusRequester.requestFocus() - } - }) { - Icon( - modifier = - modifier - .semantics { - testTagsAsResourceId = true - }.testTag("proxyServicesUsernameRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - } - - Spacer(modifier = modifier.height(XSPadding)) - - Row( - modifier = - modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - enabled = settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name, + .padding(vertical = XSPadding), value = proxyPassword, - singleLine = true, onValueChange = { - proxyPassword = it.copy(selection = TextRange(it.text.length)) + proxyPassword = it setProxyPassword(it.text) }, - shape = RectangleShape, - label = { Text(stringResource(R.string.main_settings_proxy_password)) }, - modifier = - modifier - .focusRequester(passwordFocusRequester) - .weight(1f) - .fillMaxWidth() - .semantics { - testTagsAsResourceId = true - }.testTag("proxyServicesPasswordTextField"), + singleLine = true, + label = stringResource(R.string.main_settings_proxy_password), + enabled = settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name, + isPasswordText = !passwordVisible, trailingIcon = { val image = if (passwordVisible) { @@ -687,25 +520,18 @@ fun ProxyServicesSettingsScreen( modifier = modifier .semantics { traversalIndex = 9f } - .testTag("mainSettingsProxyPasswordVisibleButton"), + .testTag("proxyServicesPasswordVisibleButton"), onClick = { passwordVisible = !passwordVisible }, ) { Icon(imageVector = image, description) } }, - textStyle = MaterialTheme.typography.titleSmall, - visualTransformation = - if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), keyboardOptions = KeyboardOptions.Default.copy( imeAction = ImeAction.Done, keyboardType = KeyboardType.Password, ), + testTag = "proxyServicesPasswordTextField", ) if (isTalkBackEnabled(context) && proxyPassword.text.isNotEmpty()) { @@ -760,7 +586,7 @@ fun ProxyServicesSettingsScreen( contentDescription = "${proxyCheckConnectionText.lowercase()} $buttonName" testTagsAsResourceId = true - }.testTag("mainSettingsProxyServicesCheckInternetConnectionButton"), + }.testTag("proxyServicesCheckInternetConnectionButton"), text = proxyCheckConnectionText, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold, diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ValidationServicesSettingsScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ValidationServicesSettingsScreen.kt index f9cbf4dad..b4f8dc81f 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ValidationServicesSettingsScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ValidationServicesSettingsScreen.kt @@ -40,11 +40,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost @@ -65,15 +61,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics @@ -88,6 +80,7 @@ import ee.ria.DigiDoc.R import ee.ria.DigiDoc.network.siva.SivaSetting import ee.ria.DigiDoc.ui.component.menu.SettingsMenuBottomSheet import ee.ria.DigiDoc.ui.component.shared.InvisibleElement +import ee.ria.DigiDoc.ui.component.shared.PrimaryTextField import ee.ria.DigiDoc.ui.component.shared.TopBar import ee.ria.DigiDoc.ui.component.support.textFieldValueSaver import ee.ria.DigiDoc.ui.theme.Dimensions.LPadding @@ -96,7 +89,6 @@ import ee.ria.DigiDoc.ui.theme.Dimensions.XSBorder import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding import ee.ria.DigiDoc.ui.theme.buttonRoundedCornerShape import ee.ria.DigiDoc.utils.Route -import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.isTalkBackEnabled import ee.ria.DigiDoc.utils.extensions.notAccessible import ee.ria.DigiDoc.utils.snackbar.SnackBarManager import ee.ria.DigiDoc.viewmodel.shared.SharedCertificateViewModel @@ -105,7 +97,6 @@ import ee.ria.DigiDoc.viewmodel.shared.SharedSettingsViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -356,81 +347,26 @@ fun ValidationServicesSettingsScreen( } if (settingsSivaServiceChoice.value == SivaSetting.MANUAL.name) { - Spacer(modifier = modifier.height(LPadding)) - - Row( + PrimaryTextField( modifier = - modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - enabled = settingsSivaServiceChoice.value == SivaSetting.MANUAL.name, - value = settingsSivaServiceUrl, - singleLine = true, - onValueChange = { - settingsSivaServiceUrl = it - setSettingsSivaUrl(it.text) - }, - shape = RectangleShape, - label = { Text(stringResource(R.string.main_settings_siva_service_url)) }, - modifier = - modifier - .focusRequester(focusRequester) - .weight(1f) - .fillMaxWidth() - .semantics { - testTagsAsResourceId = true - }.testTag("validationServicesComponentTextField"), - trailingIcon = { - if (!isTalkBackEnabled(context) && settingsSivaServiceUrl.text.isNotEmpty()) { - IconButton(onClick = { - settingsSivaServiceUrl = TextFieldValue("") - }) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - }, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Uri, - ), - ) - - if (isTalkBackEnabled(context) && settingsSivaServiceUrl.text.isNotEmpty()) { - IconButton(onClick = { - settingsSivaServiceUrl = TextFieldValue("") - scope.launch(Main) { - focusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - focusRequester.requestFocus() - } - }) { - Icon( - modifier = - modifier - .semantics { - testTagsAsResourceId = true - }.testTag("validationServicesRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - } - - Spacer(modifier = modifier.height(SPadding)) + Modifier + .padding(vertical = LPadding), + value = settingsSivaServiceUrl, + onValueChange = { + settingsSivaServiceUrl = it + setSettingsSivaUrl(it.text) + }, + singleLine = true, + label = stringResource(R.string.main_settings_siva_service_url), + enabled = settingsSivaServiceChoice.value == SivaSetting.MANUAL.name, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Uri, + ), + testTag = "validationServicesComponentTextField", + removeIconTestTag = "validationServicesRemoveIconButton", + ) Text( modifier = diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/pinandcertificate/MyEidPinScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/pinandcertificate/MyEidPinScreen.kt index ba56162a0..c21d08eda 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/pinandcertificate/MyEidPinScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/myeid/pinandcertificate/MyEidPinScreen.kt @@ -35,7 +35,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -688,25 +687,22 @@ fun MyEidPinScreen( testTagsAsResourceId = true }.testTag("myEidCurrentPinTextField"), pin = currentPinState, - pinCodeLabel = pinCodeLabel, - pinNumberFocusRequester = currentPinFocusRequester, - previousFocusRequester = pinChangeTitleFocusRequester, + label = pinCodeLabel, + focusRequester = currentPinFocusRequester, +// previousFocusRequester = pinChangeTitleFocusRequester, pinCodeTextEdited = null, - trailingIconContentDescription = "$clearButtonText $buttonName", +// trailingIconContentDescription = "$clearButtonText $buttonName", isError = !isCurrentPinValid, keyboardImeAction = ImeAction.Next, - keyboardActions = - KeyboardActions( - onNext = { - if (isCurrentPinValid) { - showCurrentPinField.value = false - showNewRepeatPinField.value = false - showNewPinField.value = true - } else { - focusManager.clearFocus() - } - }, - ), + onDone = { + if (isCurrentPinValid) { + showCurrentPinField.value = false + showNewRepeatPinField.value = false + showNewPinField.value = true + } else { + focusManager.clearFocus() + } + }, ) if (isTalkBackEnabled(context) && currentPinState.value.isNotEmpty()) { IconButton( @@ -794,14 +790,23 @@ fun MyEidPinScreen( testTagsAsResourceId = true }.testTag("myEidNewPinTextField"), pin = newPinState, - pinCodeLabel = pinCodeLabel, - pinNumberFocusRequester = newPinFocusRequester, - previousFocusRequester = newPinDescriptionFocusRequester, + label = pinCodeLabel, + focusRequester = newPinFocusRequester, +// previousFocusRequester = newPinDescriptionFocusRequester, pinCodeTextEdited = null, - trailingIconContentDescription = "$clearButtonText $buttonName", +// trailingIconContentDescription = "$clearButtonText $buttonName", isError = !isNewPinValid, keyboardImeAction = ImeAction.Next, - keyboardActions = + onDone = { + if (isNewPinValid) { + showCurrentPinField.value = false + showNewPinField.value = false + showNewRepeatPinField.value = true + } else { + focusManager.clearFocus() + } + }, + /*keyboardActions = KeyboardActions( onNext = { if (isNewPinValid) { @@ -812,7 +817,7 @@ fun MyEidPinScreen( focusManager.clearFocus() } }, - ), + ),*/ ) if (isTalkBackEnabled(context) && newPinState.value.isNotEmpty()) { IconButton( @@ -929,11 +934,11 @@ fun MyEidPinScreen( testTagsAsResourceId = true }.testTag("myEidNewPinRepeatedTextField"), pin = newPinRepeatedState, - pinCodeLabel = pinCodeLabel, - pinNumberFocusRequester = newPinRepeatedFocusRequester, - previousFocusRequester = newPinRepeatedDescriptionFocusRequester, + label = pinCodeLabel, + focusRequester = newPinRepeatedFocusRequester, +// previousFocusRequester = newPinRepeatedDescriptionFocusRequester, pinCodeTextEdited = null, - trailingIconContentDescription = "$clearButtonText $buttonName", +// trailingIconContentDescription = "$clearButtonText $buttonName", isError = !isNewRepeatedPinValid, ) if (isTalkBackEnabled(context) && newPinRepeatedState.value.isNotEmpty()) { diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/EditValueDialog.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/EditValueDialog.kt index 248691d83..5d66200c4 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/EditValueDialog.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/EditValueDialog.kt @@ -22,9 +22,7 @@ package ee.ria.DigiDoc.ui.component.settings import android.content.res.Configuration -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -34,10 +32,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -47,20 +42,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.semantics.clearAndSetSemantics -import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTag import androidx.compose.ui.semantics.testTagsAsResourceId -import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue @@ -68,16 +56,12 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import ee.ria.DigiDoc.R import ee.ria.DigiDoc.ui.component.shared.CancelAndOkButtonRow +import ee.ria.DigiDoc.ui.component.shared.PrimaryTextField import ee.ria.DigiDoc.ui.theme.Dimensions.MPadding import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding import ee.ria.DigiDoc.ui.theme.RIADigiDocTheme -import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.formatNumbers -import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.isTalkBackEnabled import ee.ria.DigiDoc.utils.extensions.notAccessible -import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch @OptIn(ExperimentalComposeUiApi::class) @Composable @@ -138,79 +122,24 @@ fun EditValueDialog( textAlign = TextAlign.Center, ) - Spacer(modifier = modifier.height(SPadding)) - - Row( + PrimaryTextField( modifier = - modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - modifier = - modifier - .focusRequester(focusRequester) - .weight(1f) - .fillMaxWidth() - .clearAndSetSemantics { - testTagsAsResourceId = true - testTag = "editValueDialogTextField" - contentDescription = "$title ${formatNumbers(editValue.text)}" - }.testTag("editValueTextField"), - value = editValue, - onValueChange = { newValue -> - onEditValueChange(newValue.copy(selection = TextRange(newValue.text.length))) - }, - label = { Text(subtitle) }, - maxLines = 1, - singleLine = true, - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Ascii, - ), - trailingIcon = { - if (!isTalkBackEnabled(context) && editValue.text.isNotEmpty()) { - IconButton(onClick = { - onClearValueClick() - scope.launch(Main) { - focusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - focusRequester.requestFocus() - } - }) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "${stringResource(R.string.clear_text)} $buttonName", - ) - } - } - }, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), - ) - - if (isTalkBackEnabled(context) && editValue.text.isNotEmpty()) { - IconButton(onClick = onClearValueClick) { - Icon( - modifier = - modifier - .semantics { - testTagsAsResourceId = true - }.testTag("editValueRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "${stringResource(R.string.clear_text)} $buttonName", - ) - } - } - } - - Spacer(modifier = modifier.height(SPadding)) + Modifier + .padding(vertical = SPadding), + value = editValue, + onValueChange = { newValue -> + onEditValueChange(newValue) + }, + singleLine = true, + label = subtitle, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Ascii, + ), + testTag = "editValueDialogTextField", + removeIconTestTag = "editValueRemoveIconButton", + ) CancelAndOkButtonRow( modifier = modifier, diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/advanced/signingservices/MobileIdAndSmartIdServicesComponent.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/advanced/signingservices/MobileIdAndSmartIdServicesComponent.kt index 86e8c50e9..5da2cb32b 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/advanced/signingservices/MobileIdAndSmartIdServicesComponent.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/advanced/signingservices/MobileIdAndSmartIdServicesComponent.kt @@ -27,10 +27,8 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Card @@ -38,8 +36,6 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -54,9 +50,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager @@ -71,13 +65,12 @@ import androidx.compose.ui.semantics.traversalIndex import androidx.compose.ui.text.TextRange 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.text.input.TextFieldValue -import androidx.compose.ui.text.input.VisualTransformation import ee.ria.DigiDoc.R import ee.ria.DigiDoc.common.Constant.Defaults.DEFAULT_UUID_VALUE import ee.ria.DigiDoc.domain.model.settings.UUIDSetting import ee.ria.DigiDoc.ui.component.shared.InvisibleElement +import ee.ria.DigiDoc.ui.component.shared.PrimaryTextField import ee.ria.DigiDoc.ui.component.support.textFieldValueSaver import ee.ria.DigiDoc.ui.theme.Dimensions.LPadding import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding @@ -253,7 +246,7 @@ fun MobileIdAndSmartIdServicesComponent( } if (settingsUuidChoice.value == UUIDSetting.MANUAL.name) { - Spacer(modifier = modifier.height(LPadding)) +// Spacer(modifier = modifier.height(LPadding)) Row( modifier = @@ -262,24 +255,25 @@ fun MobileIdAndSmartIdServicesComponent( horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, ) { - OutlinedTextField( - enabled = settingsUuidChoice.value == UUIDSetting.MANUAL.name, + PrimaryTextField( + modifier = + Modifier + .padding(vertical = LPadding) + .weight(1f), value = settingsUuid, - singleLine = true, onValueChange = { - settingsUuid = it.copy(selection = TextRange(it.text.length)) + settingsUuid = it setSettingsUuid(it.text) }, - shape = RectangleShape, - label = { Text(accessToMobileAndSmartIdServicesText) }, - modifier = - modifier - .focusRequester(focusRequester) - .weight(1f) - .fillMaxWidth() - .semantics { - testTagsAsResourceId = true - }.testTag("mobileIdAndSmartIdServicesComponentTextField"), + singleLine = true, + enabled = settingsUuidChoice.value == UUIDSetting.MANUAL.name, + label = accessToMobileAndSmartIdServicesText, + isPasswordText = !passwordVisible, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Password, + ), trailingIcon = { val image = if (passwordVisible) { @@ -299,25 +293,13 @@ fun MobileIdAndSmartIdServicesComponent( modifier = modifier .semantics { traversalIndex = 9f } - .testTag("mainSettingsUUIDPasswordVisibleButton"), + .testTag("mobileIdAndSmartIdServicesComponentPasswordVisibleButton"), onClick = { passwordVisible = !passwordVisible }, ) { Icon(imageVector = image, description) } }, - textStyle = MaterialTheme.typography.titleSmall, - visualTransformation = - if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Password, - ), + testTag = "mobileIdAndSmartIdServicesComponentTextField", ) if (isTalkBackEnabled(context) && settingsUuid.text.isNotEmpty()) { @@ -335,7 +317,7 @@ fun MobileIdAndSmartIdServicesComponent( modifier .semantics { testTagsAsResourceId = true - }.testTag("proxyServicesUsernameRemoveIconButton"), + }.testTag("mobileIdAndSmartIdServicesComponentRemoveIconButton"), imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), contentDescription = "$clearButtonText $buttonName", ) diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/advanced/signingservices/TimestampServicesComponent.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/advanced/signingservices/TimestampServicesComponent.kt index e0b4c0946..5b4a6bdab 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/advanced/signingservices/TimestampServicesComponent.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/advanced/signingservices/TimestampServicesComponent.kt @@ -38,11 +38,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -59,15 +55,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics @@ -81,6 +73,7 @@ import androidx.navigation.NavHostController import ee.ria.DigiDoc.R import ee.ria.DigiDoc.domain.model.settings.TSASetting import ee.ria.DigiDoc.ui.component.shared.InvisibleElement +import ee.ria.DigiDoc.ui.component.shared.PrimaryTextField import ee.ria.DigiDoc.ui.component.support.textFieldValueSaver import ee.ria.DigiDoc.ui.theme.Dimensions.LPadding import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding @@ -88,13 +81,11 @@ import ee.ria.DigiDoc.ui.theme.Dimensions.XSBorder import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding import ee.ria.DigiDoc.ui.theme.buttonRoundedCornerShape import ee.ria.DigiDoc.utils.Route -import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.isTalkBackEnabled import ee.ria.DigiDoc.utils.extensions.notAccessible import ee.ria.DigiDoc.viewmodel.shared.SharedCertificateViewModel import ee.ria.DigiDoc.viewmodel.shared.SharedSettingsViewModel import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -294,81 +285,23 @@ fun TimestampServicesComponent( } if (settingsTsaServiceChoice.value == TSASetting.MANUAL.name) { - Spacer(modifier = modifier.height(LPadding)) - - Row( - modifier = - modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - enabled = settingsTsaServiceChoice.value == TSASetting.MANUAL.name, - value = settingsTsaServiceUrl, - singleLine = true, - onValueChange = { - settingsTsaServiceUrl = it.copy(selection = TextRange(it.text.length)) - setSettingsTsaUrl(it.text) - }, - shape = RectangleShape, - label = { Text(accessToTimeStampingServicesTitleText) }, - modifier = - modifier - .focusRequester(focusRequester) - .weight(1f) - .fillMaxWidth() - .semantics { - testTagsAsResourceId = true - }.testTag("timestampServicesComponentTextField"), - trailingIcon = { - if (!isTalkBackEnabled(context) && settingsTsaServiceUrl.text.isNotEmpty()) { - IconButton(onClick = { - settingsTsaServiceUrl = TextFieldValue("") - }) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - }, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Uri, - ), - ) - - if (isTalkBackEnabled(context) && settingsTsaServiceUrl.text.isNotEmpty()) { - IconButton(onClick = { - settingsTsaServiceUrl = TextFieldValue("") - scope.launch(Main) { - focusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - focusRequester.requestFocus() - } - }) { - Icon( - modifier = - modifier - .semantics { - testTagsAsResourceId = true - }.testTag("timestampServicesRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - } - - Spacer(modifier = modifier.padding(SPadding)) + PrimaryTextField( + modifier = Modifier.padding(vertical = LPadding), + value = settingsTsaServiceUrl, + onValueChange = { + settingsTsaServiceUrl = it + setSettingsTsaUrl(it.text) + }, + label = accessToTimeStampingServicesTitleText, + enabled = settingsTsaServiceChoice.value == TSASetting.MANUAL.name, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Uri, + ), + testTag = "timestampServicesComponentTextField", + removeIconTestTag = "timestampServicesRemoveIconButton", + ) Text( modifier = diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/PrimaryTextField.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/PrimaryTextField.kt new file mode 100644 index 000000000..3a576872f --- /dev/null +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/PrimaryTextField.kt @@ -0,0 +1,255 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +@file:Suppress("PackageName", "FunctionName") + +package ee.ria.DigiDoc.ui.component.shared + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.liveRegion +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import ee.ria.DigiDoc.R +import ee.ria.DigiDoc.ui.theme.Dimensions.MSPadding +import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding +import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.isTalkBackEnabled +import ee.ria.DigiDoc.utils.extensions.notAccessible +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Composable +fun PrimaryTextField( + modifier: Modifier = Modifier, + focusRequester: FocusRequester = remember { FocusRequester() }, + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + label: String, + placeholder: String = "", + readOnly: Boolean = false, + singleLine: Boolean = true, + enabled: Boolean = true, + readDigitByDigit: Boolean = false, + description: String = "", + isError: Boolean = false, + errorText: String = "", + isPasswordText: Boolean = false, + keyboardOptions: KeyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Done, + ), + trailingIcon: (@Composable () -> Unit)? = null, + onDone: (() -> Unit)? = null, + testTag: String = "", + removeIconTestTag: String = "", + descriptionTestTag: String = "", + errorTestTag: String = "", +) { + val context = LocalContext.current + val focusManager = LocalFocusManager.current + val scope = rememberCoroutineScope() + + var editingStarted by remember { mutableStateOf(false) } + + val keyboardController = LocalSoftwareKeyboardController.current + + val clearButtonText = stringResource(R.string.clear_text) + val buttonName = stringResource(R.string.button_name) + + Column(modifier = modifier) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedTextField( + modifier = + Modifier + .focusRequester(focusRequester) + .weight(1f) + .fillMaxWidth() + .onFocusChanged { focusState -> + if (!focusState.isFocused) { + editingStarted = false + } + }.semantics { + if (readDigitByDigit && value.text.isNotEmpty() && value.text.all { it.isDigit() }) { + contentDescription = value.text.split("").joinToString(" ") + } else if (isPasswordText) { + contentDescription = "" + } + testTagsAsResourceId = true + }.then(if (testTag.isNotEmpty()) Modifier.testTag(testTag) else Modifier), + enabled = enabled, + value = value, + readOnly = readOnly, + singleLine = singleLine, + onValueChange = { newValue -> + val corrected = + if (isTalkBackEnabled(context) && !editingStarted) { + editingStarted = true + newValue.copy(selection = TextRange(newValue.text.length)) + } else { + newValue + } + onValueChange(corrected) + }, + shape = RectangleShape, + label = { + Text( + text = label, + ) + }, + placeholder = { + Text( + modifier = Modifier.notAccessible(), + text = placeholder, + ) + }, + trailingIcon = { + if (trailingIcon != null) { + trailingIcon() + } else if (!readOnly && !isTalkBackEnabled(context) && value.text.isNotEmpty()) { + IconButton(onClick = { + onValueChange(TextFieldValue("")) + }) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), + contentDescription = "$clearButtonText $buttonName", + ) + } + } + }, + visualTransformation = + if (!isPasswordText) VisualTransformation.None else PasswordVisualTransformation(), + colors = + OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.primary, + ), + keyboardOptions = keyboardOptions, + keyboardActions = + KeyboardActions( + onDone = { + keyboardController?.hide() + onDone?.invoke() + }, + ), + isError = isError, + ) + + if (trailingIcon == null && !readOnly && isTalkBackEnabled(context) && value.text.isNotEmpty()) { + IconButton(onClick = { + onValueChange(TextFieldValue("")) + scope.launch(Main) { + focusRequester.requestFocus() + focusManager.clearFocus() + delay(200) + focusRequester.requestFocus() + } + }) { + Icon( + modifier = + Modifier + .semantics { testTagsAsResourceId = true } + .then( + if (removeIconTestTag.isNotEmpty()) { + Modifier.testTag(removeIconTestTag) + } else { + Modifier + }, + ), + imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), + contentDescription = "$clearButtonText $buttonName", + ) + } + } + } + + if (description.isNotEmpty()) { + Text( + text = description, + modifier = + modifier + .padding(vertical = XSPadding) + .testTag(descriptionTestTag) + .notAccessible(), + color = MaterialTheme.colorScheme.onSecondary, + textAlign = TextAlign.Start, + style = MaterialTheme.typography.labelMedium, + ) + } + + if (errorText.isNotEmpty()) { + Text( + modifier = + Modifier + .padding(top = XSPadding) + .padding(bottom = MSPadding) + .fillMaxWidth() + .semantics { + contentDescription = errorText + liveRegion = LiveRegionMode.Polite + }.testTag(errorTestTag), + text = errorText, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + ) + } + } +} diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/RoleDataView.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/RoleDataView.kt index 8448401c1..b4601858a 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/RoleDataView.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/RoleDataView.kt @@ -23,39 +23,25 @@ package ee.ria.DigiDoc.ui.component.shared import androidx.activity.compose.BackHandler import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text 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.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusTarget -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId @@ -66,12 +52,7 @@ import androidx.compose.ui.text.style.TextAlign import ee.ria.DigiDoc.R import ee.ria.DigiDoc.ui.theme.Dimensions.MPadding import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding -import ee.ria.DigiDoc.ui.theme.Dimensions.iconSizeXXS -import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.isTalkBackEnabled import ee.ria.DigiDoc.viewmodel.shared.SharedSettingsViewModel -import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch @OptIn(ExperimentalComposeUiApi::class) @Composable @@ -80,10 +61,6 @@ fun RoleDataView( sharedSettingsViewModel: SharedSettingsViewModel, onDismiss: () -> Unit = {}, ) { - val context = LocalContext.current - val focusManager = LocalFocusManager.current - val scope = rememberCoroutineScope() - val roleAndAddressTitleFocusRequester = remember { FocusRequester() } val roleFocusRequester = remember { FocusRequester() } val cityFocusRequester = remember { FocusRequester() } @@ -113,9 +90,6 @@ fun RoleDataView( mutableStateOf(TextFieldValue(text = sharedSettingsViewModel.dataStore.getRoleZip())) } - val clearButtonText = stringResource(R.string.clear_text) - val buttonName = stringResource(id = R.string.button_name) - BackHandler { onDismiss() } @@ -147,410 +121,116 @@ fun RoleDataView( textAlign = TextAlign.Start, ) - Row( + PrimaryTextField( modifier = - modifier - .fillMaxWidth() - .padding(top = MPadding), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - label = { - Text(text = roleLabel) - }, - value = rolesAndResolutions, - singleLine = true, - onValueChange = { rolesValue -> - rolesAndResolutions = rolesValue + Modifier + .padding(top = MPadding) + .focusRequester(roleFocusRequester), + value = rolesAndResolutions, + onValueChange = { rolesValue -> + rolesAndResolutions = rolesValue - val roles = - rolesAndResolutions.text - .split(",") - .map { it.trim() } - .filter { it.isNotEmpty() } - .joinToString(", ") - sharedSettingsViewModel.dataStore.setRoles(roles) - }, - modifier = - modifier - .focusRequester(roleFocusRequester) - .focusProperties { - next = cityFocusRequester - }.weight(1f) - .semantics(mergeDescendants = true) { - testTagsAsResourceId = true - }.testTag("roleAndAddressRoleTextField"), - trailingIcon = { - if (!isTalkBackEnabled(context) && rolesAndResolutions.text.isNotEmpty()) { - IconButton(onClick = { - rolesAndResolutions = TextFieldValue("") - sharedSettingsViewModel.dataStore.setRoles("") - }) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - }, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Text, - ), - ) - if (isTalkBackEnabled(context) && rolesAndResolutions.text.isNotEmpty()) { - IconButton( - modifier = - modifier - .align(Alignment.CenterVertically), - onClick = { - rolesAndResolutions = TextFieldValue("") - scope.launch(Main) { - roleFocusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - roleFocusRequester.requestFocus() - } - }, - ) { - Icon( - modifier = - modifier - .size(iconSizeXXS) - .semantics { - testTagsAsResourceId = true - }.testTag("roleAndAddressRoleRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - } - Row( - modifier = - modifier - .fillMaxWidth() - .padding(top = MPadding), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - label = { - Text(text = cityLabel) - }, - value = city, - singleLine = true, - onValueChange = { - city = it - sharedSettingsViewModel.dataStore.setRoleCity(city.text) - }, - modifier = - modifier - .focusRequester(cityFocusRequester) - .focusProperties { - previous = roleFocusRequester - next = stateFocusRequester - }.weight(1f) - .semantics(mergeDescendants = true) { - testTagsAsResourceId = true - }.testTag("roleAndAddressCityTextField"), - trailingIcon = { - if (!isTalkBackEnabled(context) && city.text.isNotEmpty()) { - IconButton(onClick = { - city = TextFieldValue("") - sharedSettingsViewModel.dataStore.setRoleCity("") - }) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - }, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Text, - ), - ) - if (isTalkBackEnabled(context) && city.text.isNotEmpty()) { - IconButton( - modifier = - modifier - .align(Alignment.CenterVertically), - onClick = { - city = TextFieldValue("") - scope.launch(Main) { - cityFocusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - cityFocusRequester.requestFocus() - } - }, - ) { - Icon( - modifier = - modifier - .size(iconSizeXXS) - .semantics { - testTagsAsResourceId = true - }.testTag("roleAndAddressCityRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - } - Row( - modifier = - modifier - .fillMaxWidth() - .padding(top = MPadding), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - label = { - Text(text = stateLabel) - }, - value = state, - singleLine = true, - onValueChange = { - state = it - sharedSettingsViewModel.dataStore.setRoleState(state.text) - }, - modifier = - modifier - .focusRequester(stateFocusRequester) - .focusProperties { - previous = cityFocusRequester - next = countryFocusRequester - }.weight(1f) - .semantics(mergeDescendants = true) { - testTagsAsResourceId = true - }.testTag("roleAndAddressStateTextField"), - trailingIcon = { - if (!isTalkBackEnabled(context) && state.text.isNotEmpty()) { - IconButton(onClick = { - state = TextFieldValue("") - sharedSettingsViewModel.dataStore.setRoleState("") - }) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - }, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Text, - ), - ) - if (isTalkBackEnabled(context) && state.text.isNotEmpty()) { - IconButton( - modifier = - modifier - .align(Alignment.CenterVertically), - onClick = { - state = TextFieldValue("") - scope.launch(Main) { - stateFocusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - stateFocusRequester.requestFocus() - } - }, - ) { - Icon( - modifier = - modifier - .size(iconSizeXXS) - .semantics { - testTagsAsResourceId = true - }.testTag("roleAndAddressStateRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - } - Row( - modifier = - modifier - .fillMaxWidth() - .padding(top = MPadding), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - label = { - Text(text = countryLabel) - }, - value = country, - singleLine = true, - onValueChange = { - country = it - sharedSettingsViewModel.dataStore.setRoleCountry(country.text) - }, - modifier = - modifier - .focusRequester(countryFocusRequester) - .focusProperties { - previous = stateFocusRequester - next = zipFocusRequester - }.weight(1f) - .semantics(mergeDescendants = true) { - testTagsAsResourceId = true - }.testTag("roleAndAddressCountryTextField"), - trailingIcon = { - if (!isTalkBackEnabled(context) && country.text.isNotEmpty()) { - IconButton(onClick = { - country = TextFieldValue("") - sharedSettingsViewModel.dataStore.setRoleCountry("") - }) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - }, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Text, - ), - ) - if (isTalkBackEnabled(context) && country.text.isNotEmpty()) { - IconButton( - modifier = - modifier - .align(Alignment.CenterVertically), - onClick = { - country = TextFieldValue("") - scope.launch(Main) { - countryFocusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - countryFocusRequester.requestFocus() - } - }, - ) { - Icon( - modifier = - modifier - .size(iconSizeXXS) - .semantics { - testTagsAsResourceId = true - }.testTag("roleAndAddressCountryRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - } - Row( - modifier = - modifier - .fillMaxWidth() - .padding(top = MPadding), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - label = { - Text(text = zipLabel) - }, - value = zip, - singleLine = true, - onValueChange = { - zip = it - sharedSettingsViewModel.dataStore.setRoleZip(zip.text) - }, - modifier = - modifier - .focusRequester(zipFocusRequester) - .focusProperties { - previous = countryFocusRequester - }.weight(1f) - .semantics(mergeDescendants = true) { - testTagsAsResourceId = true - }.testTag("roleAndAddressZipTextField"), - trailingIcon = { - if (!isTalkBackEnabled(context) && zip.text.isNotEmpty()) { - IconButton(onClick = { - zip = TextFieldValue("") - sharedSettingsViewModel.dataStore.setRoleZip("") - }) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - }, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Ascii, - ), - ) - if (isTalkBackEnabled(context) && zip.text.isNotEmpty()) { - IconButton( - modifier = - modifier - .align(Alignment.CenterVertically), - onClick = { - zip = TextFieldValue("") - scope.launch(Main) { - zipFocusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - zipFocusRequester.requestFocus() - } - }, - ) { - Icon( - modifier = - modifier - .size(iconSizeXXS) - .semantics { - testTagsAsResourceId = true - }.testTag("roleAndAddressZipRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - } + val roles = + rolesAndResolutions.text + .split(",") + .map { it.trim() } + .filter { it.isNotEmpty() } + .joinToString(", ") + sharedSettingsViewModel.dataStore.setRoles(roles) + }, + singleLine = true, + label = roleLabel, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Text, + ), + onDone = { + cityFocusRequester.requestFocus() + }, + testTag = "roleAndAddressRoleTextField", + removeIconTestTag = "roleAndAddressRoleRemoveIconButton", + ) + + PrimaryTextField( + modifier = Modifier.padding(top = MPadding), + value = city, + onValueChange = { + city = it + sharedSettingsViewModel.dataStore.setRoleCity(city.text) + }, + singleLine = true, + label = cityLabel, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Text, + ), + onDone = { + stateFocusRequester.requestFocus() + }, + testTag = "roleAndAddressCityTextField", + removeIconTestTag = "roleAndAddressCityRemoveIconButton", + ) + + PrimaryTextField( + modifier = Modifier.padding(top = MPadding), + value = state, + onValueChange = { + state = it + sharedSettingsViewModel.dataStore.setRoleState(state.text) + }, + singleLine = true, + label = stateLabel, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Text, + ), + onDone = { + countryFocusRequester.requestFocus() + }, + testTag = "roleAndAddressStateTextField", + removeIconTestTag = "roleAndAddressStateRemoveIconButton", + ) + + PrimaryTextField( + modifier = Modifier.padding(top = MPadding), + value = country, + onValueChange = { + country = it + sharedSettingsViewModel.dataStore.setRoleCountry(country.text) + }, + singleLine = true, + label = countryLabel, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Text, + ), + onDone = { + zipFocusRequester.requestFocus() + }, + testTag = "roleAndAddressCountryTextField", + removeIconTestTag = "roleAndAddressCountryRemoveIconButton", + ) + + PrimaryTextField( + modifier = Modifier.padding(top = MPadding), + value = zip, + onValueChange = { + zip = it + sharedSettingsViewModel.dataStore.setRoleZip(zip.text) + }, + singleLine = true, + label = zipLabel, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Ascii, + ), + testTag = "roleAndAddressZipTextField", + removeIconTestTag = "roleAndAddressZipRemoveIconButton", + ) } } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/SecurePinTextField.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/SecurePinTextField.kt index 34cd7b78d..f566883e0 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/SecurePinTextField.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/SecurePinTextField.kt @@ -21,7 +21,11 @@ package ee.ria.DigiDoc.ui.component.shared -import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -33,103 +37,176 @@ import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState -import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.semantics.LiveRegionMode +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.liveRegion import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId -import androidx.compose.ui.semantics.traversalIndex import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.zIndex import ee.ria.DigiDoc.R +import ee.ria.DigiDoc.ui.theme.Dimensions.MSPadding +import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding import ee.ria.DigiDoc.ui.theme.Dimensions.iconSizeXXS import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.isTalkBackEnabled +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch -@OptIn(ExperimentalComposeUiApi::class) @Composable fun SecurePinTextField( - modifier: Modifier, + modifier: Modifier = Modifier, + focusRequester: FocusRequester = remember { FocusRequester() }, pin: MutableState, - pinCodeLabel: String, - pinNumberFocusRequester: FocusRequester, - previousFocusRequester: FocusRequester, - pinCodeTextEdited: MutableState?, - trailingIconContentDescription: String, - isError: Boolean, + label: String, + pinCodeTextEdited: MutableState? = null, + isError: Boolean = false, + errorText: String = "", keyboardImeAction: ImeAction = ImeAction.Done, - keyboardActions: KeyboardActions = KeyboardActions.Default, + onDone: (() -> Unit)? = null, + removeIconTestTag: String = "", + errorTestTag: String = "", ) { val context = LocalContext.current + val focusManager = LocalFocusManager.current + val scope = rememberCoroutineScope() - OutlinedTextField( - label = { - Text(text = pinCodeLabel) - }, - value = "*".repeat(pin.value.size), - singleLine = true, - modifier = - modifier - .focusRequester(pinNumberFocusRequester) - .focusProperties { - previous = previousFocusRequester - }.zIndex(1f) - .focusable() - .semantics { - traversalIndex = 1f - testTagsAsResourceId = true - }.testTag("pinTextField"), - onValueChange = { newValue -> - val digitsOnly = newValue.filter { it.isDigit() } - if (digitsOnly.isEmpty()) { - if (pin.value.isNotEmpty()) { - pin.value = pin.value.dropLast(1).toByteArray() - } - } else { - pin.value = pin.value + digitsOnly.last().code.toByte() - } - pinCodeTextEdited?.value = true - }, - trailingIcon = { - if (!isTalkBackEnabled(context) && pin.value.isNotEmpty()) { - IconButton( - modifier = - modifier - .zIndex(2f) - .semantics { traversalIndex = 2f } - .testTag("pinRemoveButton"), - onClick = { pin.value = byteArrayOf() }, - ) { + val keyboardController = LocalSoftwareKeyboardController.current + + val clearButtonText = stringResource(R.string.clear_text) + val buttonName = stringResource(R.string.button_name) + + Column(modifier = modifier) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedTextField( + modifier = + Modifier + .focusRequester(focusRequester) + .weight(1f) + .fillMaxWidth() + .semantics { + testTagsAsResourceId = true + }.testTag("pinTextField"), + label = { + Text(text = label) + }, + value = "*".repeat(pin.value.size), + singleLine = true, + onValueChange = { newValue -> + val digitsOnly = newValue.filter { it.isDigit() } + if (digitsOnly.isEmpty()) { + if (pin.value.isNotEmpty()) { + pin.value = pin.value.dropLast(1).toByteArray() + } + } else { + pin.value += digitsOnly.last().code.toByte() + } + pinCodeTextEdited?.value = true + }, + trailingIcon = { + if (!isTalkBackEnabled(context) && pin.value.isNotEmpty()) { + IconButton(onClick = { + pin.value = byteArrayOf() + scope.launch(Main) { + focusRequester.requestFocus() + focusManager.clearFocus() + delay(200) + focusRequester.requestFocus() + } + }) { + Icon( + modifier = + Modifier + .size(iconSizeXXS) + .semantics { testTagsAsResourceId = true } + .testTag(removeIconTestTag), + imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), + contentDescription = "$clearButtonText $buttonName", + ) + } + } + }, + colors = + OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.primary, + ), + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = keyboardImeAction, + keyboardType = KeyboardType.NumberPassword, + ), + keyboardActions = + KeyboardActions( + onDone = { + keyboardController?.hide() + onDone?.invoke() + }, + ), + isError = isError, + ) + + if (isTalkBackEnabled(context) && pin.value.isNotEmpty()) { + IconButton(onClick = { + pin.value = byteArrayOf() + pinCodeTextEdited?.value = true + scope.launch(Main) { + focusRequester.requestFocus() + focusManager.clearFocus() + delay(200) + focusRequester.requestFocus() + } + }) { Icon( modifier = - modifier - .size(iconSizeXXS) - .semantics { - testTagsAsResourceId = true - }.testTag("pinRemoveButtonIcon"), + Modifier + .semantics { testTagsAsResourceId = true } + .then( + if (removeIconTestTag.isNotEmpty()) { + Modifier.testTag(removeIconTestTag) + } else { + Modifier + }, + ), imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = trailingIconContentDescription, + contentDescription = "$clearButtonText $buttonName", ) } } - }, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = keyboardImeAction, - keyboardType = KeyboardType.NumberPassword, - ), - keyboardActions = keyboardActions, - isError = isError, - ) + } + + if (errorText.isNotEmpty()) { + Text( + modifier = + Modifier + .padding(top = XSPadding) + .padding(bottom = MSPadding) + .fillMaxWidth() + .semantics { + contentDescription = errorText + liveRegion = LiveRegionMode.Polite + }.testTag(errorTestTag), + text = errorText, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + ) + } + } } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/IdCardView.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/IdCardView.kt index 8282cbf71..20ea10ba9 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/IdCardView.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/IdCardView.kt @@ -46,7 +46,6 @@ import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -101,12 +100,10 @@ import ee.ria.DigiDoc.ui.theme.Dimensions.LPadding import ee.ria.DigiDoc.ui.theme.Dimensions.MSPadding import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding import ee.ria.DigiDoc.ui.theme.Dimensions.iconSizeXXL -import ee.ria.DigiDoc.ui.theme.Dimensions.iconSizeXXS import ee.ria.DigiDoc.ui.theme.Dimensions.loadingBarSize import ee.ria.DigiDoc.ui.theme.RIADigiDocTheme import ee.ria.DigiDoc.ui.theme.buttonRoundCornerShape import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.formatNumbers -import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.isTalkBackEnabled import ee.ria.DigiDoc.utils.extensions.notAccessible import ee.ria.DigiDoc.utils.pin.PinCodeUtil.shouldShowPINCodeError import ee.ria.DigiDoc.utils.snackbar.SnackBarManager.showMessage @@ -741,85 +738,26 @@ fun IdCardView( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(MSPadding), ) { - Row( + SecurePinTextField( modifier = - modifier - .fillMaxWidth() - .padding(top = MSPadding), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - SecurePinTextField( - modifier = - modifier - .weight(1f) - .zIndex(3f) - .semantics { - traversalIndex = 3f - testTagsAsResourceId = true - }.testTag("idCardPinTextField"), - pin = pinCode, - pinCodeLabel = pinText, - pinNumberFocusRequester = pinCodeFocusRequester, - previousFocusRequester = readyToSignFocusRequester, - pinCodeTextEdited = pinCodeTextEdited, - trailingIconContentDescription = "$clearButtonText $buttonName", - isError = - pinCodeTextEdited.value && - shouldShowPINCodeError( - pinCode.value, - codeType, - ), - ) - if (isTalkBackEnabled(context) && pinCode.value.isNotEmpty()) { - IconButton( - modifier = - modifier - .zIndex(4f) - .align(Alignment.CenterVertically) - .semantics { - traversalIndex = 4f - testTagsAsResourceId = true - }.testTag("idCardPinRemoveButton"), - onClick = { - pinCode.value = byteArrayOf() - scope.launch(Main) { - pinCodeFocusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - pinCodeFocusRequester.requestFocus() - } - }, - ) { - Icon( - modifier = - modifier - .size(iconSizeXXS) - .semantics { - testTagsAsResourceId = true - }.testTag("idCardPinRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - } - - if (pinCodeLengthErrorText.isNotEmpty()) { - Text( - modifier = - modifier - .padding(bottom = MSPadding) - .fillMaxWidth() - .focusable(true) - .semantics { contentDescription = pinCodeLengthErrorText } - .testTag("idCardPinError"), - text = pinCodeLengthErrorText, - textAlign = TextAlign.Start, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error, - ) - } + Modifier + .focusRequester(pinCodeFocusRequester) + .semantics { + testTagsAsResourceId = true + }.testTag("idCardPinTextField"), + pin = pinCode, + label = pinText, + pinCodeTextEdited = pinCodeTextEdited, + isError = + pinCodeTextEdited.value && + shouldShowPINCodeError( + pinCode.value, + codeType, + ), + errorText = pinCodeLengthErrorText, + removeIconTestTag = "idCardPinRemoveIconButton", + errorTestTag = "idCardPinError", + ) } } } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/MobileIdView.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/MobileIdView.kt index a8c2fff19..5055cc61b 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/MobileIdView.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/MobileIdView.kt @@ -26,18 +26,13 @@ import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.activity.compose.LocalActivity import androidx.compose.foundation.background -import androidx.compose.foundation.focusable import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.rememberScrollState @@ -45,13 +40,8 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -61,22 +51,17 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.TextRange @@ -91,19 +76,17 @@ import ee.ria.DigiDoc.libdigidoclib.domain.model.RoleData import ee.ria.DigiDoc.ui.component.shared.CancelAndOkButtonRow import ee.ria.DigiDoc.ui.component.shared.HrefMessageDialog import ee.ria.DigiDoc.ui.component.shared.InvisibleElement +import ee.ria.DigiDoc.ui.component.shared.PrimaryTextField import ee.ria.DigiDoc.ui.component.shared.RoleDataView import ee.ria.DigiDoc.ui.component.support.textFieldValueSaver -import ee.ria.DigiDoc.ui.theme.Dimensions.MPadding import ee.ria.DigiDoc.ui.theme.Dimensions.MSPadding import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding -import ee.ria.DigiDoc.ui.theme.Dimensions.iconSizeXXS import ee.ria.DigiDoc.ui.theme.RIADigiDocTheme import ee.ria.DigiDoc.ui.theme.buttonRoundCornerShape import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.addInvisibleElement import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.isTalkBackEnabled import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.removeInvisibleElement -import ee.ria.DigiDoc.utils.extensions.notAccessible import ee.ria.DigiDoc.utils.snackbar.SnackBarManager.showMessage import ee.ria.DigiDoc.utilsLib.validator.PersonalCodeValidator import ee.ria.DigiDoc.viewmodel.MobileIdViewModel @@ -111,7 +94,6 @@ import ee.ria.DigiDoc.viewmodel.shared.SharedContainerViewModel import ee.ria.DigiDoc.viewmodel.shared.SharedSettingsViewModel import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch @@ -208,9 +190,6 @@ fun MobileIdView( val phoneNumberFocusRequester = remember { FocusRequester() } val personalCodeFocusRequester = remember { FocusRequester() } - val clearButtonText = stringResource(R.string.clear_text) - val buttonName = stringResource(id = R.string.button_name) - val phoneNumberWithInvisibleSpaces = TextFieldValue(addInvisibleElement(countryCodeAndPhone.text)) val personalCodeWithInvisibleSpaces = TextFieldValue(addInvisibleElement(personalCode.text)) @@ -429,244 +408,96 @@ fun MobileIdView( testTagsAsResourceId = true }.testTag("mobileIdViewContainer"), ) { - Row( + PrimaryTextField( modifier = modifier - .fillMaxWidth() - .padding(top = XSPadding), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - label = { - Text( - modifier = modifier.notAccessible(), - text = countryCodeAndPhoneNumberLabel, - ) + .padding(top = XSPadding) + .focusRequester(phoneNumberFocusRequester) + .semantics(mergeDescendants = true) { + testTagsAsResourceId = true + }.testTag("signatureUpdateMobileIdPhoneNo"), + value = + if (!isTalkBackEnabled(context)) { + countryCodeAndPhone + } else { + phoneNumberWithInvisibleSpaces }, - placeholder = { - Text( - modifier = modifier.notAccessible(), - text = - stringResource( - id = R.string.mobile_id_country_code_and_phone_number_placeholder, - ), - ) - }, - value = - if (!isTalkBackEnabled(context)) { - countryCodeAndPhone - } else { - phoneNumberWithInvisibleSpaces.copy( - selection = TextRange(phoneNumberWithInvisibleSpaces.text.length), - ) - }, - singleLine = true, - onValueChange = { - countryCodeAndPhoneEdited.value = true + onValueChange = { + countryCodeAndPhoneEdited.value = true + countryCodeAndPhone = if (!isTalkBackEnabled(context)) { - countryCodeAndPhone = it.copy(selection = TextRange(it.text.length)) + it } else { - val noInvisibleElement = - TextFieldValue(removeInvisibleElement(it.text)) - countryCodeAndPhone = - noInvisibleElement.copy(selection = TextRange(noInvisibleElement.text.length)) - } - }, - modifier = - modifier - .focusRequester(phoneNumberFocusRequester) - .focusProperties { - next = personalCodeFocusRequester - }.weight(1f) - .semantics(mergeDescendants = true) { - testTagsAsResourceId = true - }.testTag("signatureUpdateMobileIdPhoneNo"), - shape = RectangleShape, - trailingIcon = { - if (!isTalkBackEnabled(context) && countryCodeAndPhone.text.isNotEmpty()) { - IconButton(onClick = { - countryCodeAndPhone = TextFieldValue("") - }) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } + TextFieldValue(removeInvisibleElement(it.text)) } - }, - isError = - countryCodeAndPhoneEdited.value && - !mobileIdViewModel.isPhoneNumberValid(countryCodeAndPhone.text), - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Decimal, - ), - ) - if (isTalkBackEnabled(context) && countryCodeAndPhone.text.isNotEmpty()) { - IconButton( - modifier = - modifier - .align(Alignment.CenterVertically), - onClick = { - countryCodeAndPhone = TextFieldValue("") - scope.launch(Main) { - phoneNumberFocusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - phoneNumberFocusRequester.requestFocus() - } - }, - ) { - Icon( - modifier = - modifier - .size(iconSizeXXS) - .semantics { - testTagsAsResourceId = true - }.testTag("smartIdCountryCodeAndPhoneNumberRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - } - if (countryCodeAndPhoneErrorText.isNotEmpty()) { - Text( - modifier = - modifier - .padding(top = XSPadding) - .padding(bottom = MSPadding) - .fillMaxWidth() - .focusable(enabled = true) - .semantics { contentDescription = countryCodeAndPhoneErrorText } - .testTag("mobileIdPhoneNoErrorText"), - text = countryCodeAndPhoneErrorText, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, - ) - } - - Row( + }, + singleLine = true, + label = countryCodeAndPhoneNumberLabel, + placeholder = + stringResource( + id = R.string.mobile_id_country_code_and_phone_number_placeholder, + ), + readDigitByDigit = true, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Decimal, + ), + onDone = { + personalCodeFocusRequester.requestFocus() + }, + isError = + countryCodeAndPhoneEdited.value && + !mobileIdViewModel.isPhoneNumberValid(countryCodeAndPhone.text), + errorText = countryCodeAndPhoneErrorText, + testTag = "signatureUpdateMobileIdPhoneNo", + removeIconTestTag = "mobileIdCountryCodeAndPhoneNumberRemoveIconButton", + errorTestTag = "mobileIdPhoneNoErrorText", + ) + + PrimaryTextField( modifier = - modifier - .fillMaxWidth() - .padding(top = MPadding), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - label = { - Text(personalCodeLabel) - }, - value = - if (!isTalkBackEnabled(context)) { - personalCode - } else { - personalCodeWithInvisibleSpaces.copy( - selection = TextRange(personalCodeWithInvisibleSpaces.text.length), - ) + Modifier + .padding(top = MSPadding) + .focusRequester(personalCodeFocusRequester) + .focusProperties { + previous = phoneNumberFocusRequester }, - singleLine = true, - onValueChange = { - personalCodeEdited.value = true + value = + if (!isTalkBackEnabled(context)) { + personalCode + } else { + personalCodeWithInvisibleSpaces + }, + onValueChange = { + personalCodeEdited.value = true + personalCode = if (!isTalkBackEnabled(context)) { - personalCode = it.copy(selection = TextRange(it.text.length)) + it } else { - val noInvisibleElement = TextFieldValue(removeInvisibleElement(it.text)) - personalCode = - noInvisibleElement.copy(selection = TextRange(noInvisibleElement.text.length)) - } - }, - modifier = - modifier - .focusRequester(personalCodeFocusRequester) - .focusProperties { - previous = phoneNumberFocusRequester - }.weight(1f) - .semantics(mergeDescendants = true) { - testTagsAsResourceId = true - }.testTag("signatureUpdateMobileIdPersonalCode"), - trailingIcon = { - if (!isTalkBackEnabled(context) && personalCode.text.isNotEmpty()) { - IconButton(onClick = { - personalCode = TextFieldValue("") - }) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } + TextFieldValue(removeInvisibleElement(it.text)) } - }, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, + }, + singleLine = true, + label = personalCodeLabel, + readDigitByDigit = true, + isError = + personalCodeEdited.value && + !mobileIdViewModel.isPersonalCodeValid( + personalCode.text, ), - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Number, - ), - shape = RectangleShape, - isError = - personalCodeEdited.value && - !mobileIdViewModel.isPersonalCodeValid( - personalCode.text, - ), - ) - if (isTalkBackEnabled(context) && personalCode.text.isNotEmpty()) { - IconButton( - modifier = - modifier - .align(Alignment.CenterVertically), - onClick = { - personalCode = TextFieldValue("") - scope.launch(Main) { - personalCodeFocusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - personalCodeFocusRequester.requestFocus() - } - }, - ) { - Icon( - modifier = - modifier - .size(iconSizeXXS) - .semantics { - testTagsAsResourceId = true - }.testTag("smartIdPersonalCodeRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - } - if (personalCodeErrorText.isNotEmpty()) { - Text( - modifier = - modifier - .fillMaxWidth() - .padding(top = XSPadding) - .padding(bottom = MSPadding) - .focusable(enabled = true) - .semantics { contentDescription = personalCodeErrorText } - .testTag("signatureUpdateMobileIdPersonalCodeErrorText"), - text = personalCodeErrorText, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, - ) - } + errorText = personalCodeErrorText, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Number, + ), + testTag = "mobileIdPersonalCode", + removeIconTestTag = "mobileIdPersonalCodeRemoveIconButton", + errorTestTag = "mobileIdPersonalCodeErrorText", + ) } } } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/NFCView.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/NFCView.kt index 70f8f0142..115fde5ed 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/NFCView.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/NFCView.kt @@ -27,18 +27,14 @@ import androidx.activity.compose.BackHandler import androidx.activity.compose.LocalActivity import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.focusable import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.rememberScrollState @@ -46,11 +42,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -63,26 +55,21 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId -import androidx.compose.ui.semantics.traversalIndex import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -104,13 +91,12 @@ import ee.ria.DigiDoc.smartcardreader.nfc.NfcSmartCardReaderManager.NfcStatus import ee.ria.DigiDoc.ui.component.shared.CancelAndOkButtonRow import ee.ria.DigiDoc.ui.component.shared.HrefMessageDialog import ee.ria.DigiDoc.ui.component.shared.InvisibleElement +import ee.ria.DigiDoc.ui.component.shared.PrimaryTextField import ee.ria.DigiDoc.ui.component.shared.RoleDataView import ee.ria.DigiDoc.ui.component.shared.SecurePinTextField import ee.ria.DigiDoc.ui.component.support.textFieldValueSaver -import ee.ria.DigiDoc.ui.theme.Dimensions.MSPadding import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding -import ee.ria.DigiDoc.ui.theme.Dimensions.iconSizeXXS import ee.ria.DigiDoc.ui.theme.RIADigiDocTheme import ee.ria.DigiDoc.ui.theme.buttonRoundCornerShape import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.addInvisibleElement @@ -124,7 +110,6 @@ import ee.ria.DigiDoc.viewmodel.shared.SharedContainerViewModel import ee.ria.DigiDoc.viewmodel.shared.SharedSettingsViewModel import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch @@ -599,149 +584,55 @@ fun NFCView( testTagsAsResourceId = true }.testTag("nfcViewContainer"), ) { - Row( + PrimaryTextField( modifier = - modifier - .fillMaxWidth() - .padding(top = XSPadding), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - label = { - Text(text = canNumberLabel) + Modifier + .padding(top = XSPadding) + .focusRequester(canNumberFocusRequester) + .testTag("nfcCanNumber"), + value = + if (!isTalkBackEnabled(context)) { + canNumber + } else { + canNumberWithInvisibleSpaces }, - value = - if (!isTalkBackEnabled(context)) { - canNumber - } else { - canNumberWithInvisibleSpaces.copy( - selection = TextRange(canNumberWithInvisibleSpaces.text.length), - ) - }, - singleLine = true, - onValueChange = { - canNumberTextEdited.value = true + onValueChange = { + canNumberTextEdited.value = true + canNumber = if (!isTalkBackEnabled(context)) { - canNumber = it.copy(selection = TextRange(it.text.length)) + it } else { - val noInvisibleElement = - TextFieldValue(removeInvisibleElement(it.text)) - canNumber = - noInvisibleElement.copy( - selection = - TextRange( - noInvisibleElement.text.length, - ), - ) + TextFieldValue(removeInvisibleElement(it.text)) } - }, - modifier = - modifier - .focusRequester(canNumberFocusRequester) - .then( - if (showPinField) { - modifier.focusProperties { - next = pinNumberFocusRequester - } - } else { - modifier - }, - ).weight(1f) - .semantics(mergeDescendants = true) { - testTagsAsResourceId = true - contentDescription = canNumberLocationText - }.testTag("signatureUpdateNFCCAN"), - trailingIcon = { - if (!isTalkBackEnabled(context) && canNumber.text.isNotEmpty()) { - IconButton(onClick = { - canNumber = TextFieldValue("") - }) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - }, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), - keyboardOptions = - if (showPinField) { - KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Number, - ) - } else { - KeyboardOptions.Default.copy( - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Number, - ) - }, - isError = - canNumberTextEdited.value && - nfcViewModel.shouldShowCANNumberError(canNumber.text), - ) - if (isTalkBackEnabled(context) && canNumber.text.isNotEmpty()) { - IconButton( - modifier = - modifier - .align(Alignment.CenterVertically), - onClick = { - canNumber = TextFieldValue("") - scope.launch(Main) { - canNumberFocusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - canNumberFocusRequester.requestFocus() - } - }, - ) { - Icon( - modifier = - modifier - .size(iconSizeXXS) - .semantics { - testTagsAsResourceId = true - }.testTag("nfcCanNumberRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - } - Text( - text = canNumberLocationText, - modifier = - modifier - .padding(vertical = XSPadding) - .testTag("signatureInputMethodTitle") - .notAccessible(), - color = MaterialTheme.colorScheme.onSecondary, - textAlign = TextAlign.Start, - style = MaterialTheme.typography.labelMedium, + }, + singleLine = true, + label = canNumberLabel, + readDigitByDigit = true, + description = canNumberLocationText, + onDone = { + pinNumberFocusRequester.requestFocus() + }, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = + if (showPinField) { + ImeAction.Next + } else { + ImeAction.Done + }, + keyboardType = KeyboardType.Number, + ), + isError = + canNumberTextEdited.value && + nfcViewModel.shouldShowCANNumberError(canNumber.text), + errorText = canNumberErrorText, + testTag = "nfcCanNumberTextField", + removeIconTestTag = "nfcCanNumberRemoveIconButton", + descriptionTestTag = "nfcCanNumberLocationText", + errorTestTag = "nfcCanErrorText", ) - if (canNumberErrorText.isNotEmpty()) { - Text( - modifier = - modifier - .padding(top = XSPadding) - .padding(bottom = MSPadding) - .fillMaxWidth() - .focusable(enabled = true) - .semantics { - contentDescription = canNumberErrorText - testTagsAsResourceId = true - }.testTag("nfcCANErrorText"), - text = canNumberErrorText, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, - ) - } + val pinCodeTextEdited = rememberSaveable { mutableStateOf(false) } val pinCodeErrorText = if (pinCodeTextEdited.value && pinCode.value.isNotEmpty()) { @@ -764,83 +655,28 @@ fun NFCView( } if (showPinField) { - Row( + SecurePinTextField( modifier = - modifier - .fillMaxWidth() - .padding(top = XSPadding), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - SecurePinTextField( - modifier = - modifier - .weight(1f) - .semantics { - testTagsAsResourceId = true - }.testTag("nfcPinTextField"), - pin = pinCode, - pinCodeLabel = pinCodeLabel, - pinNumberFocusRequester = pinNumberFocusRequester, - previousFocusRequester = canNumberFocusRequester, - pinCodeTextEdited = pinCodeTextEdited, - trailingIconContentDescription = "$clearButtonText $buttonName", - isError = - pinCodeTextEdited.value && - shouldShowPINCodeError( - pinCode.value, - codeType, - ), - ) - if (isTalkBackEnabled(context) && pinCode.value.isNotEmpty()) { - IconButton( - modifier = - modifier - .align(Alignment.CenterVertically) - .semantics { - traversalIndex = 9f - testTagsAsResourceId = true - }.testTag("nfcPinRemoveButton"), - onClick = { - pinCode.value = byteArrayOf() - scope.launch(Main) { - pinNumberFocusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - pinNumberFocusRequester.requestFocus() - } - }, - ) { - Icon( - modifier = - modifier - .size(iconSizeXXS) - .semantics { - testTagsAsResourceId = true - }.testTag("nfcPinRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - } - - if (pinCodeErrorText.isNotEmpty()) { - Text( - modifier = - modifier - .padding(vertical = XSPadding) - .fillMaxWidth() - .focusable(enabled = true) - .semantics { - contentDescription = pinCodeErrorText - testTagsAsResourceId = true - }.testTag("nfcPinErrorText"), - text = pinCodeErrorText, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, - ) - } + Modifier + .focusRequester(pinNumberFocusRequester) + .focusProperties { + previous = canNumberFocusRequester + }.semantics { + testTagsAsResourceId = true + }.testTag("nfcPinTextField"), + pin = pinCode, + label = pinCodeLabel, + pinCodeTextEdited = pinCodeTextEdited, + isError = + pinCodeTextEdited.value && + shouldShowPINCodeError( + pinCode.value, + codeType, + ), + errorText = pinCodeErrorText, + removeIconTestTag = "nfcPinRemoveButton", + errorTestTag = "nfcPinError", + ) } } } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SmartIdView.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SmartIdView.kt index 3ac94e5ae..6d7f1b6fe 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SmartIdView.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SmartIdView.kt @@ -30,16 +30,13 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.rememberScrollState @@ -48,12 +45,8 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -64,7 +57,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -74,7 +66,6 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag @@ -89,7 +80,6 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.toSize import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.asFlow import ee.ria.DigiDoc.R @@ -98,14 +88,13 @@ import ee.ria.DigiDoc.libdigidoclib.domain.model.RoleData import ee.ria.DigiDoc.ui.component.shared.CancelAndOkButtonRow import ee.ria.DigiDoc.ui.component.shared.HrefMessageDialog import ee.ria.DigiDoc.ui.component.shared.InvisibleElement +import ee.ria.DigiDoc.ui.component.shared.PrimaryTextField import ee.ria.DigiDoc.ui.component.shared.RoleDataView import ee.ria.DigiDoc.ui.component.shared.dialog.OptionChooserDialog import ee.ria.DigiDoc.ui.component.support.textFieldValueSaver -import ee.ria.DigiDoc.ui.theme.Dimensions.MPadding import ee.ria.DigiDoc.ui.theme.Dimensions.MSPadding import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding -import ee.ria.DigiDoc.ui.theme.Dimensions.iconSizeXXS import ee.ria.DigiDoc.ui.theme.RIADigiDocTheme import ee.ria.DigiDoc.ui.theme.buttonRoundCornerShape import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.addInvisibleElement @@ -118,7 +107,6 @@ import ee.ria.DigiDoc.viewmodel.shared.SharedContainerViewModel import ee.ria.DigiDoc.viewmodel.shared.SharedSettingsViewModel import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch @@ -430,37 +418,32 @@ fun SmartIdView( .focusable(false) .fillMaxWidth(), ) { - OutlinedTextField( - label = { - Text(countryTitleText) - }, - value = countryString, - onValueChange = {}, - readOnly = true, - singleLine = true, + PrimaryTextField( modifier = - modifier + Modifier .focusRequester(countryFocusRequester) .focusProperties { next = personalCodeFocusRequester - }.fillMaxWidth() - .onGloballyPositioned { coordinates -> - textFieldSize = coordinates.size.toSize() }, + value = TextFieldValue(text = countryString), + onValueChange = {}, + readOnly = true, + singleLine = true, + label = countryTitleText, trailingIcon = { Icon( imageVector = ImageVector.vectorResource( R.drawable.ic_baseline_keyboard_arrow_down_24, ), - contentDescription = "$countryTitleText $countryString", + contentDescription = countryTitleText, modifier = - modifier - .clickable { - openOptionChooserDialog = !openOptionChooserDialog - }, + modifier.clickable { + openOptionChooserDialog = !openOptionChooserDialog + }, ) }, + testTag = "smartIdCountryChooser", ) if (!openOptionChooserDialog) { @@ -522,160 +505,89 @@ fun SmartIdView( } } - Row( + PrimaryTextField( modifier = - modifier - .fillMaxWidth() - .padding(top = MPadding), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - OutlinedTextField( - enabled = countryString.isNotEmpty(), - label = { - Text(text = personalCodeTitleText) - }, - value = - if (!isTalkBackEnabled(context)) { - personalCode - } else { - personalCodeWithInvisibleSpaces.copy( - selection = TextRange(personalCodeWithInvisibleSpaces.text.length), - ) + Modifier + .padding(top = MSPadding) + .focusRequester(personalCodeFocusRequester) + .focusProperties { + previous = countryFocusRequester }, - singleLine = true, - onValueChange = { newValue -> - if (!isTalkBackEnabled(context)) { - val rawText = newValue.text - val cursorPosition = newValue.selection.start - - // Check if selected country is Latvia - if (selectedCountry == 2) { - val allowedChars = rawText.filter { char -> char.isDigit() || char == '-' } - - if (allowedChars != personalCode.text) { - val (formattedText, cursorPosition) = - smartIdViewModel - .formatLatvianPersonalCode( - allowedChars, - cursorPosition, - personalCode.text, - ) - - personalCode = - TextFieldValue( - text = formattedText, - selection = TextRange(cursorPosition), - ) - } else { - personalCode = - TextFieldValue( - text = allowedChars, - selection = TextRange(minOf(cursorPosition, allowedChars.length)), + value = + if (!isTalkBackEnabled(context)) { + personalCode + } else { + personalCodeWithInvisibleSpaces + }, + onValueChange = { newValue -> + if (!isTalkBackEnabled(context)) { + val rawText = newValue.text + val cursorPosition = newValue.selection.start + + // Check if selected country is Latvia + if (selectedCountry == 2) { + val allowedChars = rawText.filter { char -> char.isDigit() || char == '-' } + + if (allowedChars != personalCode.text) { + val (formattedText, cursorPosition) = + smartIdViewModel + .formatLatvianPersonalCode( + allowedChars, + cursorPosition, + personalCode.text, ) - } - } else { - val cleaned = rawText.filter { char -> char.isDigit() } - val newCursorPosition = minOf(cursorPosition, cleaned.length) personalCode = TextFieldValue( - text = cleaned, - selection = TextRange(newCursorPosition), + text = formattedText, + selection = TextRange(cursorPosition), + ) + } else { + personalCode = + TextFieldValue( + text = allowedChars, + selection = TextRange(minOf(cursorPosition, allowedChars.length)), ) } } else { - val noInvisibleElement = TextFieldValue(removeInvisibleElement(newValue.text)) + val cleaned = rawText.filter { char -> char.isDigit() } + val newCursorPosition = minOf(cursorPosition, cleaned.length) + personalCode = - noInvisibleElement.copy( - selection = TextRange(noInvisibleElement.text.length), + TextFieldValue( + text = cleaned, + selection = TextRange(newCursorPosition), ) } - }, - modifier = - modifier - .focusRequester(personalCodeFocusRequester) - .focusProperties { - previous = countryFocusRequester - }.weight(1f) - .semantics(mergeDescendants = true) { - testTagsAsResourceId = true - }.testTag("smartIdPersonalCodeTextField"), - trailingIcon = { - if (!isTalkBackEnabled(context) && personalCode.text.isNotEmpty()) { - IconButton(onClick = { - personalCode = TextFieldValue("") - }) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) - } - } - }, - placeholder = { - if (selectedCountry == 2) Text("123456-78901") - }, - colors = - OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.primary, - ), - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Done, - keyboardType = - if (selectedCountry == 2) { - KeyboardType.Phone - } else { - KeyboardType.Number - }, - ), - isError = !smartIdViewModel.isPersonalCodeValid(personalCode.text), - ) - if (isTalkBackEnabled(context) && personalCode.text.isNotEmpty()) { - IconButton( - modifier = - modifier - .align(Alignment.CenterVertically), - onClick = { - personalCode = TextFieldValue("") - scope.launch(Main) { - personalCodeFocusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - personalCodeFocusRequester.requestFocus() - } - }, - ) { - Icon( - modifier = - modifier - .size(iconSizeXXS) - .semantics { - testTagsAsResourceId = true - }.testTag("smartIdPersonalCodeRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) + } else { + personalCode = TextFieldValue(removeInvisibleElement(newValue.text)) } - } - } - if (personalCodeErrorText.isNotEmpty()) { - Text( - modifier = - modifier - .padding(top = XSPadding) - .padding(bottom = MSPadding) - .fillMaxWidth() - .focusable(true) - .semantics { contentDescription = personalCodeErrorText } - .testTag("smartIdPersonalCodeErrorText"), - text = personalCodeErrorText, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, - ) - } + }, + singleLine = true, + label = personalCodeTitleText, + placeholder = + if (selectedCountry == 2) { + "123456-78901" + } else { + "" + }, + readDigitByDigit = true, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Done, + keyboardType = + if (selectedCountry == 2) { + KeyboardType.Phone + } else { + KeyboardType.Number + }, + ), + isError = !smartIdViewModel.isPersonalCodeValid(personalCode.text), + errorText = personalCodeErrorText, + testTag = "smartIdPersonalCodeTextField", + removeIconTestTag = "smartIdPersonalCodeRemoveIconButton", + errorTestTag = "smartIdPersonalCodeErrorText", + ) } } }