diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt index 9b1d27d3e7..1902112008 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/field/BitwardenTextField.kt @@ -2,7 +2,6 @@ package com.x8bit.bitwarden.ui.platform.components.field import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues @@ -15,10 +14,13 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable @@ -50,9 +52,9 @@ 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.bitwarden.ui.platform.base.util.cardStyle import com.bitwarden.ui.platform.base.util.nullableTestTag +import com.bitwarden.ui.platform.base.util.simpleVerticalScrollbar import com.bitwarden.ui.platform.base.util.toPx import com.bitwarden.ui.platform.base.util.withLineBreaksAtWidth import com.bitwarden.ui.platform.components.appbar.color.bitwardenMenuItemColors @@ -206,6 +208,7 @@ fun BitwardenTextField( * defining the layout of the actions. */ @Suppress("LongMethod", "CyclomaticComplexMethod") +@OptIn(ExperimentalMaterial3Api::class) @Composable fun BitwardenTextField( label: String?, @@ -269,7 +272,15 @@ fun BitwardenTextField( var lastTextValue by remember(value) { mutableStateOf(value = value) } CompositionLocalProvider(value = LocalTextToolbar provides textToolbar) { var hasFocused by remember { mutableStateOf(value = false) } - Box(modifier = modifier.defaultMinSize(minHeight = 60.dp)) { + val filteredAutoCompleteList = autoCompleteOptions + .filter { it.startsWith(textFieldValue.text) && it != textFieldValue.text } + .toImmutableList() + val isDropDownExpanded = filteredAutoCompleteList.isNotEmpty() && hasFocused + ExposedDropdownMenuBox( + expanded = isDropDownExpanded, + onExpandedChange = { hasFocused = false }, + modifier = modifier.defaultMinSize(minHeight = 60.dp), + ) { Column( modifier = Modifier .onGloballyPositioned { widthPx = it.size.width } @@ -364,6 +375,7 @@ fun BitwardenTextField( visualTransformation = visualTransformation, modifier = Modifier .nullableTestTag(tag = textFieldTestTag) + .menuAnchor(type = MenuAnchorType.PrimaryEditable) .fillMaxWidth() .onFocusChanged { focusState -> focused = focusState.isFocused @@ -387,17 +399,14 @@ fun BitwardenTextField( } ?: Spacer(modifier = Modifier.height(height = cardStyle?.let { 6.dp } ?: 0.dp)) } - val filteredAutoCompleteList = autoCompleteOptions - .filter { option -> - option.startsWith(textFieldValue.text) && option != textFieldValue.text - } - .toImmutableList() - DropdownMenu( - expanded = filteredAutoCompleteList.isNotEmpty() && hasFocused, + val scrollState = rememberScrollState() + ExposedDropdownMenu( + expanded = isDropDownExpanded, shape = BitwardenTheme.shapes.menu, containerColor = BitwardenTheme.colorScheme.background.primary, - properties = PopupProperties(), onDismissRequest = { hasFocused = false }, + scrollState = scrollState, + modifier = Modifier.simpleVerticalScrollbar(state = scrollState), ) { filteredAutoCompleteList.forEach { DropdownMenuItem( diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/base/util/ModifierExtensions.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/base/util/ModifierExtensions.kt index 9c470018a3..a8fbcc4171 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/base/util/ModifierExtensions.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/base/util/ModifierExtensions.kt @@ -3,6 +3,7 @@ package com.bitwarden.ui.platform.base.util import android.os.Build +import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -21,9 +22,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.scale import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.input.key.Key @@ -78,6 +82,42 @@ fun Modifier.scrolledContainerBackground( ) } +/** + * Draws a very simple non-intractable scrollbar on the end side of the component. + */ +@OmitFromCoverage +@Composable +fun Modifier.simpleVerticalScrollbar( + state: ScrollState, + scrollbarWidth: Dp = 6.dp, + color: Color = BitwardenTheme.colorScheme.stroke.divider, + layoutDirection: LayoutDirection = LocalLayoutDirection.current, +): Modifier = + this then Modifier.drawWithContent { + drawContent() + val viewHeight = state.viewportSize.toFloat() + val contentHeight = state.maxValue + viewHeight + val scrollbarHeight = (10.dp.toPx()..viewHeight) + .takeUnless { it.isEmpty() } + ?.let { (viewHeight * (viewHeight / contentHeight)).coerceIn(range = it) } + ?: 0f + val variableZone = viewHeight - scrollbarHeight + val scrollbarYOffset = (state.value.toFloat() / state.maxValue) * variableZone + val halfScrollbarWidthPx = scrollbarWidth.toPx() / 2 + drawRoundRect( + cornerRadius = CornerRadius(x = halfScrollbarWidthPx, y = halfScrollbarWidthPx), + color = color, + topLeft = Offset( + x = when (layoutDirection) { + LayoutDirection.Ltr -> this.size.width - scrollbarWidth.toPx() + LayoutDirection.Rtl -> 0f + }, + y = scrollbarYOffset, + ), + size = Size(width = scrollbarWidth.toPx(), height = scrollbarHeight), + ) + } + /** * Adds a bottom divider specified by the given [topAppBarScrollBehavior] and its current scroll * state.