diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentScreen.kt index 5ea32514fa..e7985551a9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.x8bit.bitwarden.BuildConfig import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar @@ -35,6 +36,7 @@ import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter +import kotlinx.collections.immutable.persistentListOf /** * Displays the about self-hosted/custom environment screen. @@ -115,6 +117,15 @@ fun EnvironmentScreen( onValueChange = remember(viewModel) { { viewModel.trySendAction(EnvironmentAction.ServerUrlChange(it)) } }, + autoCompleteOptions = if (BuildConfig.BUILD_TYPE != "release") { + persistentListOf( + "https://vault.qa.bitwarden.pw", + "https://qa-team.sh.bitwarden.pw", + "https://vault.usdev.bitwarden.pw", + ) + } else { + persistentListOf() + }, keyboardType = KeyboardType.Uri, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt index 00adea0f9c..1b12a0f153 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt @@ -1,6 +1,10 @@ package com.x8bit.bitwarden.ui.platform.components.field +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -16,6 +20,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalTextToolbar @@ -26,14 +31,19 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.PopupProperties import com.x8bit.bitwarden.ui.platform.base.util.toPx import com.x8bit.bitwarden.ui.platform.base.util.withLineBreaksAtWidth +import com.x8bit.bitwarden.ui.platform.components.appbar.color.bitwardenMenuItemColors import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors import com.x8bit.bitwarden.ui.platform.components.field.toolbar.BitwardenCutCopyTextToolbar import com.x8bit.bitwarden.ui.platform.components.field.toolbar.BitwardenEmptyTextToolbar import com.x8bit.bitwarden.ui.platform.components.model.IconResource import com.x8bit.bitwarden.ui.platform.components.model.TextToolbarType import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList /** * Component that allows the user to input text. This composable will manage the state of @@ -79,6 +89,7 @@ fun BitwardenTextField( autoFocus: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, textToolbarType: TextToolbarType = TextToolbarType.DEFAULT, + autoCompleteOptions: ImmutableList = persistentListOf(), ) { var widthPx by remember { mutableIntStateOf(0) } val focusRequester = remember { FocusRequester() } @@ -112,54 +123,80 @@ fun BitwardenTextField( } var lastTextValue by remember(value) { mutableStateOf(value = value) } CompositionLocalProvider(value = LocalTextToolbar provides textToolbar) { - OutlinedTextField( - colors = bitwardenTextFieldColors(), - modifier = modifier - .onGloballyPositioned { widthPx = it.size.width } - .focusRequester(focusRequester), - enabled = enabled, - label = { Text(text = label) }, - value = textFieldValue, - leadingIcon = leadingIconResource?.let { iconResource -> - { - Icon( - painter = iconResource.iconPainter, - contentDescription = iconResource.contentDescription, + var hasFocused by remember { mutableStateOf(value = false) } + Box(modifier = modifier) { + OutlinedTextField( + colors = bitwardenTextFieldColors(), + modifier = Modifier + .onGloballyPositioned { widthPx = it.size.width } + .onFocusEvent { focusState -> hasFocused = focusState.hasFocus } + .focusRequester(focusRequester) + .fillMaxWidth(), + enabled = enabled, + label = { Text(text = label) }, + value = textFieldValue, + leadingIcon = leadingIconResource?.let { iconResource -> + { + Icon( + painter = iconResource.iconPainter, + contentDescription = iconResource.contentDescription, + ) + } + }, + trailingIcon = trailingIconContent, + placeholder = placeholder?.let { + { + Text( + text = it, + style = textStyle, + ) + } + }, + supportingText = hint?.let { + { + Text( + text = hint, + style = BitwardenTheme.typography.bodySmall, + ) + } + }, + onValueChange = { + hasFocused = true + textFieldValueState = it + val stringChangedSinceLastInvocation = lastTextValue != it.text + lastTextValue = it.text + if (stringChangedSinceLastInvocation) { + onValueChange(it.text) + } + }, + singleLine = singleLine, + readOnly = readOnly, + textStyle = textStyle, + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = keyboardType), + isError = isError, + visualTransformation = visualTransformation, + ) + val filteredAutoCompleteList = autoCompleteOptions + .filter { option -> + option.startsWith(textFieldValue.text) && option != textFieldValue.text + } + .toImmutableList() + DropdownMenu( + expanded = filteredAutoCompleteList.isNotEmpty() && hasFocused, + shape = BitwardenTheme.shapes.menu, + containerColor = BitwardenTheme.colorScheme.background.primary, + properties = PopupProperties(), + onDismissRequest = { hasFocused = false }, + ) { + filteredAutoCompleteList.forEach { + DropdownMenuItem( + colors = bitwardenMenuItemColors(), + text = { Text(text = it, style = textStyle) }, + onClick = { onValueChange(it) }, ) } - }, - trailingIcon = trailingIconContent, - placeholder = placeholder?.let { - { - Text( - text = it, - style = textStyle, - ) - } - }, - supportingText = hint?.let { - { - Text( - text = hint, - style = BitwardenTheme.typography.bodySmall, - ) - } - }, - onValueChange = { - textFieldValueState = it - val stringChangedSinceLastInvocation = lastTextValue != it.text - lastTextValue = it.text - if (stringChangedSinceLastInvocation) { - onValueChange(it.text) - } - }, - singleLine = singleLine, - readOnly = readOnly, - textStyle = textStyle, - keyboardOptions = KeyboardOptions.Default.copy(keyboardType = keyboardType), - isError = isError, - visualTransformation = visualTransformation, - ) + } + } } if (autoFocus) { LaunchedEffect(Unit) { focusRequester.requestFocus() }