Fix flicker on TextField autocomplete (#5456)

This commit is contained in:
David Perez
2025-07-02 08:57:00 -05:00
committed by GitHub
parent 79493a55bd
commit eae870cb3a
2 changed files with 61 additions and 12 deletions

View File

@@ -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(

View File

@@ -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.