diff --git a/briar-desktop/src/main/kotlin/androidx/compose/material/TextFieldExt.kt b/briar-desktop/src/main/kotlin/androidx/compose/material/TextFieldExt.kt index f91ff6866348b605e71fc69cdb3541f503bd5fe3..6c06bb0807b614bc4469ba9366f9f920b8d950f0 100644 --- a/briar-desktop/src/main/kotlin/androidx/compose/material/TextFieldExt.kt +++ b/briar-desktop/src/main/kotlin/androidx/compose/material/TextFieldExt.kt @@ -116,27 +116,30 @@ fun TextField( ) } -val KeyEvent.isModifierKeyPressed: Boolean +private val KeyEvent.isModifierKeyPressed: Boolean get() = isShiftPressed || isAltPressed || isMetaPressed || isCtrlPressed -fun TextFieldValue.insertOrReplaceBy(replacement: CharSequence) = this.copy( +private fun TextFieldValue.insertOrReplaceBy(replacement: CharSequence) = this.copy( text = text.replaceRange(selection.min, selection.max, replacement), selection = TextRange(selection.min + 1, selection.min + 1) ) -// tab navigation for multi-line TextField is broken upstream -// see https://github.com/JetBrains/compose-jb/issues/109 -@OptIn(ExperimentalComposeUiApi::class) -@Composable -fun Modifier.moveFocusOnTab( - focusManager: FocusManager = LocalFocusManager.current -) = onPreviewKeyEvent { - if (it.type == KeyEventType.KeyDown && it.key == Key.Tab) { - focusManager.moveFocus( - if (it.isShiftPressed) FocusDirection.Previous - else FocusDirection.Next - ) - return@onPreviewKeyEvent true +object TextFieldExt { + + // tab navigation for multi-line TextField is broken upstream + // see https://github.com/JetBrains/compose-jb/issues/109 + @OptIn(ExperimentalComposeUiApi::class) + @Composable + fun Modifier.moveFocusOnTab( + focusManager: FocusManager = LocalFocusManager.current + ) = onPreviewKeyEvent { + if (it.type == KeyEventType.KeyDown && it.key == Key.Tab) { + focusManager.moveFocus( + if (it.isShiftPressed) FocusDirection.Previous + else FocusDirection.Next + ) + return@onPreviewKeyEvent true + } + false } - false } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/SearchTextField.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/SearchTextField.kt index 1d32ffc4ca6c1039454f5bac0b17d6dc908e83d8..cbfa8141ef5e7afc5e69408b09763e246320da71 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/SearchTextField.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/SearchTextField.kt @@ -33,6 +33,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import org.briarproject.briar.desktop.ui.ColoredIconButton +import org.briarproject.briar.desktop.utils.AccessibilityUtils.description import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n @Composable @@ -48,7 +49,7 @@ fun SearchTextField(searchValue: String, onValueChange: (String) -> Unit, onCont shape = RoundedCornerShape(0.dp), leadingIcon = { val padding = Modifier.padding(top = 8.dp, bottom = 8.dp, start = 32.dp, end = 4.dp) - Icon(Icons.Filled.Search, i18n("access.contacts.search"), padding) + Icon(Icons.Filled.Search, null, padding) }, trailingIcon = { ColoredIconButton( @@ -59,6 +60,6 @@ fun SearchTextField(searchValue: String, onValueChange: (String) -> Unit, onCont modifier = Modifier.padding(end = 8.dp) ) }, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize().description(i18n("access.contacts.filter")) ) } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt index aab5f86a7dc9555cfee3de2c04009555f84caadd..e020c1822d62748205119f4cac6a809a36ee5015 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/contact/add/remote/AddContactDialog.kt @@ -96,6 +96,7 @@ import org.briarproject.briar.desktop.theme.Orange500 import org.briarproject.briar.desktop.theme.Red500 import org.briarproject.briar.desktop.theme.surfaceVariant import org.briarproject.briar.desktop.ui.Constants.DIALOG_WIDTH +import org.briarproject.briar.desktop.utils.AccessibilityUtils.description import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18nF import org.briarproject.briar.desktop.utils.PreviewUtils @@ -323,7 +324,7 @@ fun OwnLink( duration = SnackbarDuration.Short, ) } - } + }.description(i18n("contact.add.remote.your_link_hint")) ) { Text( handshakeLink, @@ -384,7 +385,7 @@ fun ContactLink( remoteHandshakeLink, setRemoteHandshakeLink, label = { Text(i18n("contact.add.remote.contact_link_hint")) }, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().description(i18n("contact.add.remote.contact_link_hint")), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Next), singleLine = true, onEnter = { aliasFocusRequester.requestFocus() }, @@ -443,7 +444,8 @@ fun Alias( alias, setAddContactAlias, label = { Text(i18n("contact.add.remote.choose_nickname")) }, - modifier = Modifier.fillMaxWidth().focusRequester(aliasFocusRequester), + modifier = Modifier.fillMaxWidth().focusRequester(aliasFocusRequester) + .description(i18n("contact.add.remote.choose_nickname")), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), singleLine = true, onEnter = onSubmitAddContactDialog, diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationInput.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationInput.kt index 1787696cd4793c265fb8b2d21a18b104907d6969..6c11e914aafe51f517ae0c29db65720adb910a59 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationInput.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/conversation/ConversationInput.kt @@ -32,11 +32,11 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.TextFieldExt.moveFocusOnTab import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Send -import androidx.compose.material.moveFocusOnTab import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt index 3085a284c2272bc683f112eb80f0ed21b144b5ee..79cf12487ca6738605616121d6ee994d680c7407 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/login/LoginScreen.kt @@ -45,6 +45,7 @@ import org.briarproject.briar.desktop.login.LoginSubViewModel.State.MIGRATING import org.briarproject.briar.desktop.login.LoginSubViewModel.State.SIGNED_OUT import org.briarproject.briar.desktop.login.LoginSubViewModel.State.STARTED import org.briarproject.briar.desktop.login.LoginSubViewModel.State.STARTING +import org.briarproject.briar.desktop.utils.AccessibilityUtils.description import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n @OptIn(ExperimentalMaterialApi::class) @@ -118,7 +119,8 @@ fun LoginForm( isError = passwordInvalidError, errorMessage = i18n("startup.error.password_wrong"), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Next), - modifier = Modifier.fillMaxWidth().focusRequester(initialFocusRequester), + modifier = Modifier.fillMaxWidth().focusRequester(initialFocusRequester) + .description(i18n("startup.field.password")), onEnter = onEnter ) TextButton(onClick = { passwordForgotten.value = true }) { diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationScreen.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationScreen.kt index a92db09023873b50143226db6a3ca8594a977e54..271a398666713f45f71a83735a1c37cd151d7c72 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationScreen.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/login/RegistrationScreen.kt @@ -44,6 +44,7 @@ import org.briarproject.briar.desktop.login.RegistrationSubViewModel.State.CREAT import org.briarproject.briar.desktop.login.RegistrationSubViewModel.State.CREATING import org.briarproject.briar.desktop.login.RegistrationSubViewModel.State.INSERT_NICKNAME import org.briarproject.briar.desktop.login.RegistrationSubViewModel.State.INSERT_PASSWORD +import org.briarproject.briar.desktop.utils.AccessibilityUtils.description import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n @Composable @@ -121,7 +122,8 @@ fun NicknameForm( isError = nicknameTooLongError, errorMessage = i18n("startup.error.name_too_long"), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), - modifier = Modifier.fillMaxWidth().focusRequester(initialFocusRequester), + modifier = Modifier.fillMaxWidth().focusRequester(initialFocusRequester) + .description(i18n("startup.field.nickname")), onEnter = onSubmit ) @@ -168,7 +170,7 @@ fun PasswordForm( keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Next), modifier = Modifier.fillMaxWidth().run { if (focusRequester != null) focusRequester(focusRequester) else this - }, + }.description(i18n(keyLabelPassword)), onEnter = { focusManager.moveFocus(FocusDirection.Next) }, ) OutlinedPasswordTextField( @@ -180,7 +182,7 @@ fun PasswordForm( showErrorWhen = AFTER_FIRST_FOCUSSED, errorMessage = i18n("startup.error.passwords_not_match"), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done), - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().description(i18n(keyLabelPasswordConfirmation)), onEnter = onSubmit, ) } diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/ChangePasswordDialog.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/ChangePasswordDialog.kt index afe98b3c5d6af23945ab8e4f8115f3f3b4ef3ddb..e972bf95884c6fa8cd4e8168cc9ff2fa657d28df 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/ChangePasswordDialog.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/settings/ChangePasswordDialog.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.text.input.KeyboardType import org.briarproject.bramble.api.crypto.DecryptionResult import org.briarproject.bramble.api.crypto.DecryptionResult.INVALID_PASSWORD import org.briarproject.briar.desktop.login.PasswordForm +import org.briarproject.briar.desktop.utils.AccessibilityUtils.description import org.briarproject.briar.desktop.utils.InternationalizationUtils.i18n import org.briarproject.briar.desktop.utils.PreviewUtils.preview import java.lang.Float.min @@ -216,7 +217,8 @@ fun PasswordForm( showErrorWhen = InitialFocusState.FROM_START, errorMessage = i18n("startup.error.password_wrong"), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Next), - modifier = Modifier.fillMaxWidth().focusRequester(initialFocusRequester), + modifier = Modifier.fillMaxWidth().focusRequester(initialFocusRequester) + .description(i18n("settings.security.password.current")), onEnter = { focusManager.moveFocus(FocusDirection.Next) }, ) PasswordForm( diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/AccessibilityUtils.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/AccessibilityUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..33d638451c515a0ac4231c6016491cc08ce2196a --- /dev/null +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/utils/AccessibilityUtils.kt @@ -0,0 +1,33 @@ +/* + * Briar Desktop + * Copyright (C) 2021-2022 The Briar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package org.briarproject.briar.desktop.utils + +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics + +object AccessibilityUtils { + /** + * Add a content description for accessibility services. + */ + // Currently, mainly used as a work-around for poor Compose accessibility support, + // see, e.g., https://github.com/JetBrains/compose-jb/issues/2136 + fun Modifier.description(description: String) = + semantics { contentDescription = description } +} diff --git a/briar-desktop/src/main/resources/strings/BriarDesktop.properties b/briar-desktop/src/main/resources/strings/BriarDesktop.properties index d4f2c7da2a33bce5ca73011fe0e9e1d2ce4d070d..0586f6f815ec43e8f8be53da492969986a6ae05b 100644 --- a/briar-desktop/src/main/resources/strings/BriarDesktop.properties +++ b/briar-desktop/src/main/resources/strings/BriarDesktop.properties @@ -5,7 +5,7 @@ access.contacts.add=Add contact access.contact.menu=Show contact menu access.contacts.dropdown.connections.expand=Expand connection menu access.contacts.dropdown.contacts.expand=Expand contact menu -access.contacts.search=Icon for searching contacts +access.contacts.filter=Filter existing contacts by name or alias and add new contacts access.contacts.pending.remove=Remove pending contact access.introduction.back.contact=Go back to contact screen of introduction process access.introduction.close=Close introduction screen @@ -95,6 +95,7 @@ introduction.response.declined.received_by_introducee={0} says that {1} declined contact.add.title_dialog=Add Contact contact.add.remote.title=Add Contact at a Distance contact.add.remote.your_link=Give this link to the contact you want to add +contact.add.remote.your_link_hint=Own link contact.add.remote.copy_tooltip=Copy contact.add.remote.contact_link=Enter the link from your contact here contact.add.remote.contact_link_hint=Contact\'s link