chore: Minor cleanup of the BitwardenTextField and BitwardenPasswordField (#7427)

This commit is contained in:
David Perez authored and GitHub committed 2026-09-28 16:32:57 +00:00
1 parent d999e25461
commit 8ea455d910
6 files changed
+241 -175

No files matched your search

@@ -1,18 +1,14 @@
package com.bitwarden.ui.platform.components.field
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
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.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
@@ -26,13 +22,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
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.platform.LocalClipboard
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalTextToolbar
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.res.stringResource
@@ -45,17 +37,15 @@ import androidx.compose.ui.unit.dp
import com.bitwarden.ui.platform.base.util.cardStyle
import com.bitwarden.ui.platform.base.util.nullableTestTag
import com.bitwarden.ui.platform.base.util.tabNavigation
import com.bitwarden.ui.platform.components.button.BitwardenHelpIconButton
import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.bitwarden.ui.platform.components.button.model.BitwardenHelpButtonData
import com.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors
import com.bitwarden.ui.platform.components.field.label.BitwardenTextFieldLabel
import com.bitwarden.ui.platform.components.field.model.TextToolbarType
import com.bitwarden.ui.platform.components.field.toolbar.BitwardenCutCopyTextToolbar
import com.bitwarden.ui.platform.components.field.toolbar.BitwardenEmptyTextToolbar
import com.bitwarden.ui.platform.components.field.support.BitwardenTextFieldSupportingContent
import com.bitwarden.ui.platform.components.field.util.toTextToolbar
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.row.BitwardenRowOfActions
import com.bitwarden.ui.platform.components.support.BitwardenSupportingContent
import com.bitwarden.ui.platform.components.util.nonLetterColorVisualTransformation
import com.bitwarden.ui.platform.components.util.passwordVisualTransformation
import com.bitwarden.ui.platform.resource.BitwardenDrawable
@@ -75,6 +65,8 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme
* @param supportingContent An optional supporting content that will appear below the text input.
* @param supportingContentPadding The padding to be placed on the [supportingContent].
* @param modifier Modifier for the composable.
* @param interactionSource A [MutableInteractionSource] for observing and emitting interactions
* for this component.
* @param helpData An optional help button to be displayed in the label.
* @param readOnly `true` if the input should be read-only and not accept user interactions.
* @param singleLine when `true`, this text field becomes a single line that horizontally scrolls
@@ -105,6 +97,7 @@ fun BitwardenPasswordField(
supportingContent: @Composable (ColumnScope.() -> Unit)?,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
helpData: BitwardenHelpButtonData? = null,
readOnly: Boolean = false,
singleLine: Boolean = true,
@@ -129,18 +122,12 @@ fun BitwardenPasswordField(
textFieldValueState = textFieldValue
}
}
val textToolbar = when (textToolbarType) {
TextToolbarType.DEFAULT -> BitwardenCutCopyTextToolbar(
value = textFieldValue,
onValueChange = onValueChange,
defaultTextToolbar = LocalTextToolbar.current,
clipboardManager = LocalClipboard.current.nativeClipboard,
focusManager = LocalFocusManager.current,
)
TextToolbarType.NONE -> BitwardenEmptyTextToolbar
}
val textToolbar = textToolbarType.toTextToolbar(
textFieldValue = textFieldValue,
onValueChange = onValueChange,
)
var lastTextValue by remember(value) { mutableStateOf(value = value) }
val isFocused by interactionSource.collectIsFocusedAsState()
CompositionLocalProvider(value = LocalTextToolbar provides textToolbar) {
Column(
modifier = modifier
@@ -150,34 +137,19 @@ fun BitwardenPasswordField(
paddingTop = 6.dp,
paddingBottom = 0.dp,
)
.tabNavigation()
.focusRequester(focusRequester = focusRequester),
.tabNavigation(),
) {
var focused by remember { mutableStateOf(value = false) }
TextField(
colors = bitwardenTextFieldColors(),
textStyle = BitwardenTheme.typography.sensitiveInfoSmall,
label = label?.let {
{
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = it)
helpData?.let { helpButtonData ->
val targetSize = if (textFieldValue.text.isEmpty() || focused) {
16.dp
} else {
12.dp
}
val size by animateDpAsState(
targetValue = targetSize,
label = "${helpButtonData.contentDescription}_animation",
)
Spacer(modifier = Modifier.width(width = 8.dp))
BitwardenHelpIconButton(
helpData = helpButtonData,
modifier = Modifier.size(size = size),
)
}
}
BitwardenTextFieldLabel(
label = it,
textFieldValue = textFieldValue,
helpData = helpData,
isFieldFocused = isFocused,
)
}
},
value = textFieldValue,
@@ -228,26 +200,17 @@ fun BitwardenPasswordField(
},
)
},
interactionSource = interactionSource,
modifier = Modifier
.nullableTestTag(tag = passwordFieldTestTag)
.fillMaxWidth()
.onFocusChanged { focusState -> focused = focusState.isFocused },
.focusRequester(focusRequester = focusRequester),
)
BitwardenTextFieldSupportingContent(
content = supportingContent,
supportingContentPadding = supportingContentPadding,
noContentCardPadding = 6.dp,
)
supportingContent
?.let { content ->
Spacer(modifier = Modifier.height(height = 6.dp))
BitwardenHorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
)
BitwardenSupportingContent(
cardStyle = null,
insets = supportingContentPadding,
content = content,
)
}
?: Spacer(modifier = Modifier.height(height = 6.dp))
}
}
if (autoFocus) {
@@ -266,6 +229,8 @@ fun BitwardenPasswordField(
* @param showPasswordChange Lambda that is called when user request show/hide be toggled.
* @param onValueChange Callback that is triggered when the password changes.
* @param modifier Modifier for the composable.
* @param interactionSource A [MutableInteractionSource] for observing and emitting interactions
* for this component.
* @param helpData An optional help button to be displayed in the label.
* @param readOnly `true` if the input should be read-only and not accept user interactions.
* @param singleLine when `true`, this text field becomes a single line that horizontally scrolls
@@ -295,6 +260,7 @@ fun BitwardenPasswordField(
onValueChange: (String) -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
helpData: BitwardenHelpButtonData? = null,
readOnly: Boolean = false,
singleLine: Boolean = true,
@@ -317,6 +283,7 @@ fun BitwardenPasswordField(
showPasswordTestTag = showPasswordTestTag,
onValueChange = onValueChange,
modifier = modifier,
interactionSource = interactionSource,
helpData = helpData,
readOnly = readOnly,
singleLine = singleLine,
@@ -351,6 +318,8 @@ fun BitwardenPasswordField(
* @param value Current next on the text field.
* @param onValueChange Callback that is triggered when the password changes.
* @param modifier Modifier for the composable.
* @param interactionSource A [MutableInteractionSource] for observing and emitting interactions
* for this component.
* @param helpData An optional help button to be displayed in the label.
* @param initialShowPassword The initial state of the show/hide password control. A value of
* `false` (the default) indicates that that password should begin in the hidden state.
@@ -382,6 +351,7 @@ fun BitwardenPasswordField(
supportingContent: @Composable (ColumnScope.() -> Unit)?,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
helpData: BitwardenHelpButtonData? = null,
initialShowPassword: Boolean = false,
readOnly: Boolean = false,
@@ -406,6 +376,7 @@ fun BitwardenPasswordField(
showPasswordTestTag = showPasswordTestTag,
onValueChange = onValueChange,
modifier = modifier,
interactionSource = interactionSource,
helpData = helpData,
readOnly = readOnly,
singleLine = singleLine,
@@ -431,6 +402,8 @@ fun BitwardenPasswordField(
* @param value Current next on the text field.
* @param onValueChange Callback that is triggered when the password changes.
* @param modifier Modifier for the composable.
* @param interactionSource A [MutableInteractionSource] for observing and emitting interactions
* for this component.
* @param helpData An optional help button to be displayed in the label.
* @param readOnly `true` if the input should be read-only and not accept user interactions.
* @param singleLine when `true`, this text field becomes a single line that horizontally scrolls
@@ -459,6 +432,7 @@ fun BitwardenPasswordField(
onValueChange: (String) -> Unit,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
helpData: BitwardenHelpButtonData? = null,
readOnly: Boolean = false,
singleLine: Boolean = true,
@@ -481,6 +455,7 @@ fun BitwardenPasswordField(
showPassword = showPassword,
showPasswordChange = { showPassword = !showPassword },
onValueChange = onValueChange,
interactionSource = interactionSource,
helpData = helpData,
readOnly = readOnly,
singleLine = singleLine,
@@ -1,18 +1,15 @@
package com.bitwarden.ui.platform.components.field
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
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
@@ -20,6 +17,7 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuBoxScope
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
@@ -31,21 +29,14 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.focus.onFocusEvent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalTextToolbar
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
@@ -60,18 +51,16 @@ 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
import com.bitwarden.ui.platform.components.button.BitwardenHelpIconButton
import com.bitwarden.ui.platform.components.button.model.BitwardenHelpButtonData
import com.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors
import com.bitwarden.ui.platform.components.field.label.BitwardenTextFieldLabel
import com.bitwarden.ui.platform.components.field.model.TextToolbarType
import com.bitwarden.ui.platform.components.field.toolbar.BitwardenCutCopyTextToolbar
import com.bitwarden.ui.platform.components.field.toolbar.BitwardenEmptyTextToolbar
import com.bitwarden.ui.platform.components.field.support.BitwardenTextFieldSupportingContent
import com.bitwarden.ui.platform.components.field.util.toTextToolbar
import com.bitwarden.ui.platform.components.icon.BitwardenIcon
import com.bitwarden.ui.platform.components.icon.model.IconData
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.row.BitwardenRowOfActions
import com.bitwarden.ui.platform.components.support.BitwardenSupportingContent
import com.bitwarden.ui.platform.theme.BitwardenTheme
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -84,6 +73,8 @@ import kotlinx.collections.immutable.toImmutableList
* @param label label for the text field.
* @param value current next on the text field.
* @param modifier modifier for the composable.
* @param interactionSource A [MutableInteractionSource] for observing and emitting interactions
* for this component.
* @param onValueChange callback that is triggered when the input of the text field changes.
* @param helpData An optional help button to be displayed in the label.
* @param placeholder the optional placeholder to be displayed when the text field is in focus and
@@ -118,6 +109,7 @@ fun BitwardenTextField(
onValueChange: (String) -> Unit,
cardStyle: CardStyle?,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
helpData: BitwardenHelpButtonData? = null,
placeholder: String? = null,
leadingIconData: IconData? = null,
@@ -146,6 +138,7 @@ fun BitwardenTextField(
label = label,
value = value,
onValueChange = onValueChange,
interactionSource = interactionSource,
helpData = helpData,
placeholder = placeholder,
leadingIconData = leadingIconData,
@@ -188,6 +181,8 @@ fun BitwardenTextField(
* @param label label for the text field.
* @param value current next on the text field.
* @param modifier modifier for the composable.
* @param interactionSource A [MutableInteractionSource] for observing and emitting interactions
* for this component.
* @param helpData An optional help button to be displayed in the label.
* @param onValueChange callback that is triggered when the input of the text field changes.
* @param supportingContent An optional supporting content composable that will appear below the
@@ -217,8 +212,8 @@ fun BitwardenTextField(
* in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in
* defining the layout of the actions.
*/
@Suppress("LongMethod", "CyclomaticComplexMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun BitwardenTextField(
label: String?,
@@ -227,6 +222,7 @@ fun BitwardenTextField(
supportingContent: (@Composable ColumnScope.() -> Unit)?,
cardStyle: CardStyle?,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
helpData: BitwardenHelpButtonData? = null,
supportingContentPadding: PaddingValues = PaddingValues(vertical = 12.dp, horizontal = 16.dp),
placeholder: String? = null,
@@ -254,7 +250,7 @@ fun BitwardenTextField(
val focusRequester = remember { FocusRequester() }
val formattedText = if (shouldAddCustomLineBreaks) {
value.withLineBreaksAtWidth(
// Adjust for built in padding
// Adjust for built-in padding
widthPx = widthPx - 32.dp.toPx(),
monospacedTextStyle = textStyle,
)
@@ -270,82 +266,45 @@ fun BitwardenTextField(
textFieldValueState = textFieldValue
}
}
val textToolbar = when (textToolbarType) {
TextToolbarType.DEFAULT -> BitwardenCutCopyTextToolbar(
value = textFieldValue,
onValueChange = onValueChange,
defaultTextToolbar = LocalTextToolbar.current,
clipboardManager = LocalClipboard.current.nativeClipboard,
focusManager = LocalFocusManager.current,
)
TextToolbarType.NONE -> BitwardenEmptyTextToolbar
}
val textToolbar = textToolbarType.toTextToolbar(
textFieldValue = textFieldValue,
onValueChange = onValueChange,
)
var lastTextValue by remember(value) { mutableStateOf(value = value) }
val isFocused by interactionSource.collectIsFocusedAsState()
CompositionLocalProvider(value = LocalTextToolbar provides textToolbar) {
var hasFocused by remember { mutableStateOf(value = false) }
val filteredAutoCompleteList = autoCompleteOptions
.filter { it.startsWith(textFieldValue.text) && it != textFieldValue.text }
.toImmutableList()
val isDropDownExpanded = filteredAutoCompleteList.isNotEmpty() && hasFocused
val isDropDownExpanded = filteredAutoCompleteList.isNotEmpty() && isFocused
ExposedDropdownMenuBox(
expanded = isDropDownExpanded,
onExpandedChange = {
hasFocused = !hasFocused
focusRequester.requestFocus()
// The expanded state is managed via focus and if we have options to display.
},
modifier = modifier.defaultMinSize(minHeight = 60.dp),
) {
Column(
modifier = Modifier
.onGloballyPositioned { widthPx = it.size.width }
.onFocusEvent { focusState -> hasFocused = focusState.hasFocus }
.cardStyle(
cardStyle = cardStyle,
paddingTop = 6.dp,
paddingBottom = 0.dp,
)
.fillMaxWidth()
.semantics {
customActions = listOfNotNull(
helpData?.let {
CustomAccessibilityAction(
label = it.contentDescription,
action = {
it.onClick()
true
},
)
},
)
},
.fillMaxWidth(),
) {
var focused by remember { mutableStateOf(false) }
TextField(
colors = bitwardenTextFieldColors(textColor = textColor),
enabled = enabled,
label = label?.let {
{
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = it)
helpData?.let { helpButtonData ->
val targetSize = if (textFieldValue.text.isEmpty() || focused) {
16.dp
} else {
12.dp
}
val size by animateDpAsState(
targetValue = targetSize,
label = "${helpButtonData.contentDescription}_animation",
)
Spacer(modifier = Modifier.width(width = 8.dp))
BitwardenHelpIconButton(
helpData = helpButtonData,
modifier = Modifier.size(size = size),
)
}
}
BitwardenTextFieldLabel(
label = it,
textFieldValue = textFieldValue,
helpData = helpData,
isFieldFocused = isFocused,
)
}
},
value = textFieldValue,
@@ -359,7 +318,6 @@ fun BitwardenTextField(
},
placeholder = placeholder?.let { { Text(text = it, style = textStyle) } },
onValueChange = {
hasFocused = true
textFieldValueState = it
val stringChangedSinceLastInvocation = lastTextValue != it.text
lastTextValue = it.text
@@ -387,6 +345,7 @@ fun BitwardenTextField(
},
isError = isError,
visualTransformation = visualTransformation,
interactionSource = interactionSource,
modifier = Modifier
.nullableTestTag(tag = textFieldTestTag)
.menuAnchor(
@@ -394,49 +353,27 @@ fun BitwardenTextField(
enabled = false,
)
.fillMaxWidth()
.focusRequester(focusRequester)
.focusRequester(focusRequester = focusRequester)
.onFocusChanged { focusState ->
focused = focusState.isFocused
if (focused) {
if (focusState.isFocused) {
textFieldValueState = textFieldValueState.copy(
selection = TextRange(textFieldValueState.text.length),
)
}
},
)
supportingContent
?.let { content ->
Spacer(modifier = Modifier.height(height = 6.dp))
BitwardenHorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
)
BitwardenSupportingContent(
cardStyle = null,
insets = supportingContentPadding,
content = content,
)
}
?: Spacer(modifier = Modifier.height(height = cardStyle?.let { 6.dp } ?: 0.dp))
}
val scrollState = rememberScrollState()
ExposedDropdownMenu(
expanded = isDropDownExpanded,
shape = BitwardenTheme.shapes.menu,
containerColor = BitwardenTheme.colorScheme.background.primary,
onDismissRequest = { hasFocused = false },
scrollState = scrollState,
modifier = Modifier.simpleVerticalScrollbar(state = scrollState),
) {
filteredAutoCompleteList.forEach {
DropdownMenuItem(
colors = bitwardenMenuItemColors(),
text = { Text(text = it, style = textStyle) },
onClick = { onValueChange(it) },
)
}
BitwardenTextFieldSupportingContent(
content = supportingContent,
supportingContentPadding = supportingContentPadding,
noContentCardPadding = cardStyle?.let { 6.dp } ?: 0.dp,
)
}
AutocompleteDropDownMenu(
isExpanded = isDropDownExpanded,
autoCompleteList = filteredAutoCompleteList,
onValueClicked = onValueChange,
textStyle = textStyle,
)
}
}
if (autoFocus) {
@@ -444,6 +381,36 @@ fun BitwardenTextField(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ExposedDropdownMenuBoxScope.AutocompleteDropDownMenu(
isExpanded: Boolean,
autoCompleteList: ImmutableList<String>,
onValueClicked: (String) -> Unit,
textStyle: TextStyle,
modifier: Modifier = Modifier,
scrollState: ScrollState = rememberScrollState(),
) {
ExposedDropdownMenu(
expanded = isExpanded,
shape = BitwardenTheme.shapes.menu,
containerColor = BitwardenTheme.colorScheme.background.primary,
onDismissRequest = {
// This cannot be dismissed while the TextField has focus.
},
scrollState = scrollState,
modifier = modifier.simpleVerticalScrollbar(state = scrollState),
) {
autoCompleteList.forEach {
DropdownMenuItem(
colors = bitwardenMenuItemColors(),
text = { Text(text = it, style = textStyle) },
onClick = { onValueClicked(it) },
)
}
}
}
@Preview
@Composable
private fun BitwardenTextField_preview() {
@@ -2,7 +2,6 @@ package com.bitwarden.ui.platform.components.field.interceptor
import android.view.inputmethod.EditorInfo
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.platform.InterceptPlatformTextInput
import androidx.compose.ui.platform.PlatformTextInputInterceptor
import androidx.compose.ui.platform.PlatformTextInputMethodRequest
@@ -11,8 +10,7 @@ import androidx.compose.ui.platform.PlatformTextInputSession
/**
* Interceptor that disables the [EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING] flag on text inputs.
*/
@OptIn(ExperimentalComposeUiApi::class)
object NoPersonalizedLearningInterceptor : PlatformTextInputInterceptor {
private object NoPersonalizedLearningInterceptor : PlatformTextInputInterceptor {
override suspend fun interceptStartInputMethod(
request: PlatformTextInputMethodRequest,
nextHandler: PlatformTextInputSession,
@@ -35,7 +33,6 @@ object NoPersonalizedLearningInterceptor : PlatformTextInputInterceptor {
* This is a modified version of the workaround provided by Google here:
* https://issuetracker.google.com/issues/359257538#comment2
*/
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun IncognitoInput(content: @Composable () -> Unit) {
InterceptPlatformTextInput(
@@ -0,0 +1,51 @@
package com.bitwarden.ui.platform.components.field.label
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.platform.components.button.BitwardenHelpIconButton
import com.bitwarden.ui.platform.components.button.model.BitwardenHelpButtonData
import com.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.bitwarden.ui.platform.components.field.BitwardenTextField
/**
* Represents a Bitwarden-styled label for a text field.
*
* Intended to be reused for all implementations of a TextField, such as the [BitwardenTextField]
* and [BitwardenPasswordField].
*/
@Composable
internal fun BitwardenTextFieldLabel(
label: String,
textFieldValue: TextFieldValue,
helpData: BitwardenHelpButtonData?,
isFieldFocused: Boolean,
modifier: Modifier = Modifier,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
Text(text = label)
helpData?.let { helpButtonData ->
val size by animateDpAsState(
targetValue = if (textFieldValue.text.isEmpty() || isFieldFocused) 16.dp else 12.dp,
label = "${helpButtonData.contentDescription}_animation",
)
Spacer(modifier = Modifier.width(width = 8.dp))
BitwardenHelpIconButton(
helpData = helpButtonData,
modifier = Modifier.size(size = size),
)
}
}
}
@@ -0,0 +1,46 @@
package com.bitwarden.ui.platform.components.field.support
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.bitwarden.ui.platform.components.field.BitwardenTextField
import com.bitwarden.ui.platform.components.support.BitwardenSupportingContent
/**
* Lays out the Bitwarden-styled supporting content for a text field.
*
* Intended to be reused for all implementations of a TextField, such as the [BitwardenTextField]
* and [BitwardenPasswordField].
*/
@Composable
internal fun ColumnScope.BitwardenTextFieldSupportingContent(
content: (@Composable ColumnScope.() -> Unit)?,
supportingContentPadding: PaddingValues,
noContentCardPadding: Dp,
) {
if (content != null) {
Spacer(modifier = Modifier.height(height = 6.dp))
BitwardenHorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
)
BitwardenSupportingContent(
cardStyle = null,
insets = supportingContentPadding,
content = content,
)
} else {
// Pad out the card when there is no content.
Spacer(modifier = Modifier.height(height = noContentCardPadding))
}
}
@@ -0,0 +1,30 @@
package com.bitwarden.ui.platform.components.field.util
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalTextToolbar
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.text.input.TextFieldValue
import com.bitwarden.ui.platform.components.field.model.TextToolbarType
import com.bitwarden.ui.platform.components.field.toolbar.BitwardenCutCopyTextToolbar
import com.bitwarden.ui.platform.components.field.toolbar.BitwardenEmptyTextToolbar
/**
* Creates the appropriate type of [TextToolbar] based on the [TextToolbarType] and provided values.
*/
@Composable
fun TextToolbarType.toTextToolbar(
textFieldValue: TextFieldValue,
onValueChange: (String) -> Unit,
): TextToolbar = when (this) {
TextToolbarType.DEFAULT -> BitwardenCutCopyTextToolbar(
value = textFieldValue,
onValueChange = onValueChange,
defaultTextToolbar = LocalTextToolbar.current,
clipboardManager = LocalClipboard.current.nativeClipboard,
focusManager = LocalFocusManager.current,
)
TextToolbarType.NONE -> BitwardenEmptyTextToolbar
}