From 3e6b8397d6c46e586eadac7dacb570b674095947 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 17 Sep 2026 11:03:19 -0400 Subject: [PATCH] Fix label font weight on focused fields. --- .../org/signal/core/ui/compose/TextFields.kt | 24 +++++++++++++++++++ .../totpnameentry/TotpNameEntryScreen.kt | 6 ++++- .../screens/addusername/AddUsernameScreen.kt | 6 ++++- .../screens/aepentry/EnterAepScreen.kt | 6 ++++- .../createprofile/CreateProfileScreen.kt | 11 +++++++-- .../EnterLocalBackupV1PassphaseScreen.kt | 6 ++++- .../phonenumber/PhoneNumberEntryScreen.kt | 6 ++++- .../SignalLoginCredentialEntryScreen.kt | 11 +++++++-- .../SignalLoginPaymentScreen.kt | 8 ++++++- 9 files changed, 74 insertions(+), 10 deletions(-) diff --git a/core/ui/src/main/java/org/signal/core/ui/compose/TextFields.kt b/core/ui/src/main/java/org/signal/core/ui/compose/TextFields.kt index a60cbde603..1bcf1e27cf 100644 --- a/core/ui/src/main/java/org/signal/core/ui/compose/TextFields.kt +++ b/core/ui/src/main/java/org/signal/core/ui/compose/TextFields.kt @@ -6,7 +6,9 @@ package org.signal.core.ui.compose import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.relocation.BringIntoViewRequester @@ -20,6 +22,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.material3.TextFieldColors import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable @@ -38,6 +41,7 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp @@ -188,6 +192,26 @@ object TextFields { } } + /** + * A text field label that sits at Label Medium weight once it has minimized above the input, which is what happens + * when the field is focused or already has text in it. + * + * [interactionSource] must be the same instance given to the text field. + */ + @Composable + fun Label( + text: String, + hasText: Boolean, + interactionSource: InteractionSource + ) { + val isFocused by interactionSource.collectIsFocusedAsState() + + Text( + text = text, + fontWeight = if (isFocused || hasText) FontWeight.Medium else FontWeight.Normal + ) + } + private fun String.createSelection(): TextRange { return when { isEmpty() -> TextRange.Zero diff --git a/feature/app-settings/src/main/java/org/signal/appsettings/totpnameentry/TotpNameEntryScreen.kt b/feature/app-settings/src/main/java/org/signal/appsettings/totpnameentry/TotpNameEntryScreen.kt index 9b88822202..2c1538bdc1 100644 --- a/feature/app-settings/src/main/java/org/signal/appsettings/totpnameentry/TotpNameEntryScreen.kt +++ b/feature/app-settings/src/main/java/org/signal/appsettings/totpnameentry/TotpNameEntryScreen.kt @@ -6,6 +6,7 @@ package org.signal.appsettings.totpnameentry import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -36,6 +37,7 @@ import org.signal.core.ui.compose.DayNightPreviews import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.Scaffolds import org.signal.core.ui.compose.SignalIcons +import org.signal.core.ui.compose.TextFields @VisibleForTesting object TotpNameEntryTestTags { @@ -53,6 +55,7 @@ fun TotpNameEntryScreen( onEvent: (TotpNameEntryEvent) -> Unit ) { val focusRequester = remember { FocusRequester() } + val interactionSource = remember { MutableInteractionSource() } LaunchedEffect(Unit) { focusRequester.requestFocus() @@ -86,7 +89,8 @@ fun TotpNameEntryScreen( TextField( value = state.name, onValueChange = { onEvent(TotpNameEntryEvent.NameChanged(it)) }, - label = { Text(text = stringResource(R.string.TotpNameEntryScreen__name)) }, + label = { TextFields.Label(stringResource(R.string.TotpNameEntryScreen__name), state.name.isNotEmpty(), interactionSource) }, + interactionSource = interactionSource, singleLine = true, enabled = !state.submitting, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), diff --git a/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameScreen.kt index 9e20c25823..8c150658dc 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameScreen.kt @@ -6,6 +6,7 @@ package org.signal.registration.screens.addusername import androidx.compose.foundation.Image +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope @@ -65,6 +66,7 @@ import org.signal.core.ui.compose.Buttons import org.signal.core.ui.compose.Dialogs import org.signal.core.ui.compose.Dividers import org.signal.core.ui.compose.Previews +import org.signal.core.ui.compose.TextFields import org.signal.core.util.UsernameUtil import org.signal.libsignal.usernames.Username import org.signal.registration.R @@ -221,6 +223,7 @@ private fun ColumnScope.UsernameEntry( onEvent: (AddUsernameScreenEvents) -> Unit ) { val focusRequester = remember { FocusRequester() } + val interactionSource = remember { MutableInteractionSource() } val validationMessage: String? = state.validationError?.message() LaunchedEffect(Unit) { @@ -244,7 +247,8 @@ private fun ColumnScope.UsernameEntry( TextField( value = state.username, onValueChange = { onEvent(AddUsernameScreenEvents.UsernameChanged(it)) }, - label = { Text(stringResource(R.string.AddUsernameScreen__username)) }, + label = { TextFields.Label(stringResource(R.string.AddUsernameScreen__username), state.username.isNotEmpty(), interactionSource) }, + interactionSource = interactionSource, singleLine = true, enabled = !state.showSpinner, isError = state.validationError != null, diff --git a/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepScreen.kt index d3502b2edd..a3d77bff2c 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/aepentry/EnterAepScreen.kt @@ -5,6 +5,7 @@ package org.signal.registration.screens.aepentry +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -58,6 +59,7 @@ import org.signal.core.ui.compose.AllDevicePreviews import org.signal.core.ui.compose.Buttons import org.signal.core.ui.compose.Dialogs import org.signal.core.ui.compose.Previews +import org.signal.core.ui.compose.TextFields import org.signal.passwordmanager.SignalCredentialManager import org.signal.passwordmanager.compose.attachPasswordAutoFillHelper import org.signal.passwordmanager.compose.passwordAutoFillHelper @@ -275,6 +277,7 @@ private fun RecoveryKeyTextField(state: EnterAepState, onEvent: (EnterAepEvents) var requestFocus by remember { mutableStateOf(true) } val keyboardController = LocalSoftwareKeyboardController.current val autoFillHelper = passwordAutoFillHelper { onEvent(EnterAepEvents.BackupKeyChanged(it)) } + val interactionSource = remember { MutableInteractionSource() } TextField( value = state.recoveryKey.enteredText, @@ -282,7 +285,8 @@ private fun RecoveryKeyTextField(state: EnterAepState, onEvent: (EnterAepEvents) onEvent(EnterAepEvents.BackupKeyChanged(it)) autoFillHelper.onValueChanged(it) }, - label = { Text(stringResource(R.string.EnterAepScreen__recovery_key)) }, + label = { TextFields.Label(stringResource(R.string.EnterAepScreen__recovery_key), state.recoveryKey.enteredText.isNotEmpty(), interactionSource) }, + interactionSource = interactionSource, textStyle = MaterialTheme.typography.bodyLarge.copy( fontFamily = MonoTypeface.fontFamily(), lineHeight = 36.sp diff --git a/feature/registration/src/main/java/org/signal/registration/screens/createprofile/CreateProfileScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/createprofile/CreateProfileScreen.kt index 6cffcb8078..f88e20bf97 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/createprofile/CreateProfileScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/createprofile/CreateProfileScreen.kt @@ -11,6 +11,7 @@ import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -50,6 +51,7 @@ import org.signal.core.ui.compose.Buttons import org.signal.core.ui.compose.Dialogs import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.SignalIcons +import org.signal.core.ui.compose.TextFields import org.signal.core.ui.rememberWindowBreakpoint import org.signal.registration.R import org.signal.registration.screens.RegistrationScaffold @@ -113,6 +115,9 @@ private fun CompactLayout( onAvatarClick: () -> Unit, modifier: Modifier = Modifier ) { + val givenNameInteractionSource = remember { MutableInteractionSource() } + val familyNameInteractionSource = remember { MutableInteractionSource() } + RegistrationScaffold( modifier = modifier .fillMaxSize() @@ -152,7 +157,8 @@ private fun CompactLayout( OutlinedTextField( value = state.givenName, onValueChange = { onEvent(CreateProfileScreenEvents.GivenNameChanged(it)) }, - label = { Text(stringResource(R.string.CreateProfileScreen__first_name_required)) }, + label = { TextFields.Label(stringResource(R.string.CreateProfileScreen__first_name_required), state.givenName.isNotEmpty(), givenNameInteractionSource) }, + interactionSource = givenNameInteractionSource, singleLine = true, enabled = !state.isSubmitting, keyboardOptions = KeyboardOptions( @@ -169,7 +175,8 @@ private fun CompactLayout( OutlinedTextField( value = state.familyName, onValueChange = { onEvent(CreateProfileScreenEvents.FamilyNameChanged(it)) }, - label = { Text(stringResource(R.string.CreateProfileScreen__last_name_optional)) }, + label = { TextFields.Label(stringResource(R.string.CreateProfileScreen__last_name_optional), state.familyName.isNotEmpty(), familyNameInteractionSource) }, + interactionSource = familyNameInteractionSource, singleLine = true, enabled = !state.isSubmitting, keyboardOptions = KeyboardOptions( diff --git a/feature/registration/src/main/java/org/signal/registration/screens/localbackuprestore/EnterLocalBackupV1PassphaseScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/localbackuprestore/EnterLocalBackupV1PassphaseScreen.kt index 19dc459d46..f38e0a22a1 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/localbackuprestore/EnterLocalBackupV1PassphaseScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/localbackuprestore/EnterLocalBackupV1PassphaseScreen.kt @@ -5,6 +5,7 @@ package org.signal.registration.screens.localbackuprestore +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -51,6 +52,7 @@ import androidx.compose.ui.unit.sp import org.signal.core.ui.compose.AllDevicePreviews import org.signal.core.ui.compose.Buttons import org.signal.core.ui.compose.Previews +import org.signal.core.ui.compose.TextFields import org.signal.registration.R import org.signal.registration.screens.OnePaneRegistrationScaffold import org.signal.registration.screens.RegistrationScaffold @@ -233,13 +235,15 @@ private fun PassphraseTextField( val focusRequester = remember { FocusRequester() } var requestFocus by remember { mutableStateOf(true) } val keyboardController = LocalSoftwareKeyboardController.current + val interactionSource = remember { MutableInteractionSource() } TextField( value = passphrase, onValueChange = { newValue -> onPassphraseChange(newValue.filter { it.isDigit() }) }, - label = { Text(stringResource(R.string.LocalBackupRestoreScreen__passphrase)) }, + label = { TextFields.Label(stringResource(R.string.LocalBackupRestoreScreen__passphrase), passphrase.isNotEmpty(), interactionSource) }, + interactionSource = interactionSource, textStyle = MaterialTheme.typography.bodyLarge.copy( fontFamily = FontFamily.Monospace, lineHeight = 36.sp diff --git a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreen.kt index 5d388d6c54..543a0feb1d 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreen.kt @@ -14,6 +14,7 @@ import androidx.activity.result.IntentSenderRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -80,6 +81,7 @@ import org.signal.core.ui.compose.IconButtons.IconButton import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.Scaffolds import org.signal.core.ui.compose.SignalIcons +import org.signal.core.ui.compose.TextFields import org.signal.core.util.Util import org.signal.core.util.logging.Log import org.signal.registration.PendingRestoreOption @@ -515,6 +517,7 @@ private fun PhoneNumberInputFields( ) { var phoneNumberTextFieldValue by remember { mutableStateOf(TextFieldValue(state.formattedNumber)) } val focusRequester = remember { FocusRequester() } + val interactionSource = remember { MutableInteractionSource() } val hasValidCountry = state.countryName.isNotEmpty() val isAccountId = state.enteredAccountId != null val label = if (isAccountId) R.string.RegistrationActivity_account_id else R.string.RegistrationActivity_phone_number_description @@ -594,7 +597,8 @@ private fun PhoneNumberInputFields( .weight(1f) .focusRequester(focusRequester) .testTag(TestTags.PHONE_NUMBER_PHONE_FIELD), - label = { Text(stringResource(label)) }, + label = { TextFields.Label(stringResource(label), phoneNumberTextFieldValue.text.isNotEmpty(), interactionSource) }, + interactionSource = interactionSource, isError = state.isNumberInvalid || state.accountIdError != null, supportingText = supportingText, keyboardOptions = if (isAccountId) { diff --git a/feature/registration/src/main/java/org/signal/registration/screens/signallogincredentials/SignalLoginCredentialEntryScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/signallogincredentials/SignalLoginCredentialEntryScreen.kt index 5142d44c7c..129ba664f5 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/signallogincredentials/SignalLoginCredentialEntryScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/signallogincredentials/SignalLoginCredentialEntryScreen.kt @@ -6,6 +6,7 @@ package org.signal.registration.screens.signallogincredentials import androidx.compose.foundation.Image +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -65,6 +66,7 @@ import org.signal.core.ui.compose.Buttons import org.signal.core.ui.compose.Dialogs import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.SignalIcons +import org.signal.core.ui.compose.TextFields import org.signal.passwordmanager.SignalCredentialManager import org.signal.passwordmanager.compose.attachPasswordAutoFillHelper import org.signal.passwordmanager.compose.passwordAutoFillHelper @@ -302,10 +304,13 @@ private fun AccountIdTextField( onEvent: (SignalLoginCredentialEntryScreenEvents) -> Unit, modifier: Modifier = Modifier ) { + val interactionSource = remember { MutableInteractionSource() } + TextField( value = state.accountId, onValueChange = { onEvent(SignalLoginCredentialEntryScreenEvents.AccountIdChanged(it)) }, - label = { Text(stringResource(R.string.SignalLoginCredentialEntryScreen__account_id)) }, + label = { TextFields.Label(stringResource(R.string.SignalLoginCredentialEntryScreen__account_id), state.accountId.isNotEmpty(), interactionSource) }, + interactionSource = interactionSource, singleLine = true, textStyle = accountIdTextStyle(), colors = TextFieldDefaults.colors( @@ -344,6 +349,7 @@ private fun RecoveryKeyTextField( if (revealed) AepVisualTransformation(RecoveryKeyGroups.GROUP_SIZE) else PasswordVisualTransformation() } val isError = state.recoveryKey.error != null || state.areCredentialsIncorrect + val interactionSource = remember { MutableInteractionSource() } val minimizedLabelHeight = MaterialTheme.typography.bodySmall.lineHeight.takeIf { it.isSp } ?: MINIMIZED_LABEL_LINE_HEIGHT val firstLineCenterY = with(LocalDensity.current) { TEXT_FIELD_TOP_PADDING + minimizedLabelHeight.toDp() + RECOVERY_KEY_LINE_HEIGHT.toDp() / 2 @@ -356,7 +362,8 @@ private fun RecoveryKeyTextField( onEvent(SignalLoginCredentialEntryScreenEvents.RecoveryKeyChanged(it)) autoFillHelper.onValueChanged(it) }, - label = { Text(stringResource(R.string.SignalLoginCredentialEntryScreen__recovery_key)) }, + label = { TextFields.Label(stringResource(R.string.SignalLoginCredentialEntryScreen__recovery_key), state.recoveryKey.enteredText.isNotEmpty(), interactionSource) }, + interactionSource = interactionSource, singleLine = !revealed, minLines = if (revealed) 3 else 1, textStyle = MaterialTheme.typography.bodyLarge.copy( diff --git a/feature/registration/src/main/java/org/signal/registration/screens/signalloginpayment/SignalLoginPaymentScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/signalloginpayment/SignalLoginPaymentScreen.kt index e283702d1d..0abf4c8610 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/signalloginpayment/SignalLoginPaymentScreen.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/signalloginpayment/SignalLoginPaymentScreen.kt @@ -8,6 +8,7 @@ package org.signal.registration.screens.signalloginpayment import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope @@ -34,6 +35,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -60,6 +62,7 @@ import org.signal.core.ui.compose.DayNightPreviews import org.signal.core.ui.compose.Dialogs import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.SignalIcons +import org.signal.core.ui.compose.TextFields import org.signal.core.ui.compose.theme.SignalTheme import org.signal.registration.R import org.signal.registration.screens.OnePaneRegistrationScaffold @@ -451,12 +454,15 @@ private fun ManualReceiptCredentialEntry( state: SignalLoginPaymentState, onEvent: (SignalLoginPaymentScreenEvents) -> Unit ) { + val interactionSource = remember { MutableInteractionSource() } + Spacer(modifier = Modifier.height(16.dp)) TextField( value = state.manualReceiptCredential.value, onValueChange = { onEvent(SignalLoginPaymentScreenEvents.ManualReceiptCredentialChanged(ManualReceiptCredential(it))) }, - label = { Text(stringResource(R.string.SignalLoginPaymentScreen__paste_a_receipt_credential)) }, + label = { TextFields.Label(stringResource(R.string.SignalLoginPaymentScreen__paste_a_receipt_credential), state.manualReceiptCredential.value.isNotEmpty(), interactionSource) }, + interactionSource = interactionSource, enabled = !state.showSpinner, maxLines = 3, modifier = Modifier