tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit d3e144be0f4e4d2c289c2926c585e8a728da7b6a
parent ff35a035f6521642db602b984e7bc51e46f8226a
Author: iorgamgabriel <iorgamgabriel@yahoo.com>
Date:   Mon, 24 Nov 2025 10:46:51 +0000

Bug 1983189 - Update TextField custom component to match M3 design r=android-reviewers,007

Differential Revision: https://phabricator.services.mozilla.com/D272229

Diffstat:
Mmobile/android/android-components/components/compose/base/src/main/java/mozilla/components/compose/base/textfield/TextField.kt | 365+++++++++++++++++++------------------------------------------------------------
Mmobile/android/android-components/components/compose/base/src/main/java/mozilla/components/compose/base/textfield/TrailingIconScope.kt | 14++++----------
Mmobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt | 6+++---
Mmobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt | 3+++
Mmobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAutofillRobot.kt | 18++++++++++++------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/BookmarksScreen.kt | 8+-------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/AddLoginScreen.kt | 15+++------------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/EditLoginScreen.kt | 17++++++-----------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginDetailsScreen.kt | 30+++++-------------------------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/SavedLoginsScreen.kt | 65+++++++++++++++++++++++++++++++----------------------------------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/TrailingIcons.kt | 4++--
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchBar.kt | 10+++++-----
12 files changed, 160 insertions(+), 395 deletions(-)

diff --git a/mobile/android/android-components/components/compose/base/src/main/java/mozilla/components/compose/base/textfield/TextField.kt b/mobile/android/android-components/components/compose/base/src/main/java/mozilla/components/compose/base/textfield/TextField.kt @@ -4,57 +4,35 @@ package mozilla.components.compose.base.textfield -import androidx.compose.foundation.background -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row 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.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldColors import androidx.compose.material3.TextFieldDefaults -import androidx.compose.material3.TextFieldDefaults.indicatorLine import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue 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.graphics.Color -import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.res.painterResource -import androidx.compose.ui.semantics.clearAndSetSemantics -import androidx.compose.ui.semantics.error -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import mozilla.components.compose.base.modifier.thenConditional import mozilla.components.compose.base.theme.AcornTheme -import mozilla.components.ui.icons.R as iconsR - -private val FocusedIndicatorLineThickness = 2.dp -private val UnfocusedIndicatorLineThickness = 1.dp -private val NoIndicatorLineThickness = 0.dp - -private val TrailingIconHeight = 24.dp +import mozilla.components.ui.icons.R /** * UI for a text field. @@ -64,6 +42,7 @@ private val TrailingIconHeight = 24.dp * @param placeholder The text displayed when the text field is empty. * @param errorText The message displayed when there is an error. * @param modifier Modifier to be applied to the text field layout. + * @param supportingText Optional helper text that can be displayed below the input field. * @param label Optional text displayed as a header above the input field. * @param isError Whether there is an error with the input value. When set to true, error styling * will be applied to the text field. @@ -73,11 +52,9 @@ private val TrailingIconHeight = 24.dp * the maxLines attribute will be automatically set to 1. * @param maxLines The maximum number of input lines visible to the user at once. * @param minLines The minimum number of input lines visible to the user at once. - * @param minHeight The minimum height constraint for the input field. - * @param trailingIcons The optional composable for adding trailing icons at the end of the text field + * @param trailingIcon The optional composable for adding a trailing icon at the end of the text field * container. * @param colors [TextFieldColors] to use for styling text field colors. - * @param style [TextFieldStyle] to use for styling text field. * @param visualTransformation The visual transformation filter for changing the visual representation * of the input. By default no visual transformation is applied. * @param keyboardOptions Software keyboard options that contains configuration such as [KeyboardType] and [ImeAction]. @@ -85,7 +62,6 @@ private val TrailingIconHeight = 24.dp * called. Note that this IME action may be different from what you specified in * [KeyboardOptions.imeAction]. */ -@Suppress("LongMethod", "CognitiveComplexMethod") @Composable fun TextField( value: String, @@ -93,264 +69,88 @@ fun TextField( placeholder: String, errorText: String, modifier: Modifier = Modifier, + supportingText: String? = null, label: String? = null, isError: Boolean = false, isEnabled: Boolean = true, singleLine: Boolean = true, maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, minLines: Int = 1, - minHeight: Dp = TrailingIconHeight, - trailingIcons: @Composable (TrailingIconScope.() -> Unit)? = null, - colors: TextFieldColors = TextFieldColors.default(), - style: TextFieldStyle = TextFieldStyle.default(), + trailingIcon: @Composable (TrailingIconScope.() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + colors: TextFieldColors = TextFieldDefaults.colors(), visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions(), ) { - val interactionSource = remember { MutableInteractionSource() } - - // We use the Material textFieldColors for the indicator line as it keeps track of error - // and focused states - val indicatorLineColors = TextFieldDefaults.colors( - focusedIndicatorColor = colors.focusedIndicatorColor, - unfocusedIndicatorColor = colors.unfocusedIndicatorColor, - errorIndicatorColor = colors.errorIndicatorColor, - ) - - Column( - modifier = modifier, - ) { - BasicTextField( - value = value, - onValueChange = onValueChange, - modifier = Modifier - .fillMaxWidth() - .thenConditional( - modifier = Modifier.semantics { error(errorText) }, - ) { isError } - .indicatorLine( - enabled = isEnabled, - isError = isError, - interactionSource = interactionSource, - colors = indicatorLineColors, - focusedIndicatorLineThickness = FocusedIndicatorLineThickness, - unfocusedIndicatorLineThickness = if (isEnabled) { - UnfocusedIndicatorLineThickness - } else { - NoIndicatorLineThickness - }, - ) - .defaultMinSize( - minWidth = TextFieldDefaults.MinWidth, - ), - enabled = isEnabled, - textStyle = style.inputStyle.copy( - color = colors.inputColor, - ), - cursorBrush = SolidColor(if (isError) colors.errorCursorColor else colors.cursorColor), - visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - interactionSource = interactionSource, - singleLine = singleLine, - maxLines = maxLines, - minLines = minLines, - decorationBox = @Composable { innerTextField -> - Column( - modifier = Modifier.fillMaxWidth(), - ) { - label?.let { - Text( - text = label, - style = style.labelStyle, - color = colors.labelColor, - ) - } - - Spacer(modifier = Modifier.height(4.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Box( - modifier = Modifier - .weight(1f) - // Ensures that the text field will remain the same height as the trailing icon - .heightIn(min = minHeight), - // The difference in alignment is to ensure that the placeholder text - // aligns with the cursor when more than 1 line is displayed - contentAlignment = if (singleLine || maxLines == 1) { - Alignment.CenterStart - } else { - Alignment.TopStart - }, - ) { - // This only controls the cursor and the input text - innerTextField() - - if (value.isEmpty()) { - Text( - text = placeholder, - style = style.placeholderStyle, - color = colors.placeholderColor, - ) - } - } - - if (isError) { - Spacer(modifier = Modifier.width(12.dp)) - - Icon( - painter = painterResource(iconsR.drawable.mozac_ic_warning_fill_24), - contentDescription = null, - tint = colors.errorTrailingIconColor, - ) - } else if (trailingIcons != null) { - Spacer(modifier = Modifier.width(12.dp)) - - Row { - with(TrailingIconScope(this)) { - trailingIcons.invoke(this) - } - } - } - } - - // Padding between the text (and trailing icon), and the indicator line - Spacer(modifier = Modifier.height(4.dp)) - } - }, - ) - - if (isError) { - Spacer(modifier = Modifier.height(4.dp)) - + val labelComposable: @Composable (() -> Unit)? = label?.let { + @Composable { Text( - text = errorText, - // The a11y for this is handled via the above `BasicTextField` - modifier = Modifier.clearAndSetSemantics { }, - style = style.errorTextStyle, - color = colors.errorTextColor, + text = label, + style = AcornTheme.typography.caption, ) } } -} - -/** - * [Color]s to use for the cursor and indicator line of [TextField]. - * - * @property inputColor The color for the input text. - * @property labelColor The color for the label. - * @property placeholderColor The color for the placeholder. - * @property errorTextColor The color for the error text. - * @property cursorColor The color for the cursor. - * @property errorCursorColor The error color for the cursor. - * @property focusedIndicatorColor The color for the indicator when focused. - * @property unfocusedIndicatorColor The color for the indicator when not focused. - * @property errorIndicatorColor The error color for the indicator. - * @property errorTrailingIconColor The trailing icon color of the warning icon. - */ -data class TextFieldColors( - val inputColor: Color, - val labelColor: Color, - val placeholderColor: Color, - val errorTextColor: Color, - val cursorColor: Color, - val errorCursorColor: Color, - val focusedIndicatorColor: Color, - val unfocusedIndicatorColor: Color, - val errorIndicatorColor: Color, - val errorTrailingIconColor: Color, -) { - - /** - * @see [TextFieldColors]. - */ - companion object { - - /** - * The default colors for [TextField]. - * - * @param inputColor @see [TextFieldColors.inputColor]. - * @param labelColor @see [TextFieldColors.labelColor]. - * @param placeholderColor @see [TextFieldColors.placeholderColor]. - * @param errorTextColor @see [TextFieldColors.errorTextColor]. - * @param cursorColor @see [TextFieldColors.cursorColor]. - * @param errorCursorColor @see [TextFieldColors.errorCursorColor]. - * @param focusedIndicatorColor @see [TextFieldColors.focusedIndicatorColor]. - * @param unfocusedIndicatorColor @see [TextFieldColors.unfocusedIndicatorColor]. - * @param errorIndicatorColor @see [TextFieldColors.errorIndicatorColor]. - * @param errorTrailingIconColor @see [TextFieldColors.errorTrailingIconColor]. - */ - @Composable - @ReadOnlyComposable - fun default( - inputColor: Color = AcornTheme.colors.textPrimary, - labelColor: Color = AcornTheme.colors.textPrimary, - placeholderColor: Color = AcornTheme.colors.textSecondary, - errorTextColor: Color = AcornTheme.colors.textCritical, - cursorColor: Color = AcornTheme.colors.borderFormDefault, - errorCursorColor: Color = AcornTheme.colors.borderFormDefault, - focusedIndicatorColor: Color = AcornTheme.colors.borderFormDefault, - unfocusedIndicatorColor: Color = AcornTheme.colors.borderFormDefault, - errorIndicatorColor: Color = AcornTheme.colors.borderCritical, - errorTrailingIconColor: Color = AcornTheme.colors.iconCritical, - ) = TextFieldColors( - inputColor = inputColor, - labelColor = labelColor, - placeholderColor = placeholderColor, - errorTextColor = errorTextColor, - cursorColor = cursorColor, - errorCursorColor = errorCursorColor, - focusedIndicatorColor = focusedIndicatorColor, - unfocusedIndicatorColor = unfocusedIndicatorColor, - errorIndicatorColor = errorIndicatorColor, - errorTrailingIconColor = errorTrailingIconColor, + val placeholderComposable: @Composable () -> Unit = { + Text( + text = placeholder, + style = AcornTheme.typography.body1, ) } -} + val supportingTextComposable: @Composable () -> Unit = { + if (isError) { + Text( + text = errorText, + style = AcornTheme.typography.caption, + ) + } else { + supportingText?.let { + Text( + text = it, + style = AcornTheme.typography.caption, + ) + } + } + } -/** - * [TextStyle]s to use for the [TextField]. - * - * @property inputStyle The text style for the input text. - * @property labelStyle The text style for the label text. - * @property placeholderStyle The text style for the placeholder text. - * @property errorTextStyle The text style for the error text. - */ -data class TextFieldStyle( - val inputStyle: TextStyle, - val labelStyle: TextStyle, - val placeholderStyle: TextStyle, - val errorTextStyle: TextStyle, -) { + val trailingIconCompose: @Composable () -> Unit = { + if (isError) { + Spacer(modifier = Modifier.width(12.dp)) - /** - * @see [TextFieldStyle]. - */ - companion object { + Icon( + painter = painterResource(R.drawable.mozac_ic_warning_fill_24), + contentDescription = null, + ) + } else if (trailingIcon != null) { + Spacer(modifier = Modifier.width(12.dp)) - /** - * The default text styles for [TextField]. - * - * @param inputStyle @see [TextFieldStyle.inputStyle]. - * @param labelStyle @see [TextFieldStyle.labelStyle]. - * @param placeholderStyle @see [TextFieldStyle.placeholderStyle]. - * @param errorTextStyle @see [TextFieldStyle.errorTextStyle]. - */ - fun default( - inputStyle: TextStyle = AcornTheme.typography.subtitle1, - labelStyle: TextStyle = AcornTheme.typography.caption, - placeholderStyle: TextStyle = AcornTheme.typography.subtitle1, - errorTextStyle: TextStyle = AcornTheme.typography.caption, - ) = TextFieldStyle( - inputStyle = inputStyle, - labelStyle = labelStyle, - placeholderStyle = placeholderStyle, - errorTextStyle = errorTextStyle, - ) + Row { + with(TrailingIconScope(this)) { + trailingIcon.invoke(this) + } + } + } } + OutlinedTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier + .fillMaxWidth(), + enabled = isEnabled, + textStyle = AcornTheme.typography.body1, + label = labelComposable, + placeholder = placeholderComposable, + leadingIcon = leadingIcon, + trailingIcon = trailingIconCompose, + supportingText = supportingTextComposable, + isError = isError, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + singleLine = singleLine, + maxLines = maxLines, + minLines = minLines, + ) } private data class TextFieldPreviewState( @@ -358,12 +158,12 @@ private data class TextFieldPreviewState( val label: String, val placeholder: String = "Placeholder", val errorText: String = "Error text", + val supportingText: String? = "Supporting text", val isError: Boolean = false, val singleLine: Boolean = true, val maxLines: Int = Int.MAX_VALUE, val minLines: Int = 1, - val minHeight: Dp = TrailingIconHeight, - val trailingIcons: @Composable (TrailingIconScope.() -> Unit)? = null, + val trailingIcon: @Composable (TrailingIconScope.() -> Unit)? = null, ) private class TextFieldParameterProvider : PreviewParameterProvider<TextFieldPreviewState> { @@ -416,14 +216,12 @@ private class TextFieldParameterProvider : PreviewParameterProvider<TextFieldPre TextFieldPreviewState( initialText = "Typed", label = "Typed, No error, 1 trailing icon", - minHeight = 48.dp, - trailingIcons = { CrossTextFieldButton {} }, + trailingIcon = { CrossTextFieldButton {} }, ), TextFieldPreviewState( initialText = "Typed", label = "Typed, No error, 2 trailing icons", - minHeight = 48.dp, - trailingIcons = { + trailingIcon = { EyeTextFieldButton {} CrossTextFieldButton {} }, @@ -432,12 +230,20 @@ private class TextFieldParameterProvider : PreviewParameterProvider<TextFieldPre initialText = "Typed", label = "Typed, Error, 2 trailing icons", isError = true, - minHeight = 48.dp, - trailingIcons = { + trailingIcon = { EyeTextFieldButton {} CrossTextFieldButton {} }, ), + TextFieldPreviewState( + initialText = "", + label = "Empty, Supporting text, Maximum lines is 2", + errorText = "Error text", + supportingText = "Supporting text", + isError = false, + singleLine = false, + maxLines = 2, + ), ) } @@ -449,9 +255,8 @@ private fun TextFieldPreview( var text by remember { mutableStateOf(textFieldState.initialText) } AcornTheme { - Column( + Surface( modifier = Modifier - .background(color = AcornTheme.colors.layer1) .padding(8.dp), ) { TextField( @@ -459,14 +264,14 @@ private fun TextFieldPreview( onValueChange = { text = it }, placeholder = textFieldState.placeholder, errorText = textFieldState.errorText, + supportingText = textFieldState.supportingText, modifier = Modifier.fillMaxWidth(), label = textFieldState.label, isError = textFieldState.isError, singleLine = textFieldState.singleLine, maxLines = textFieldState.maxLines, minLines = textFieldState.minLines, - minHeight = textFieldState.minHeight, - trailingIcons = textFieldState.trailingIcons, + trailingIcon = textFieldState.trailingIcon, ) } } diff --git a/mobile/android/android-components/components/compose/base/src/main/java/mozilla/components/compose/base/textfield/TrailingIconScope.kt b/mobile/android/android-components/components/compose/base/src/main/java/mozilla/components/compose/base/textfield/TrailingIconScope.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -17,7 +16,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation @@ -48,7 +46,6 @@ class TrailingIconScope(rowScope: RowScope) : RowScope by rowScope { ) = TrailingIconButton( iconId = iconsR.drawable.mozac_ic_eye_24, contentDescription = contentDescription, - tint = MaterialTheme.colorScheme.onSurfaceVariant, onTrailingIconClick = onTrailingIconClick, ) @@ -62,7 +59,6 @@ class TrailingIconScope(rowScope: RowScope) : RowScope by rowScope { ) = TrailingIconButton( iconId = iconsR.drawable.mozac_ic_cross_circle_fill_24, contentDescription = contentDescription, - tint = MaterialTheme.colorScheme.onSurfaceVariant, onTrailingIconClick = onTrailingIconClick, ) @@ -70,7 +66,6 @@ class TrailingIconScope(rowScope: RowScope) : RowScope by rowScope { private fun TrailingIconButton( @DrawableRes iconId: Int, contentDescription: Text?, - tint: Color, onTrailingIconClick: () -> Unit, ) { IconButton( @@ -80,7 +75,6 @@ class TrailingIconScope(rowScope: RowScope) : RowScope by rowScope { Icon( painter = painterResource(id = iconId), contentDescription = null, - tint = tint, ) } } @@ -105,7 +99,7 @@ private fun EyeTextFieldButtonPreview() { .fillMaxWidth() .padding(8.dp), label = "Eye", - trailingIcons = { EyeTextFieldButton { isPasswordVisible = !isPasswordVisible } }, + trailingIcon = { EyeTextFieldButton { isPasswordVisible = !isPasswordVisible } }, visualTransformation = if (isPasswordVisible) { VisualTransformation.None } else { @@ -138,7 +132,7 @@ private fun EyeTextFieldButtonPrivatePreview() { .fillMaxWidth() .padding(8.dp), label = "Eye", - trailingIcons = { EyeTextFieldButton { isPasswordVisible = !isPasswordVisible } }, + trailingIcon = { EyeTextFieldButton { isPasswordVisible = !isPasswordVisible } }, visualTransformation = if (isPasswordVisible) { VisualTransformation.None } else { @@ -167,7 +161,7 @@ private fun CrossTextFieldButtonPreview() { .fillMaxWidth() .padding(8.dp), label = "Cross", - trailingIcons = { CrossTextFieldButton { textFieldInput = "" } }, + trailingIcon = { CrossTextFieldButton { textFieldInput = "" } }, ) } } @@ -194,7 +188,7 @@ private fun CrossTextFieldButtonPrivatePreview() { .fillMaxWidth() .padding(8.dp), label = "Cross", - trailingIcons = { CrossTextFieldButton { textFieldInput = "" } }, + trailingIcon = { CrossTextFieldButton { textFieldInput = "" } }, ) } } diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt @@ -300,7 +300,7 @@ private fun ComposeTestRule.addFolderButton() = onNodeWithContentDescription(getStringResource(R.string.bookmark_add_new_folder_button_content_description)) private fun ComposeTestRule.addFolderTitleField() = - onNodeWithTag(ADD_BOOKMARK_FOLDER_NAME_TEXT_FIELD).onChildAt(0) + onNodeWithTag(ADD_BOOKMARK_FOLDER_NAME_TEXT_FIELD) private fun ComposeTestRule.navigateUpButton() = onNodeWithContentDescription(getStringResource(R.string.bookmark_navigate_back_button_content_description)) @@ -309,13 +309,13 @@ private fun ComposeTestRule.threeDotMenuButton(bookmarkedItem: String) = onNodeWithContentDescription("Item Menu for $bookmarkedItem") private fun ComposeTestRule.bookmarkNameEditBox() = - onNodeWithTag(EDIT_BOOKMARK_ITEM_TITLE_TEXT_FIELD).onChildAt(0) + onNodeWithTag(EDIT_BOOKMARK_ITEM_TITLE_TEXT_FIELD) private fun ComposeTestRule.bookmarkFolderSelector() = onNodeWithText("Bookmarks") private fun ComposeTestRule.bookmarkURLEditBox() = - onNodeWithTag(EDIT_BOOKMARK_ITEM_URL_TEXT_FIELD).onChildAt(0) + onNodeWithTag(EDIT_BOOKMARK_ITEM_URL_TEXT_FIELD) private fun ComposeTestRule.selectFolderNewFolderButton() = onNodeWithText(getStringResource(R.string.bookmark_select_folder_new_folder_button_title)) diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt @@ -1293,6 +1293,9 @@ class BrowserRobot { } fun clickBrokenSiteFormSendButton(composeTestRule: ComposeTestRule) { + Log.i(TAG, "clickBrokenSiteFormSendButton: Trying to close the keyboard.") + closeSoftKeyboard() + Log.i(TAG, "clickBrokenSiteFormSendButton: Closed the keyboard.") Log.i(TAG, "clickBrokenSiteFormSendButton: Trying to click the \"Cancel\" button") composeTestRule.onNodeWithText(getStringResource(R.string.webcompat_reporter_send)) .performClick() diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAutofillRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAutofillRobot.kt @@ -325,6 +325,9 @@ class SettingsSubMenuAutofillRobot(private val composeTestRule: ComposeTestRule) } fun clickCountryDropdown() { + Log.i(TAG, "clickCountryDropdown: Trying to close the keyboard.") + closeSoftKeyboard() + Log.i(TAG, "clickCountryDropdown: Closed the keyboard.") Log.i(TAG, "clickCountryDropdown: Trying to click \"Country or region\" dropdown") composeTestRule.countryDropDown().performClick() Log.i(TAG, "clickCountryDropdown: Clicked \"Country or region\" dropdown") @@ -387,6 +390,9 @@ class SettingsSubMenuAutofillRobot(private val composeTestRule: ComposeTestRule) Log.i(TAG, "fillAndSaveAddress: Trying to set \"Zip\" to $zipCode") composeTestRule.zipCodeTextInput().performTextInput(zipCode) Log.i(TAG, "fillAndSaveAddress: \"Zip\" was set to $zipCode") + Log.i(TAG, "fillAndSaveAddress: Trying to close the keyboard.") + closeSoftKeyboard() + Log.i(TAG, "fillAndSaveAddress: Closed the keyboard.") Log.i(TAG, "fillAndSaveAddress: Trying to click \"Country or region\" dropdown button") composeTestRule.countryDropDown().performClick() Log.i(TAG, "fillAndSaveAddress: Clicked \"Country or region\" dropdown button") @@ -663,14 +669,14 @@ private fun ComposeTestRule.editAddressToolbarTitle() = onNodeWithText(getString private fun ComposeTestRule.toolbarCheckmarkButton() = onNodeWithContentDescription(getStringResource(R.string.address_menu_save_address)) private fun navigateBackButton() = itemWithDescription(getStringResource(R.string.action_bar_up_description)) private fun ComposeTestRule.navigateBackButton() = onNodeWithContentDescription("Navigate back") -private fun ComposeTestRule.nameTextInput() = onNodeWithTag(EditAddressTestTag.NAME_FIELD).onChildAt(0) -private fun ComposeTestRule.streetAddressTextInput() = onNodeWithTag(EditAddressTestTag.STREET_ADDRESS_FIELD).onChildAt(0) -private fun ComposeTestRule.cityTextInput() = onNodeWithTag(EditAddressTestTag.ADDRESS_LEVEL2_FIELD).onChildAt(0) +private fun ComposeTestRule.nameTextInput() = onNodeWithTag(EditAddressTestTag.NAME_FIELD) +private fun ComposeTestRule.streetAddressTextInput() = onNodeWithTag(EditAddressTestTag.STREET_ADDRESS_FIELD) +private fun ComposeTestRule.cityTextInput() = onNodeWithTag(EditAddressTestTag.ADDRESS_LEVEL2_FIELD) private fun ComposeTestRule.subRegionDropDown() = onNodeWithTag(EditAddressTestTag.ADDRESS_LEVEL1_FIELD) -private fun ComposeTestRule.zipCodeTextInput() = onNodeWithTag(EditAddressTestTag.POSTAL_CODE_FIELD).onChildAt(0) +private fun ComposeTestRule.zipCodeTextInput() = onNodeWithTag(EditAddressTestTag.POSTAL_CODE_FIELD) private fun ComposeTestRule.countryDropDown() = onNodeWithTag(EditAddressTestTag.COUNTRY_FIELD) -private fun ComposeTestRule.phoneTextInput() = onNodeWithTag(EditAddressTestTag.TEL_FIELD).onChildAt(0) -private fun ComposeTestRule.emailTextInput() = onNodeWithTag(EditAddressTestTag.EMAIL_FIELD).onChildAt(0) +private fun ComposeTestRule.phoneTextInput() = onNodeWithTag(EditAddressTestTag.TEL_FIELD) +private fun ComposeTestRule.emailTextInput() = onNodeWithTag(EditAddressTestTag.EMAIL_FIELD) private fun ComposeTestRule.saveButton() = onNodeWithTag(EditAddressTestTag.SAVE_BUTTON) private fun ComposeTestRule.cancelButton() = onNodeWithTag(EditAddressTestTag.CANCEL_BUTTON) private fun ComposeTestRule.deleteAddressButton() = onNodeWithTag(EditAddressTestTag.DELETE_BUTTON) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/BookmarksScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bookmarks/BookmarksScreen.kt @@ -105,7 +105,6 @@ import mozilla.components.compose.base.snackbar.Snackbar import mozilla.components.compose.base.snackbar.displaySnackbar import mozilla.components.compose.base.text.Text import mozilla.components.compose.base.textfield.TextField -import mozilla.components.compose.base.textfield.TextFieldColors import mozilla.components.compose.base.utils.BackInvokedHandler import mozilla.components.compose.browser.awesomebar.AwesomeBar import mozilla.components.compose.browser.awesomebar.AwesomeBarDefaults @@ -138,7 +137,6 @@ import org.mozilla.fenix.search.SearchFragmentStore import org.mozilla.fenix.theme.FirefoxTheme import mozilla.components.ui.icons.R as iconsR -private val IconButtonHeight = 48.dp private const val MATERIAL_DESIGN_SCRIM = "#52000000" /** @@ -1683,15 +1681,11 @@ private fun ClearableTextField( .onFocusChanged { isFocused = it.isFocused } .padding(0.dp) .paddingFromBaseline(0.dp), - minHeight = IconButtonHeight, - trailingIcons = { + trailingIcon = { if (isFocused && value.isNotEmpty()) { CrossTextFieldButton { onValueChange("") } } }, - colors = TextFieldColors.default( - placeholderColor = MaterialTheme.colorScheme.onSurface, - ), ) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/AddLoginScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/AddLoginScreen.kt @@ -35,8 +35,6 @@ import androidx.compose.ui.unit.dp import mozilla.components.compose.base.annotation.FlexibleWindowLightDarkPreview import mozilla.components.compose.base.button.IconButton import mozilla.components.compose.base.textfield.TextField -import mozilla.components.compose.base.textfield.TextFieldColors -import mozilla.components.compose.base.textfield.TextFieldStyle import mozilla.components.lib.state.ext.observeAsState import mozilla.components.support.ktx.util.URLStringUtils.isHttpOrHttps import mozilla.components.support.ktx.util.URLStringUtils.isValidHost @@ -45,8 +43,6 @@ import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.Theme import mozilla.components.ui.icons.R as iconsR -private val IconButtonHeight = 48.dp - @Composable internal fun AddLoginScreen(store: LoginsStore) { Scaffold( @@ -146,8 +142,7 @@ private fun AddLoginHost(store: LoginsStore) { ) .width(FirefoxTheme.layout.size.containerMaxWidth), label = stringResource(R.string.preferences_passwords_saved_logins_site), - minHeight = IconButtonHeight, - trailingIcons = { + trailingIcon = { if (isFocused && isValidHost(host)) { CrossTextFieldButton { store.dispatch(AddLoginAction.HostChanged("")) } } @@ -162,8 +157,6 @@ private fun AddLoginHost(store: LoginsStore) { modifier = Modifier .padding(horizontal = FirefoxTheme.layout.space.static200) .width(FirefoxTheme.layout.size.containerMaxWidth), - style = TextFieldStyle.default().labelStyle, - color = TextFieldColors.default().placeholderColor, ) } } @@ -191,8 +184,7 @@ private fun AddLoginUsername(store: LoginsStore) { ) .width(FirefoxTheme.layout.size.containerMaxWidth), label = stringResource(R.string.preferences_passwords_saved_logins_username), - minHeight = IconButtonHeight, - trailingIcons = { + trailingIcon = { if (isFocused && addLoginState?.username?.isNotEmpty() == true) { CrossTextFieldButton { store.dispatch(AddLoginAction.UsernameChanged("")) } } @@ -222,8 +214,7 @@ private fun AddLoginPassword(store: LoginsStore) { ) .width(FirefoxTheme.layout.size.containerMaxWidth), label = stringResource(R.string.preferences_passwords_saved_logins_password), - minHeight = IconButtonHeight, - trailingIcons = { + trailingIcon = { if (isFocused && state?.password?.isNotEmpty() == true) { CrossTextFieldButton { store.dispatch(AddLoginAction.PasswordChanged("")) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/EditLoginScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/EditLoginScreen.kt @@ -32,16 +32,13 @@ import androidx.compose.ui.unit.dp import mozilla.components.compose.base.annotation.FlexibleWindowLightDarkPreview import mozilla.components.compose.base.button.IconButton import mozilla.components.compose.base.textfield.TextField -import mozilla.components.compose.base.textfield.TextFieldColors -import mozilla.components.compose.base.textfield.TextFieldStyle +import mozilla.components.compose.base.theme.AcornTheme import mozilla.components.lib.state.ext.observeAsState import org.mozilla.fenix.R import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.Theme import mozilla.components.ui.icons.R as iconsR -private val IconButtonHeight = 48.dp - @Composable internal fun EditLoginScreen(store: LoginsStore) { val state by store.observeAsState(store.state) { it } @@ -131,8 +128,8 @@ internal fun EditLoginTopBar(store: LoginsStore, loginItem: LoginItem) { private fun EditLoginUrl(url: String) { Text( text = stringResource(R.string.preferences_passwords_saved_logins_site), - style = TextFieldStyle.default().labelStyle, - color = TextFieldColors.default().labelColor, + style = AcornTheme.typography.caption, + color = AcornTheme.colors.textPrimary, modifier = Modifier .padding(horizontal = FirefoxTheme.layout.space.static200) .width(FirefoxTheme.layout.size.containerMaxWidth), @@ -140,7 +137,7 @@ private fun EditLoginUrl(url: String) { Text( text = url, - style = TextFieldStyle.default().placeholderStyle, + style = AcornTheme.typography.subtitle1, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), modifier = Modifier .padding( @@ -171,8 +168,7 @@ private fun EditLoginUsername(store: LoginsStore, user: String) { ) .width(FirefoxTheme.layout.size.containerMaxWidth), label = stringResource(R.string.preferences_passwords_saved_logins_username), - minHeight = IconButtonHeight, - trailingIcons = { + trailingIcon = { if (editState?.newUsername?.isNotEmpty() == true) { CrossTextFieldButton { store.dispatch(EditLoginAction.UsernameChanged("")) @@ -204,8 +200,7 @@ private fun EditLoginPassword(store: LoginsStore, pass: String) { ) .width(FirefoxTheme.layout.size.containerMaxWidth), label = stringResource(R.string.preferences_passwords_saved_logins_password), - minHeight = IconButtonHeight, - trailingIcons = { + trailingIcon = { EyePasswordIconButton( isPasswordVisible = isPasswordVisible, onTrailingIconClick = { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginDetailsScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/LoginDetailsScreen.kt @@ -48,8 +48,6 @@ import mozilla.components.compose.base.menu.MenuItem import mozilla.components.compose.base.snackbar.Snackbar import mozilla.components.compose.base.snackbar.displaySnackbar import mozilla.components.compose.base.textfield.TextField -import mozilla.components.compose.base.textfield.TextFieldColors -import mozilla.components.compose.base.textfield.TextFieldStyle import mozilla.components.lib.state.ext.observeAsState import org.mozilla.fenix.R import org.mozilla.fenix.theme.FirefoxTheme @@ -204,26 +202,18 @@ private fun LoginDetailMenu( @Composable private fun LoginDetailsUrl(store: LoginsStore, url: String) { - Text( - text = stringResource(R.string.preferences_passwords_saved_logins_site), - style = TextFieldStyle.default().labelStyle, - color = TextFieldColors.default().labelColor, - modifier = Modifier - .padding(horizontal = FirefoxTheme.layout.space.static200) - .width(FirefoxTheme.layout.size.containerMaxWidth), - ) - TextField( value = url, onValueChange = {}, isEnabled = false, placeholder = "", errorText = "", + label = stringResource(R.string.preferences_passwords_saved_logins_site), modifier = Modifier .padding(horizontal = FirefoxTheme.layout.space.static200) .wrapContentHeight() .width(FirefoxTheme.layout.size.containerMaxWidth), - trailingIcons = { + trailingIcon = { IconButton( onClick = { store.dispatch(DetailLoginAction.GoToSiteClicked(url)) @@ -248,26 +238,18 @@ private fun LoginDetailsUsername( val usernameSnackbarText = stringResource(R.string.logins_username_copied) val coroutineScope = rememberCoroutineScope() - Text( - text = stringResource(R.string.preferences_passwords_saved_logins_username), - style = TextFieldStyle.default().labelStyle, - color = TextFieldColors.default().labelColor, - modifier = Modifier - .padding(horizontal = FirefoxTheme.layout.space.static200) - .width(FirefoxTheme.layout.size.containerMaxWidth), - ) - TextField( value = username, onValueChange = {}, isEnabled = false, placeholder = "", errorText = "", + label = stringResource(R.string.preferences_passwords_saved_logins_username), modifier = Modifier .padding(horizontal = FirefoxTheme.layout.space.static200) .wrapContentHeight() .width(FirefoxTheme.layout.size.containerMaxWidth), - trailingIcons = { + trailingIcon = { IconButton( onClick = { store.dispatch(DetailLoginAction.CopyUsernameClicked(username)) @@ -302,8 +284,6 @@ private fun LoginDetailsPassword( Text( text = stringResource(R.string.preferences_passwords_saved_logins_password), - style = TextFieldStyle.default().labelStyle, - color = TextFieldColors.default().labelColor, modifier = Modifier .padding(horizontal = FirefoxTheme.layout.space.static200) .width(FirefoxTheme.layout.size.containerMaxWidth), @@ -319,7 +299,7 @@ private fun LoginDetailsPassword( .padding(horizontal = FirefoxTheme.layout.space.static200) .wrapContentHeight() .width(FirefoxTheme.layout.size.containerMaxWidth), - trailingIcons = { + trailingIcon = { EyePasswordIconButton( isPasswordVisible = isPasswordVisible, onTrailingIconClick = { isPasswordVisible = !isPasswordVisible }, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/SavedLoginsScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/SavedLoginsScreen.kt @@ -316,8 +316,6 @@ private fun LoginsListTopBar( ) } -private val IconButtonHeight = 48.dp - @Composable private fun SearchBar( text: String, @@ -330,40 +328,39 @@ private fun SearchBar( focusRequester.requestFocus() } - TextField( - value = text, - placeholder = stringResource(R.string.preferences_passwords_saved_logins_search_2), - onValueChange = { - store.dispatch(SearchLogins(searchText = it, loginItems = store.state.loginItems)) - }, - errorText = "", - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - minHeight = IconButtonHeight, - trailingIcons = { - if (text.isNotBlank()) { - IconButton( - onClick = { - store.dispatch( - SearchLogins( - searchText = "", - loginItems = store.state.loginItems, - ), - ) - }, - contentDescription = null, - ) { - Icon( - painter = painterResource(iconsR.drawable.mozac_ic_cross_24), + TextField( + value = text, + placeholder = stringResource(R.string.preferences_passwords_saved_logins_search_2), + onValueChange = { + store.dispatch(SearchLogins(searchText = it, loginItems = store.state.loginItems)) + }, + errorText = "", + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + trailingIcon = { + if (text.isNotBlank()) { + IconButton( + onClick = { + store.dispatch( + SearchLogins( + searchText = "", + loginItems = store.state.loginItems, + ), + ) + }, contentDescription = null, - ) + ) { + Icon( + painter = painterResource(iconsR.drawable.mozac_ic_cross_24), + contentDescription = null, + ) + } } - } - }, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - ) + }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + ) } @Composable diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/TrailingIcons.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/TrailingIcons.kt @@ -72,7 +72,7 @@ private fun EyePasswordIconButtonPreview() { .fillMaxWidth() .padding(8.dp), label = "", - trailingIcons = { + trailingIcon = { EyePasswordIconButton( isPasswordVisible = isPasswordVisible, onTrailingIconClick = { isPasswordVisible = !isPasswordVisible }, @@ -105,7 +105,7 @@ private fun EyePasswordIconButtonPrivatePreview() { .fillMaxWidth() .padding(8.dp), label = "", - trailingIcons = { + trailingIcon = { EyePasswordIconButton( isPasswordVisible = isPasswordVisible, onTrailingIconClick = { isPasswordVisible = !isPasswordVisible }, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchBar.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/settingssearch/SettingsSearchBar.kt @@ -6,9 +6,10 @@ package org.mozilla.fenix.settings.settingssearch import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect @@ -25,7 +26,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import mozilla.components.compose.base.button.IconButton import mozilla.components.compose.base.textfield.TextField -import mozilla.components.compose.base.textfield.TextFieldColors import mozilla.components.lib.state.ext.observeAsComposableState import org.mozilla.fenix.R import org.mozilla.fenix.theme.FirefoxTheme @@ -49,7 +49,7 @@ fun SettingsSearchBar( TopAppBar( modifier = Modifier - .height(72.dp), + .wrapContentHeight(), title = { TextField( value = searchQuery, @@ -63,12 +63,12 @@ fun SettingsSearchBar( placeholder = stringResource(R.string.settings_search_title), singleLine = true, errorText = stringResource(R.string.settings_search_error_message), - colors = TextFieldColors.default( + colors = TextFieldDefaults.colors( focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, errorIndicatorColor = Color.Transparent, ), - trailingIcons = { + trailingIcon = { when (state) { is SettingsSearchState.SearchInProgress, is SettingsSearchState.NoSearchResults,