diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenPasswordField.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenPasswordField.kt index bf26833e41..bf014ea1f3 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenPasswordField.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenPasswordField.kt @@ -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, diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenTextField.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenTextField.kt index db1b1ee598..4b7aca0743 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenTextField.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenTextField.kt @@ -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, + 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() { diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/interceptor/NoPersonalizedLearningInterceptor.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/interceptor/NoPersonalizedLearningInterceptor.kt index ae82bf37d5..9a00f7b783 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/interceptor/NoPersonalizedLearningInterceptor.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/interceptor/NoPersonalizedLearningInterceptor.kt @@ -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( diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/label/BitwardenTextFieldLabel.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/label/BitwardenTextFieldLabel.kt new file mode 100644 index 0000000000..d096d459dc --- /dev/null +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/label/BitwardenTextFieldLabel.kt @@ -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), + ) + } + } +} diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/support/BitwardenTextFieldSupportingContent.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/support/BitwardenTextFieldSupportingContent.kt new file mode 100644 index 0000000000..47ea46692c --- /dev/null +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/support/BitwardenTextFieldSupportingContent.kt @@ -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)) + } +} diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/util/TextToolbarTypeExtensions.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/util/TextToolbarTypeExtensions.kt new file mode 100644 index 0000000000..cfa4871ae7 --- /dev/null +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/util/TextToolbarTypeExtensions.kt @@ -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 +}