Pantrix UI

Filled Secure Text Field

A secure text field whose label is inside its box: a password, a PIN or a card's security code in the filled box of PantrixFilledTextField.

import com.pantrixui.components.inputs.filledsecuretextfield.PantrixFilledSecureTextField

Examples

At its plainest

The least there is to write: a TextFieldState and a label. The label is inside the box: it rests in the text's place while the field is empty, and moves up to its own line above the text when the field is pressed. What is typed is drawn as dots, and cannot be cut or copied.

Type something and press the eye button at the end of the box: the text shows, and another press hides it again. A press with a mouse does not take the caret out of the field; with a keyboard, Tab stops at the button after the field. That the text was shown is not saved.

PantrixFilledSecureTextField(
    state = rememberTextFieldState(),
    modifier = Modifier.fillMaxWidth(),
    label = "Vault password",
    tagKey = "safe_password"
)

Supporting text and error

supportingText stands under the box. error = true outlines the box in the danger colour; the supporting text and the caret turn to that colour too, and a screen reader announces the message as the error. The label keeps its own colour.

The field starts in error: there is a space in its text. Press the eye button to see the text, and delete the space: the error goes. It is the caller, not the field, that works out the error; here by reading password.text.

val password = rememberTextFieldState("secret password")
val hasSpace = password.text.contains(' ')
PantrixFilledSecureTextField(
    state = password,
    modifier = Modifier.fillMaxWidth(),
    label = "App lock",
    supportingText = if (hasSpace) "A password cannot contain a space" else "A password without spaces",
    error = hasSpace,
    showClearButton = true,
    tagKey = "app_lock"
)

Icon, clear button and count

leadingIcon stands before the label and the text. A slot does not size what is put in it: the example itself gives this icon the size in the filled field's style, PantrixTheme.style(FilledTextFieldStyle).iconSize. What is at the box's end is always in the same order: first the clear button, then the button that shows the text, and last trailingIcon, where one is given.

The clear button shows while the field is not empty, leaves the caret in the field and is no stop for Tab; the eye button is a Tab stop. maxLength = 16 turns down a longer password, and writes the count under the box.

PantrixFilledSecureTextField(
    state = rememberTextFieldState(),
    modifier = Modifier.fillMaxWidth(),
    label = "Admin password",
    leadingIcon = {
        PantrixIcon(
            imageVector = PantrixIcons.Person,
            contentDescription = null,
            size = PantrixTheme.style(FilledTextFieldStyle).iconSize
        )
    },
    showClearButton = true,
    maxLength = 16,
    tagKey = "admin_password"
)

A password change bound to a form

Both fields get their value, their error and their name from their field in the form. In the current password the keyboard's "Next" key moves the focus to the new password; in the new password the action key submits the form through the "Change password" button. Leave both fields empty and press the button: the form is not submitted, and the first field in error takes the focus.

The platform is told that the new password is one that is being chosen (ContentType.NewPassword): a password manager offers to suggest a new one here, not to fill in the saved one. The current password was given no contentType: the field holds ContentType.Password.

val snackbars = LocalPantrixSnackbars.current
val scope = rememberCoroutineScope()
val form = rememberFormState()
PantrixFilledSecureTextField(
    field = form.field("current_password", initial = "", Rules.notBlank("Enter your current password")),
    modifier = Modifier.fillMaxWidth(),
    form = form,
    label = "Current password",
    keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next)
)
PantrixFilledSecureTextField(
    field = form.field("new_password", initial = "", Rules.minLength(8, "The new password must be at least 8 characters")),
    modifier = Modifier.fillMaxWidth(),
    form = form,
    label = "New password",
    supportingText = "At least 8 characters",
    showClearButton = true,
    contentType = ContentType.NewPassword
)
PantrixButton(
    text = "Change password",
    onClick = {
        scope.launch {
            form.submit { snackbars?.show(message = "Password changed", tagKey = "password_changed") }
        }
    },
    form = form,
    tagKey = "change_password"
)

A security code read when it is used

The three-digit security code on the back of a card: its keyboard is the number keyboard, an input transformation turns down what is not a digit, and its count is not shown. The eye button stays, so that the user can compare the code they typed with the one on the card.

The text is read from the state when it is used: code.text, when the button is pressed. It is carried nowhere in between; the field's state is its only source. The notification says how many digits were received, not the code itself.

val snackbars = LocalPantrixSnackbars.current
val scope = rememberCoroutineScope()
val code = rememberTextFieldState()
PantrixFilledSecureTextField(
    state = code,
    modifier = Modifier.fillMaxWidth(),
    label = "Security code",
    placeholder = "000",
    supportingText = "The three digits on the back of the card",
    maxLength = 3,
    showCharacterCount = false,
    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
    inputTransformation = DigitsOnly,
    contentType = ContentType.CreditCardSecurityCode,
    tagKey = "card_code"
)
PantrixButton(
    text = "Verify card",
    onClick = {
        val entered = code.text.toString()
        val message = when (entered.length) {
            3 -> "Code received: ${entered.length} digits"
            else -> "The code must be 3 digits, you entered ${entered.length}"
        }
        scope.launch { snackbars?.show(message = message, tagKey = "card_code_read") }
    },
    tagKey = "verify_card"
)

private val DigitsOnly = InputTransformation {
    if (!asCharSequence().all(Char::isDigit)) revertAllChanges()
}

A text followed as it is typed

A caller that wants the text as it is typed follows it from the state: snapshotFlow { state.text.toString() } gives every change, here to a String the caller holds. The supporting text is made from that String, and says what the password still lacks.

The flow goes one way: the text flows from the field to the caller. Carrying it to a ViewModel is written the same way: each change is reported to the ViewModel inside collect.

var password by remember { mutableStateOf("") }
val state = rememberTextFieldState(password)
LaunchedEffect(state) {
    snapshotFlow { state.text.toString() }.collect { password = it }
}
PantrixFilledSecureTextField(
    state = state,
    modifier = Modifier.fillMaxWidth(),
    label = "New account password",
    supportingText = when {
        password.isEmpty() -> "At least 10 characters, including a digit"
        password.length < 10 -> "Characters to go: ${10 - password.length}"
        password.none(Char::isDigit) -> "Add a digit"
        else -> "The password is good enough"
    },
    contentType = ContentType.NewPassword,
    tagKey = "followed_password"
)

A PIN that is never shown

showRevealButton = false takes away the eye button: the text stays as textObfuscationMode draws it. With TextObfuscationMode.Hidden the last digit typed shows on no platform; the default, System, would show the last character typed for a moment on Android, where the system's own setting says so. textObfuscationCharacter draws an asterisk in place of the dot.

Its keyboard is a PIN's: KeyboardType.NumberPassword. maxLength = 4 takes no fifth digit, and writes the count under the box.

PantrixFilledSecureTextField(
    state = rememberTextFieldState(),
    modifier = Modifier.fillMaxWidth(),
    label = "Card PIN",
    supportingText = "Four digits",
    showRevealButton = false,
    maxLength = 4,
    textObfuscationMode = TextObfuscationMode.Hidden,
    textObfuscationCharacter = '*',
    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
    inputTransformation = DigitsOnly,
    tagKey = "card_pin"
)

private val DigitsOnly = InputTransformation {
    if (!asCharSequence().all(Char::isDigit)) revertAllChanges()
}

Disabled and read-only

enabled = false takes the field's fill away: the field is drawn in the theme's disabled text colour, takes no focus and has no button to show its text.

With readOnly = true the field keeps its fill: it can be focused, and its text shown with the eye button, but it cannot be changed.

PantrixFilledSecureTextField(
    state = rememberTextFieldState("unchanging"),
    modifier = Modifier.fillMaxWidth(),
    enabled = false,
    label = "App password",
    supportingText = "Set by your organisation",
    tagKey = "managed_password"
)
PantrixFilledSecureTextField(
    state = rememberTextFieldState("PX-7730-1184"),
    modifier = Modifier.fillMaxWidth(),
    readOnly = true,
    label = "Backup code",
    supportingText = "Shown with the eye button, cannot be changed",
    tagKey = "backup_code"
)

Its own colours and style

The filled secure field takes the filled text field's FilledTextFieldColors and FilledTextFieldStyle: the two filled fields are restyled together.

The first field's fill, label, text, icon, outline and caret are from the app's own purple; the eye button is drawn in the icon colour. What is not given, such as the fill with the focus, stays the theme's role. The second's box is fully round: FilledTextFieldStyle(shape = ShapeRole.Full).

val own = ownColors()
PantrixFilledSecureTextField(
    state = rememberTextFieldState(),
    modifier = Modifier.fillMaxWidth(),
    label = "Membership password",
    colors = FilledTextFieldColors(
        container = own.container,
        focusedOutline = own.color,
        text = own.onContainer,
        label = own.onContainer,
        icon = own.onContainer,
        cursor = own.color
    ),
    tagKey = "own_colors"
)
PantrixFilledSecureTextField(
    state = rememberTextFieldState(),
    modifier = Modifier.fillMaxWidth(),
    label = "Archive password",
    style = FilledTextFieldStyle(shape = ShapeRole.Full),
    tagKey = "pill"
)

API

state

A secure text field whose label is inside its box: a password, a PIN or a card's security code in the filled box of PantrixFilledTextField.

ParameterTypeDefaultWhat it is
stateTextFieldStatethe text and the selection. Create it with rememberTextFieldState(), and read what was typed from it: state.text where the text is used, or snapshotFlow { state.text.toString() } to follow it as it changes.
modifierModifierModifierthe modifier of the whole field: its box and what is under it. fillMaxWidth() makes the box as wide as its parent.
enabledBooleantruewhether the field can be typed into. One that cannot takes no focus, is drawn without its fill in the disabled colours, has no button to show its text and says so.
readOnlyBooleanfalsewhether the text can only be read: it can still be focused, and shown by the button.
labelString?nullwhat the field is for, written inside the box: the field's name for a screen reader. Without one the text stands in the middle of the box, and the placeholder shows whenever the field is empty.
placeholderString?nullshown where the text goes while the field is empty and its label is out of the way, which is while it has the focus.
supportingTextString?nullhelp under the box, in a smaller type. With error it is the message that says what to put right, which a screen reader announces as the error.
errorBooleanfalsewhether the text is wrong: the box is outlined in the danger role's colour, the supporting text and the caret turn to it, and a screen reader is told.
leadingIcon(@Composable () -> Unit)?nullwhat stands before the label and the text, in the secondary text colour. A slot does not size what is put in it: PantrixIcon(size = PantrixTheme.style(FilledTextFieldStyle).iconSize) is the platform's size.
trailingIcon(@Composable () -> Unit)?nullwhat stands after the text, after the clear button and the button that shows the text.
showClearButtonBooleanfalseshows a button that empties the field, while the field can be typed into and is not empty. It leaves the caret in the field, and is no stop for Tab.
showRevealButtonBooleantrueshows the button that shows the text and hides it again, while the field is enabled. Without it the text stays as textObfuscationMode draws it: for a text that is never to be shown, and for a caller that shows it another way.
maxLengthInt?nullthe most characters the user can enter, counted as String.length counts them. An edit that would pass the limit is turned down whole. Text set in code is not cut, and the user can still make it shorter. It has to be positive.
showCharacterCountBooleantrueshows count/maxLength under the box while maxLength is set.
textObfuscationModeTextObfuscationModeTextObfuscationMode.Systemhow the text is drawn while it is hidden. TextObfuscationMode.System does as the platform does: on Android the last character typed shows for a moment where the system's own setting says so, and nowhere else does anything show. Hidden shows nothing on any platform, and RevealLastTyped the last character on every one.
textObfuscationCharacterChar'•'what is drawn in place of a hidden character.
keyboardOptionsKeyboardOptionsKeyboardOptions.Defaultthe keyboard's type, its capitals, its corrections and its action key. What these leave unset is a password's, the password keyboard and no corrections, so that naming an action does not bring back a keyboard that learns what is typed. KeyboardType.NumberPassword is a PIN's.
onKeyboardActionKeyboardActionHandler?nullcalled for the keyboard's action key, for the action keyboardOptions names. Null keeps what the platform does.
inputTransformationInputTransformation?nullwhat is done to the text as the user types it. With maxLength, it comes first and the limit applies to what it lets through.
contentTypeContentType?nullwhat the field holds, for the platform to fill it in. Left out, the field holds ContentType.Password, as Foundation's secure field says for itself: ContentType.NewPassword is one that is being chosen.
styleFilledTextFieldStyle?nullthis call's own style: over every enclosing one and under the density. Only the fields it sets change, on every platform.
colorsFilledTextFieldColors?nullthis call's own colours, in place of the theme's roles: the ones it sets are used, and the rest stay the theme's. A disabled field has no fill and is drawn in the theme's disabled text colour whatever is given.
contentDescriptionString?nullwhat a screen reader says for the field: its name where it has no label. A placeholder that says the same is then left to the eye.
tagKeyString?nullthe field's automation key: the field is FilledSecureTextField_<tagKey>, its clear button FilledSecureTextFieldClear_<tagKey> and the button that shows its text FilledSecureTextFieldReveal_<tagKey>. The field reports that it took the focus, that it was cleared and that its text was shown or hidden, and never its text.
interactionSourceMutableInteractionSource?nullthe field's interactions, hoisted: its focus, its hover and its presses. It keeps a source of its own when this is null.

field

A filled secure text field bound to a form's field: its text, its error and its name come from field.

ParameterTypeDefaultWhat it is
fieldFormField<String>the field the text field shows and changes.
modifierModifierModifierthe modifier of the whole field.
formFormState?nullgiven, the keyboard's action key submits the form through the button registered with it. An action of Next or Previous moves the focus as it always does. A field that names no action gets ImeAction.Done, since the default action never reaches a handler. An onKeyboardAction of the caller's own takes the place of the submit.
enabledBooleantruewhether the field can be typed into.
readOnlyBooleanfalsewhether the text can only be read.
labelString?nullwhat the field is for, written inside the box: the field's name for a screen reader.
placeholderString?nullshown where the text goes while the field is empty and its label is out of the way.
supportingTextString?nullhelp under the box, while the field has no error to show there.
leadingIcon(@Composable () -> Unit)?nullwhat stands before the label and the text.
trailingIcon(@Composable () -> Unit)?nullwhat stands after the text, after the clear button and the button that shows the text.
showClearButtonBooleanfalseshows a button that empties the field, while it can be typed into and is not empty.
showRevealButtonBooleantrueshows the button that shows the text and hides it again, while the field is enabled.
maxLengthInt?nullthe most characters the user can enter. It has to be positive.
showCharacterCountBooleantrueshows count/maxLength under the box while maxLength is set.
textObfuscationModeTextObfuscationModeTextObfuscationMode.Systemhow the text is drawn while it is hidden.
textObfuscationCharacterChar'•'what is drawn in place of a hidden character.
keyboardOptionsKeyboardOptionsKeyboardOptions.Defaultthe keyboard's type, its capitals, its corrections and its action key. What these leave unset is a password's.
onKeyboardActionKeyboardActionHandler?nullcalled for the keyboard's action key, in place of the form's submit.
inputTransformationInputTransformation?nullwhat is done to the text as the user types it.
contentTypeContentType?nullwhat the field holds, for the platform to fill it in. Left out, it is ContentType.Password.
styleFilledTextFieldStyle?nullthis call's own style: over every enclosing one and under the density. Only the fields it sets change, on every platform.
colorsFilledTextFieldColors?nullthis call's own colours, in place of the theme's roles: the ones it sets are used, and the rest stay the theme's. A disabled field has no fill and is drawn in the theme's disabled text colour whatever is given.
contentDescriptionString?nullwhat a screen reader says for the field where it has no label.
interactionSourceMutableInteractionSource?nullthe field's interactions, hoisted. It keeps a source of its own when this is null.

FilledTextFieldStyle

PantrixFilledTextField's box: its shape, how tall and how wide it is at the least, the room around what it holds, its icons and its outline.

ParameterTypeDefaultWhat it is
shapeShapeSpecShapeSpec.UnspecifiedThe box's shape. A fully round one, ShapeRole.Full, is a pill only while the field holds one line: a field of several lines takes ShapeRole.Large instead, since a capsule's curve runs into the first line of its text.
heightDpDp.UnspecifiedThe least the box is tall.
minWidthDpDp.UnspecifiedThe least the box is wide, which keeps an empty field from growing as the user types into it.
horizontalPaddingDpDp.UnspecifiedThe room between the box's edge and its label and text, where no icon is.
verticalPaddingDpDp.UnspecifiedThe room above the label's line and below the last line of the text.
iconPaddingDpDp.UnspecifiedThe room around an icon in its slot.
iconSizeDpDp.UnspecifiedThe size an icon in a slot should take. A slot does not size what is put in it.
borderWidthDpDp.UnspecifiedHow thick the outline is that the box takes with the focus, in error and under a pointer. At rest it has none.
textRoleTypeRole?nullThe role of the type scale the text is typed in, the label rests in and the placeholder is set in: the body's, 16sp, on Android and iOS, as Material 3 types a field, and the label's, 14sp, on the desktop and the web, as a page under a pointer does.

FilledTextFieldColors

What PantrixFilledTextField is painted with, for a field that does not take the theme's: a colour that is set is used, and one that is left out stays the theme's role. A product gives the colours it changes and no others:

ParameterTypeDefaultWhat it is
containerColorColor.UnspecifiedThe box's fill at rest. The theme's neutral container.
focusedContainerColorColor.UnspecifiedThe box's fill under an outline: with the focus, and in error. The theme's surface.
hoveredOutlineColorColor.UnspecifiedThe outline under a pointer. The theme's borderHover.
focusedOutlineColorColor.UnspecifiedThe outline while the field has the focus. The theme's primary.
errorColorColor.UnspecifiedWhat says the text is wrong: the outline, the message under the box, the caret, and the count once it is past its limit. The theme's danger.
textColorColor.UnspecifiedThe typed text. The theme's textPrimary.
labelColorColor.UnspecifiedThe label, at rest in the text's place and on its own line above the text. The theme's textSecondary.
placeholderColorColor.UnspecifiedThe placeholder. The theme's textSecondary.
supportingTextColorColor.UnspecifiedThe help under the box, and the count. The theme's textSecondary.
iconColorColor.UnspecifiedWhat an icon in a slot is drawn in unless it sets a tint of its own, and the clear button's cross. The theme's textSecondary.
cursorColorColor.UnspecifiedThe caret, and with it the handles and the highlight of a selection. The theme's primary.
scrollBarColorColor.UnspecifiedThe bar that shows how far the text of a box of several lines is scrolled. The theme's borderStrong: it is a mark on the box's fill, and a box's soft edge would not be seen there.

Accessibility