Pantrix UI

Filled Text Area

A filled text area: a field for text of several lines, a comment, a message or a note, on a filled box that grows with what is typed and then scrolls, with its label inside the box, and help or an error's message below it and a count of its characters.

import com.pantrixui.components.inputs.filledtextarea.PantrixFilledTextArea

Examples

A comment that grows

A comment field: three lines while it is empty, with its label resting in the first line's place; a press on the field raises the label above the text, and the placeholder shows. As it is typed in the box grows a line at a time, and at eight lines the text scrolls inside the box. A sentence starts with a capital: the keyboard opens that way.

maxLength = 500 writes the count under the box. The line below reads comment.text as it is typed.

val comment = rememberTextFieldState()
PantrixFilledTextArea(
    state = comment,
    modifier = Modifier.fillMaxWidth(),
    label = "Comment",
    placeholder = "What did you think of the product?",
    supportingText = "Your comment is public",
    maxLength = 500,
    tagKey = "filled_comment"
)
PantrixText(
    text = "Lines: ${comment.text.lines().size}",
    color = PantrixTheme.colors.textSecondary
)

A field given a height

A height given to the field is the whole field's: Modifier.height(200.dp). The box fills what the count leaves of it; that height takes the place of the lines, and a text that does not fit scrolls inside the box.

The second field is given heightIn: the handle drags the box only within that range. A fixed height leaves the handle nothing to move.

PantrixFilledTextArea(
    state = rememberTextFieldState(DELIVERY_NOTES),
    modifier = Modifier.fillMaxWidth().height(200.dp),
    label = "Delivery instructions",
    showClearButton = true,
    maxLength = 400,
    tagKey = "filled_instructions"
)
PantrixFilledTextArea(
    state = rememberTextFieldState(),
    modifier = Modifier.fillMaxWidth().heightIn(max = 320.dp),
    label = "Extra note",
    supportingText = "The handle makes it 320dp tall at the most",
    tagKey = "filled_extra_note"
)

private const val DELIVERY_NOTES =
    "Use the back door of the building; the bell on the front door does not work. The lift is serviced on Fridays, " +
        "take the stairs to the third floor. If I am out, leave the parcel with the porter, Mr Hasan. Thank you!"

A soft limit and the count

The limit is soft: the text may pass 140 characters. Past it the count says how far past in words ("6 over the limit"), the count and the outline turn to the error's colour, and a screen reader is told the field's error, "Too long". Paste a long text: it is taken whole, and you can cut it where you choose.

Type and stop with a screen reader on: the count is told once, after typing stops.

PantrixFilledTextArea(
    state = rememberTextFieldState(ALMOST_A_POST),
    modifier = Modifier.fillMaxWidth(),
    label = "Post",
    supportingText = "140 characters at the most",
    minLines = 2,
    maxLength = 140,
    // The default: the text may pass the limit.
    overLimit = TextLimit.Allow,
    tagKey = "filled_post"
)

private const val ALMOST_A_POST =
    "The new version is out! A dark theme, faster search and an offline mode are here. Tell us what you think; " +
        "your comments guide us."

A limit that blocks, on a field whose text is a String

overLimit = TextLimit.Block turns down an edit past the limit whole: typing stops at the limit, and a paste that does not fit is not put in at all.

This field holds its text as a String: value and onValueChange. An edit past the limit is never asked of onValueChange. The text field underneath does not tell how far it is scrolled, so this overload's box has no bar.

var reason by remember { mutableStateOf("") }
PantrixFilledTextArea(
    value = reason,
    onValueChange = { reason = it },
    modifier = Modifier.fillMaxWidth(),
    label = "Reason for the return",
    placeholder = "In a few words",
    minLines = 2,
    maxLines = 4,
    maxLength = 80,
    overLimit = TextLimit.Block,
    tagKey = "filled_return_reason"
)

Sending with Ctrl/Cmd+Enter

onSubmit is called by Ctrl+Enter or Cmd+Enter on a hardware keyboard; Enter alone starts a new line. A software keyboard has no such keys, so a "Send" button stands beside it as well. Type a message and press Ctrl+Enter: the app's snackbar says the message was sent, and the field is emptied.

The send is reported as an event (submit); what was typed never is.

val snackbars = LocalPantrixSnackbars.current
val scope = rememberCoroutineScope()
val message = rememberTextFieldState()
val send = {
    if (message.text.isNotBlank()) {
        scope.launch { snackbars?.show(message = "Message sent", tagKey = "filled_message_sent") }
        message.clearText()
    }
}
PantrixFilledTextArea(
    state = message,
    modifier = Modifier.fillMaxWidth(),
    label = "Message",
    placeholder = "Send with Ctrl+Enter or Cmd+Enter",
    minLines = 2,
    maxLines = 6,
    onSubmit = send,
    tagKey = "filled_message"
)
PantrixButton(text = "Send", onClick = send, tagKey = "filled_send_message")

Bound to a form's field

The PantrixFilledTextArea that takes a field takes its value, its error and its name from the form's field: no tagKey is given, the field's id is the automation key (FilledTextArea_<id>). With form given, Ctrl+Enter or Cmd+Enter submits the form through the "Save" button.

The limit is soft, but the form is not submitted with a text past it: the field is in error with "Too long", the count says how far past, and the focus goes to the field. Leave the field empty, or go past 160 characters, and press "Save".

val snackbars = LocalPantrixSnackbars.current
val scope = rememberCoroutineScope()
val form = rememberFormState()
PantrixFilledTextArea(
    field = form.field("filled_bio", initial = "", Rules.notBlank("Tell us about yourself in a few sentences")),
    modifier = Modifier.fillMaxWidth(),
    form = form,
    label = "About me",
    supportingText = "Shown on your profile",
    maxLength = 160
)
PantrixButton(
    text = "Save",
    onClick = {
        scope.launch { form.submit { snackbars?.show(message = "Profile saved", tagKey = "filled_bio_saved") } }
    },
    form = form,
    tagKey = "filled_save_bio"
)

Disabled and read-only

enabled = false draws the field in the theme's disabled colours: it takes no focus, cannot be typed into, has no handle, and a screen reader is told it is disabled.

readOnly = true only stops changes: the field is focused, its text selected, copied and scrolled; it can be made taller with the handle and read.

PantrixFilledTextArea(
    state = rememberTextFieldState("Your order has shipped; no note can be added."),
    modifier = Modifier.fillMaxWidth(),
    enabled = false,
    label = "Shipping note",
    minLines = 2,
    tagKey = "filled_shipping_note"
)
PantrixFilledTextArea(
    state = rememberTextFieldState(RETURN_POLICY),
    modifier = Modifier.fillMaxWidth(),
    readOnly = true,
    label = "Return policy",
    maxLines = 4,
    tagKey = "filled_return_policy"
)

private const val RETURN_POLICY =
    "You can return the product within 14 days of the day you received it. It must be unused, with its labels " +
        "still on and in its original packaging. We pay for the return shipping. Your money goes back to the card you " +
        "paid with within 3 working days of the product reaching our warehouse."

Its own colours and style

colors and style are the filled text field's: FilledTextFieldColors and FilledTextFieldStyle. The colours that are given are used, and the rest stay the theme's roles. The first field's outline with the focus, its caret and its scroll bar are the app's own purple; the handle and the clear button take the icon's colour.

The second field's style draws the box rounder with ShapeRole.Large and gives it more padding.

val own = ownColors()
PantrixFilledTextArea(
    state = rememberTextFieldState(DELIVERY_NOTES),
    modifier = Modifier.fillMaxWidth(),
    label = "Note",
    showClearButton = true,
    maxLines = 4,
    colors = FilledTextFieldColors(
        focusedOutline = own.color,
        cursor = own.color,
        icon = own.color,
        scrollBar = own.color
    ),
    tagKey = "filled_own_colors"
)
PantrixFilledTextArea(
    state = rememberTextFieldState(),
    modifier = Modifier.fillMaxWidth(),
    label = "Description",
    style = FilledTextFieldStyle(shape = ShapeRole.Large, horizontalPadding = 20.dp, verticalPadding = 16.dp),
    tagKey = "filled_own_style"
)

private const val DELIVERY_NOTES =
    "Use the back door of the building; the bell on the front door does not work. The lift is serviced on Fridays, " +
        "take the stairs to the third floor. If I am out, leave the parcel with the porter, Mr Hasan. Thank you!"

The handle that resizes

On a desktop and on the web, drag the handle in the box's bottom corner: the box gets taller or shorter, and the pointer turns to an up-and-down resize arrow. A dragged box keeps that height and does not grow with its text; nor is it ever shorter than its least lines. A phone has no handle.

The second field has resizable = false: no handle, and the box grows with its text alone.

PantrixFilledTextArea(
    state = rememberTextFieldState(),
    modifier = Modifier.fillMaxWidth(),
    label = "Meeting notes",
    supportingText = "Make it taller with the handle in the bottom corner",
    tagKey = "filled_meeting_notes"
)
PantrixFilledTextArea(
    state = rememberTextFieldState(),
    modifier = Modifier.fillMaxWidth(),
    label = "Short note",
    minLines = 2,
    maxLines = 4,
    resizable = false,
    tagKey = "filled_short_note"
)

Right to left

In a right-to-left layout the icon and the clear button swap places, the handle moves to the bottom corner at the left, and the scroll bar stands at the box's left: that is where a line ends. Each paragraph takes its direction from its own first letter.

CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
    PantrixFilledTextArea(
        state = rememberTextFieldState(ARABIC_NOTE),
        modifier = Modifier.fillMaxWidth(),
        label = "ملاحظة",
        leadingIcon = {
            PantrixIcon(
                imageVector = PantrixIcons.Person,
                contentDescription = null,
                size = PantrixTheme.style(FilledTextFieldStyle).iconSize
            )
        },
        showClearButton = true,
        maxLines = 3,
        maxLength = 200,
        tagKey = "filled_arabic_note"
    )
}

private const val ARABIC_NOTE = "يرجى ترك الطرد عند البواب إذا لم أكن في المنزل. شكراً لكم!"

API

state

A filled text area: a field for text of several lines, a comment, a message or a note, on a filled box that grows with what is typed and then scrolls, with its label inside the box, and help or an error's message below it and a count of its characters.

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. A height is the whole field's as well, in place of its lines: the box fills what the supporting text leaves of it, and its text scrolls in it. The handle moves the box only within such a height, so a field that is to be dragged is given heightIn and not height.
enabledBooleantruewhether the field can be typed into. One that cannot takes no focus, is drawn in the disabled colours, has no handle and says so.
readOnlyBooleanfalsewhether the text can only be read: it can still be focused, selected, copied, scrolled and dragged taller.
labelString?nullwhat the field is for, inside the box: in the place of the first line while the field is empty and without the focus, and above the text otherwise. It is the field's name for a screen reader.
placeholderString?nullshown in the box while it is empty. It is no label: it goes as soon as the user types.
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 outline, the supporting text and the caret turn to the danger role, and a screen reader is told. A text past a soft maxLength is drawn in error as well.
leadingIcon(@Composable () -> Unit)?nullwhat stands before the text, at its first line, 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 at its first line, and after the clear button.
showClearButtonBooleanfalseshows a button at the first line that empties the field, while the field can be typed into and is not empty. It leaves the caret in the field. It is no stop for Tab, and a screen reader still reaches it as a button.
minLinesInt3how many lines tall the box is at the least: while it is empty, and when it is dragged its shortest. At least one.
maxLinesInt8how many lines tall the box grows to with its text before the text scrolls in it. No fewer than minLines: the two the same give a box that keeps its height.
resizableBooleantruewhether a handle in the box's bottom corner drags it taller or shorter. Only where the platform is used with a pointer, a desktop's or a browser's: on a phone a finger on the box scrolls its text.
maxLengthInt?nullthe most characters the text should hold, counted as String.length counts them, so that an emoji can count as two. What an edit past it does is overLimit's. It has to be positive.
overLimitTextLimitTextLimit.Allowwhat an edit past maxLength does: TextLimit.Allow takes it and shows that the text is too long, and TextLimit.Block turns it down whole.
showCharacterCountBooleantrueshows the count under the box while maxLength is set: 120/500, and past the limit how far past it, 12 over the limit. A screen reader is told it once typing pauses, at the limit and past it.
onSubmit(() -> Unit)?nullcalled for Ctrl and Enter, or Cmd and Enter, on a hardware keyboard: the text is sent. Null leaves both keys to the field. It is reported, and the text never is.
keyboardOptionsKeyboardOptionsKeyboardOptions(capitalization = KeyboardCapitalization.Sentences)the keyboard's type, its capitals and its corrections: a sentence starts with a capital unless told otherwise. The keyboard's action is always the new line, whatever is named here.
inputTransformationInputTransformation?nullwhat is done to the text as the user types it. With a maxLength that blocks, it comes first and the limit applies to what it lets through.
outputTransformationOutputTransformation?nullhow the text is shown, without changing it.
styleFilledTextFieldStyle?nullthis call's own style, the filled text field's: over every enclosing one and under the density. Only the fields it sets change, on every platform.
colorsFilledTextFieldColors?nullthis call's own colours, the filled text field's, in place of the theme's roles: the ones it sets are used, and the rest stay the theme's. A disabled field is drawn in the theme's disabled colours 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, so that the field is not named twice while it is empty.
tagKeyString?nullthe field's automation key: the field is FilledTextArea_<tagKey>, its clear button FilledTextAreaClear_<tagKey> and its handle FilledTextAreaResize_<tagKey>. The field reports that it took the focus, that it was cleared and that it was submitted, 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.

value

A text area whose text is a String its caller holds: value is what it shows, and onValueChange is asked for every change the user makes.

ParameterTypeDefaultWhat it is
valueStringthe text the field shows.
onValueChange(String) -> Unitasked for each change the user makes, with the whole new text, and by the clear button with an empty one.
modifierModifierModifierthe modifier of the whole field: its box and what is under it. A height is the whole field's, in place of its lines.
enabledBooleantruewhether the field can be typed into. One that cannot takes no focus, is drawn in the disabled colours, has no handle and says so.
readOnlyBooleanfalsewhether the text can only be read: it can still be focused, selected and copied.
labelString?nullwhat the field is for, inside the box: in the place of the first line while the field is empty and without the focus, and above the text otherwise. It is the field's name for a screen reader.
placeholderString?nullshown in the box while it is empty, once the label has risen above it.
supportingTextString?nullhelp under the box. 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 outline, the supporting text and the caret turn to the danger role, and a screen reader is told. A text past a soft maxLength is drawn in error as well.
leadingIcon(@Composable () -> Unit)?nullwhat stands before the text, at its first line.
trailingIcon(@Composable () -> Unit)?nullwhat stands after the text at its first line, and after the clear button.
showClearButtonBooleanfalseshows a button at the first line that asks for an empty text, while the field can be typed into and is not empty. It leaves the caret in the field, and is no stop for Tab.
minLinesInt3how many lines tall the box is at the least. At least one.
maxLinesInt8how many lines tall the box grows to with its text before the text scrolls in it. No fewer than minLines.
resizableBooleantruewhether a handle in the box's bottom corner drags it taller or shorter, where the platform is used with a pointer.
maxLengthInt?nullthe most characters the text should hold, counted as String.length counts them. What an edit past it does is overLimit's. It has to be positive.
overLimitTextLimitTextLimit.Allowwhat an edit past maxLength does: TextLimit.Allow asks for it and shows that the text is too long, and TextLimit.Block turns it down before onValueChange is asked.
showCharacterCountBooleantrueshows the count under the box while maxLength is set, and past the limit how far past it.
onSubmit(() -> Unit)?nullcalled for Ctrl and Enter, or Cmd and Enter, on a hardware keyboard. Null leaves both keys to the field.
keyboardOptionsKeyboardOptionsKeyboardOptions(capitalization = KeyboardCapitalization.Sentences)the keyboard's type, its capitals and its corrections. The keyboard's action is always the new line, whatever is named here.
styleFilledTextFieldStyle?nullthis call's own style, the filled text field's: over every enclosing one and under the density.
colorsFilledTextFieldColors?nullthis call's own colours, the filled text field's, in place of the theme's roles. A disabled field is drawn in the theme's disabled colours whatever is given.
contentDescriptionString?nullwhat a screen reader says for the field where it has no label.
tagKeyString?nullthe field's automation key: the field is FilledTextArea_<tagKey>, its clear button FilledTextAreaClear_<tagKey> and its handle FilledTextAreaResize_<tagKey>. The field reports that it took the focus, that it was cleared and that it was submitted, and never its text.
interactionSourceMutableInteractionSource?nullthe field's interactions, hoisted. It keeps a source of its own when this is null.

field

A text area 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 area shows and changes.
modifierModifierModifierthe modifier of the whole field.
formFormState?nullgiven, Ctrl and Enter, or Cmd and Enter, submit the form through the button registered with it: the form is checked, and then submitted or the focus sent to its first field in error. Enter alone starts a new line. An onSubmit of the caller's own takes the place of the form's submit.
enabledBooleantruewhether the field can be typed into.
readOnlyBooleanfalsewhether the text can only be read.
labelString?nullwhat the field is for, inside the box: the field's name for a screen reader.
placeholderString?nullshown in the box while it is empty.
supportingTextString?nullhelp under the box, while the field has no error to show there.
leadingIcon(@Composable () -> Unit)?nullwhat stands before the text, at its first line.
trailingIcon(@Composable () -> Unit)?nullwhat stands after the text at its first line, and after the clear button.
showClearButtonBooleanfalseshows a button that empties the field, while it can be typed into and is not empty.
minLinesInt3how many lines tall the box is at the least. At least one.
maxLinesInt8how many lines tall the box grows to with its text before the text scrolls in it. No fewer than minLines.
resizableBooleantruewhether a handle in the box's bottom corner drags it taller or shorter, where the platform is used with a pointer.
maxLengthInt?nullthe most characters the text should hold. It has to be positive.
overLimitTextLimitTextLimit.Allowwhat an edit past maxLength does: TextLimit.Allow takes it and puts the field in error, and TextLimit.Block turns it down whole.
showCharacterCountBooleantrueshows the count under the box while maxLength is set, and past the limit how far past it.
onSubmit(() -> Unit)?nullcalled for Ctrl and Enter, or Cmd and Enter, on a hardware keyboard, in place of the form's submit.
keyboardOptionsKeyboardOptionsKeyboardOptions(capitalization = KeyboardCapitalization.Sentences)the keyboard's type, its capitals and its corrections. The keyboard's action is always the new line, whatever is named here.
inputTransformationInputTransformation?nullwhat is done to the text as the user types it.
outputTransformationOutputTransformation?nullhow the text is shown, without changing it.
styleFilledTextFieldStyle?nullthis call's own style, the filled text field's: over every enclosing one and under the density.
colorsFilledTextFieldColors?nullthis call's own colours, the filled text field's, in place of the theme's roles. A disabled field is drawn in the theme's disabled colours 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.