2

I am building a Jetpack Compose login screen and need to display a floating password requirement box below the TextField. The box should dynamically appear when the user starts typing in the password field and should not affect or push other UI elements. Requirements: The box should appear directly below the password field when it is focused. It should not push other UI elements down when it appears. It should disappear when the field is not focused.

Issue I am facing: I tried using Box and Card, but they push other elements down when the box appears. I also tried Popup but it display over my keyboard when I am typing, but I am not sure if that’s the best approach in Jetpack Compose. I already created password validation part just want to know how to show this box below textfield which should not affect other component and it should visible over it.

I want to create something like this:-

enter image description here

2
  • You can use a popup, just make sure to position it correctly. Here's an example: link Commented Mar 20 at 6:23
  • The best way to do this is by calculating the co-ordinates of textfield using onGlobally Positioned and then provide those co-ordinates to PopUp - Position Provider! Commented Mar 20 at 9:51

3 Answers 3

4

Here is the sample implementation that uses coordinates available from onGloballyPositioned modifier and provide them to PopupPositionProvider

@Composable
fun PasswordFieldWithPopup() {
    var password by rememberSaveable { mutableStateOf("") }
    var isPasswordFocused by remember { mutableStateOf(false) }

    var textFieldPosition by remember { mutableStateOf(Offset.Zero) }
    var textFieldSize by remember { mutableStateOf(IntSize.Zero) }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {

            TextField(
                value = password,
                onValueChange = { password = it },
                label = { Text("Password") },
                modifier = Modifier
                    .onFocusChanged { focusState ->
                        isPasswordFocused = focusState.isFocused
                    }
                    .onGloballyPositioned { coordinates ->
                        textFieldPosition = coordinates.localToWindow(Offset.Zero)
                        textFieldSize = coordinates.size
                    }
            )

            if (isPasswordFocused && password.isNotEmpty()) {
                val popupPositionProvider = remember(textFieldPosition, textFieldSize) {
                    object : PopupPositionProvider {
                        override fun calculatePosition(
                            anchorBounds: IntRect,
                            windowSize: IntSize,
                            layoutDirection: androidx.compose.ui.unit.LayoutDirection,
                            popupContentSize: IntSize
                        ): IntOffset {
                            val xPos = textFieldPosition.x.toInt()
                            val yPos = textFieldPosition.y.toInt() + textFieldSize.height

                            return IntOffset(xPos, yPos)
                        }
                    }
                }

                Popup(
                    popupPositionProvider = popupPositionProvider,
                    onDismissRequest = {  }
                ) {
                    Card(
                        modifier = Modifier
                            .wrapContentSize()
                            .padding(8.dp)
                    ) {
                        Column(modifier = Modifier.padding(16.dp)) {
                            Text("Password requirements:")
                            Text("• At least 8 characters")
                            Text("• At least 1 capital letter")
                            Text("• At least 1 number")
                        }
                    }
                }
            }

        // Just for testing
        Text(
            "Click here to see password requirements",
            modifier = Modifier
                .padding(8.dp)
        )
    }
}

enter image description here

Sign up to request clarification or add additional context in comments.

Comments

0

You can use use it directly if you don't have any other composable below the Password field and If you have then You can show that view as persistant popup or you can bind it with your content in box

Output: Output Image

Here is the composable you are looking for

@Composable
fun PasswordTextFieldWithRequirement() {
    var password by remember { mutableStateOf("") }
    var isFocused by remember { mutableStateOf(false) }

    val focusRequester = remember { FocusRequester() }
    val interactionSource = remember { MutableInteractionSource() }
    val textFieldSize = remember { mutableStateOf(IntSize.Zero) }

    val passwordRequirements = listOf(
        "At least 8 characters" to { password.length >= 8 },
        "Contains a number" to { password.any { it.isDigit() } },
        "Contains an uppercase letter" to { password.any { it.isUpperCase() } },
        "Contains a special character" to {
            password.any { "!@#$%^&*()_+-=[]{}|;:',.<>?/".contains(it) }
        }
    )

    val textFieldModifier = Modifier.onGloballyPositioned { layoutCoordinates ->
        textFieldSize.value = layoutCoordinates.size
    }

    LaunchedEffect(interactionSource) {
        interactionSource.interactions.collect { interaction ->
            when (interaction) {
                is FocusInteraction.Focus -> isFocused = true
                is FocusInteraction.Unfocus -> isFocused = false
            }
        }
    }

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        OutlinedTextField(
            value = password,
            onValueChange = { password = it },
            label = { Text("Password") },
            visualTransformation = PasswordVisualTransformation(),
            keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Password),
            interactionSource = interactionSource,
            modifier = Modifier
                .fillMaxWidth()
                .focusRequester(focusRequester)
                .then(textFieldModifier)
        )

        if (isFocused) {
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(top = 8.dp)
                    .shadow(8.dp, shape = RoundedCornerShape(8.dp))
                    .background(MaterialTheme.colorScheme.surface)
                    .border(1.dp, MaterialTheme.colorScheme.primary, RoundedCornerShape(8.dp))
                    .padding(16.dp)
            ) {
                Column {
                    Text(text = "Password Requirements", fontWeight = FontWeight.Bold)

                    Spacer(modifier = Modifier.height(8.dp))

                    passwordRequirements.forEach { (requirement, isValid) ->
                        Row(verticalAlignment = Alignment.CenterVertically) {
                            Icon(
                                imageVector = if (isValid()) Icons.Default.Check else Icons.Default.Close,
                                contentDescription = null,
                                tint = if (isValid()) Color(0xFF228B22) else Color.Red
                            )
                            Spacer(modifier = Modifier.width(8.dp))
                            Text(requirement, color = if (isValid()) Color(0xFF228B22) else Color.Red)
                        }
                    }
                }
            }
        }
    }
}

Comments

-1

Try below code this will solve your query and make customization according to your requirements.

@Composable
fun PasswordVerification() {
    var password by remember { mutableStateOf("") }
    var showPasswordRequirements by remember { mutableStateOf(false) }

    val containsLetter = password.any { it.isLetter() }
    val containsUpperCase = password.any { it.isUpperCase() }
    val containsNumber = password.any { it.isDigit() }
    val isAtLeast8Chars = password.length >= 8

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        // Username Field (Optional)
        OutlinedTextField(
            value = "user",
            onValueChange = {},
            label = { Text("Username") },
            modifier = Modifier.fillMaxWidth(),
            enabled = false // For illustration
        )

        Spacer(modifier = Modifier.height(16.dp))

        // Password TextField
        OutlinedTextField(
            value = password,
            onValueChange = {
                password = it
                showPasswordRequirements = true
            },
            label = { Text("Password") },
            visualTransformation = PasswordVisualTransformation(),
            modifier = Modifier.fillMaxWidth(),
            isError = !containsLetter || !containsUpperCase || !containsNumber || !isAtLeast8Chars
        )

        // Password Validation Hints
        if (showPasswordRequirements) {
            Column(
                modifier = Modifier
                    .padding(top = 8.dp)
                    .fillMaxWidth()
            ) {
                PasswordRequirementRow(
                    isValid = containsLetter,
                    requirementText = "At least one letter"
                )
                PasswordRequirementRow(
                    isValid = containsUpperCase,
                    requirementText = "At least one capital letter"
                )
                PasswordRequirementRow(
                    isValid = containsNumber,
                    requirementText = "At least one number"
                )
                PasswordRequirementRow(
                    isValid = isAtLeast8Chars,
                    requirementText = "Be at least 8 characters"
                )
            }
        }
    }
}

@Composable
fun PasswordRequirementRow(isValid: Boolean, requirementText: String) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier.padding(4.dp)
    ) {
        Icon(
            imageVector = if (isValid) Icons.Default.Check else Icons.Default.Close,
            contentDescription = null,
            tint = if (isValid) Color.Green else Color.Red,
            modifier = Modifier.size(20.dp)
        )
        Spacer(modifier = Modifier.width(8.dp))
        Text(
            text = requirementText,
            color = if (isValid) Color.Green else Color.Red,
            style = MaterialTheme.typography.bodySmall
        )
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.