Pantrix UI

Multi Select

Several choices out of a list the control opens: the projects a page is narrowed to, the days a shop is open.

import com.pantrixui.components.overlay.multiselect.PantrixMultiSelect

Examples

Projects

A filter that narrows a list by project. Every row is a check box: a press ticks it or clears it, and the list stays open. The box shows the labels of what is ticked, in the options' order, and the placeholder while nothing is ticked. Here all the projects are shown while none is ticked, and the box says so in its placeholder.

It has no label; contentDescription tells a screen reader its name. The card's "Last event" line says what onSelectionChange last asked for; most of the other examples have the same line.

var chosen by rememberSaveable { mutableStateOf(emptySet<String>()) }
val report = LocalExampleEvents.current
PantrixMultiSelect(
    options = PROJECTS,
    selected = chosen,
    onSelectionChange = {
        chosen = it
        report(if (it.isEmpty()) "Showing all projects" else "Projects selected: ${it.size}")
    },
    modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
    placeholder = "All projects (${PROJECTS.size})",
    contentDescription = "Projects",
    tagKey = "projects"
)

private val PROJECTS = listOf(
    SelectOption(value = "compose", label = "Rorty Compose Android", icon = PantrixIcons.Folder, tagKey = "compose"),
    SelectOption(value = "android", label = "Rorty Android", icon = PantrixIcons.Folder, tagKey = "android"),
    SelectOption(value = "swiftui", label = "Rorty SwiftUI iOS", icon = PantrixIcons.Folder, tagKey = "swiftui"),
    SelectOption(value = "uikit", label = "Rorty UIKIT iOS", icon = PantrixIcons.Folder, tagKey = "uikit")
)

Summary text

Given valueText, the box shows it in place of the labels while something is ticked: here "5 days". With many options ticked the box does not overflow.

With nothing ticked the placeholder still shows: clear every day and the box says "Closed".

var chosen by rememberSaveable { mutableStateOf(setOf("mon", "tue", "wed", "thu", "fri")) }
val report = LocalExampleEvents.current
PantrixMultiSelect(
    options = DAYS,
    selected = chosen,
    onSelectionChange = {
        chosen = it
        report("Days selected: ${it.size}")
    },
    modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
    label = "Open days",
    placeholder = "Closed",
    valueText = "${chosen.size} days",
    tagKey = "open_days"
)

private val DAYS = listOf(
    SelectOption(value = "mon", label = "Monday", tagKey = "mon"),
    SelectOption(value = "tue", label = "Tuesday", tagKey = "tue"),
    SelectOption(value = "wed", label = "Wednesday", tagKey = "wed"),
    SelectOption(value = "thu", label = "Thursday", tagKey = "thu"),
    SelectOption(value = "fri", label = "Friday", tagKey = "fri"),
    SelectOption(value = "sat", label = "Saturday", tagKey = "sat"),
    SelectOption(value = "sun", label = "Sunday", tagKey = "sun")
)

Searchable

searchable = true puts a search field in the list: the options narrow as you type. The focus stays in the search field: the arrows move among the rows that match, the keyboard's action key ticks or clears the row the arrows are on, and the search stays as it is.

Options the search does not show keep their ticks: tick a few languages, then search for something else. The labels in the box and the count in the card's "Last event" line do not change.

var chosen by rememberSaveable { mutableStateOf(emptySet<String>()) }
val report = LocalExampleEvents.current
PantrixMultiSelect(
    options = SKILLS,
    selected = chosen,
    onSelectionChange = {
        chosen = it
        report("Languages selected: ${it.size}")
    },
    modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
    label = "Languages you know",
    placeholder = "Choose languages",
    searchable = true,
    searchPlaceholder = "Search languages…",
    tagKey = "skills"
)

private val SKILLS = listOf(
    SelectOption(value = "Kotlin", label = "Kotlin", tagKey = "kotlin"),
    SelectOption(value = "Java", label = "Java", tagKey = "java"),
    SelectOption(value = "Swift", label = "Swift", tagKey = "swift"),
    SelectOption(value = "Dart", label = "Dart", tagKey = "dart"),
    SelectOption(value = "TypeScript", label = "TypeScript", tagKey = "typescript"),
    SelectOption(value = "JavaScript", label = "JavaScript", tagKey = "javascript"),
    SelectOption(value = "Python", label = "Python", tagKey = "python"),
    SelectOption(value = "Go", label = "Go", tagKey = "go"),
    SelectOption(value = "Rust", label = "Rust", tagKey = "rust"),
    SelectOption(value = "C#", label = "C#", tagKey = "csharp"),
    SelectOption(value = "Ruby", label = "Ruby", tagKey = "ruby")
)

Partly disabled

SelectOption(enabled = false) disables a single option: the option stays in the list and is read, but it takes no press and the arrow keys pass over it. Its check box is drawn in the disabled colours too.

var chosen by rememberSaveable { mutableStateOf(setOf("mail")) }
val report = LocalExampleEvents.current
PantrixMultiSelect(
    options = CHANNELS,
    selected = chosen,
    onSelectionChange = {
        chosen = it
        report("Notification channels selected: ${it.size}")
    },
    modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
    label = "Notification channels",
    placeholder = "No notifications",
    supportingText = "SMS and calls are not in this plan.",
    tagKey = "channels"
)

private val CHANNELS = listOf(
    SelectOption(value = "mail", label = "Email", tagKey = "mail"),
    SelectOption(value = "sms", label = "SMS", enabled = false, tagKey = "sms"),
    SelectOption(value = "push", label = "Push notification", tagKey = "push"),
    SelectOption(value = "call", label = "Phone call", enabled = false, tagKey = "call")
)

Form field

A PantrixMultiSelect that takes field is bound to a form's field: the field's value is the set of what is ticked, its error takes the place of the supporting text, and its id is the automation key (MultiSelect_roles). Rules.satisfies asks for at least one to be ticked.

Press "Invite" with nothing ticked: the rule's message takes the place of the supporting text and the focus comes here. Then tick a role and clear it: the error shows as soon as the last tick is cleared, because by then the field has been touched.

val form = rememberFormState()
val scope = rememberCoroutineScope()
val roles = form.field(
    "roles",
    initial = emptySet<String>(),
    Rules.satisfies("Choose at least one role") { it.isNotEmpty() }
)
val report = LocalExampleEvents.current
PantrixMultiSelect(
    field = roles,
    options = ROLES,
    modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
    label = "Roles",
    placeholder = "Choose roles",
    supportingText = "A person can have more than one role."
)
Row(horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
    PantrixButton(
        text = "Invite",
        onClick = { scope.launch { form.submit { report("Invitation sent. Roles: ${roles.value.size}") } } },
        form = form,
        tagKey = "submit_roles"
    )
    PantrixButton(
        text = "Reset",
        onClick = {
            form.reset()
            report("Form reset")
        },
        variant = ButtonVariant.Text,
        tagKey = "reset_roles"
    )
}

private val ROLES = listOf(
    SelectOption(value = "admin", label = "Admin", tagKey = "admin"),
    SelectOption(value = "editor", label = "Editor", tagKey = "editor"),
    SelectOption(value = "viewer", label = "Viewer", tagKey = "viewer")
)

Error and disabled

error = true and enabled = false work as in a select. The border, the label and the text under a box in error are in the theme's danger role, and a screen reader announces that text as the error. A disabled box is drawn in the disabled colours, takes no focus and does not open its list; it still shows what is ticked.

PantrixMultiSelect(
    options = ROLES,
    selected = emptySet(),
    onSelectionChange = {},
    modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
    label = "Roles",
    placeholder = "Choose roles",
    supportingText = "Choose at least one role",
    error = true,
    tagKey = "roles_in_error"
)
PantrixMultiSelect(
    options = ROLES,
    selected = setOf("editor", "viewer"),
    onSelectionChange = {},
    modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
    enabled = false,
    label = "Roles",
    supportingText = "Only an admin can change the roles.",
    tagKey = "roles_disabled"
)

private val ROLES = listOf(
    SelectOption(value = "admin", label = "Admin", tagKey = "admin"),
    SelectOption(value = "editor", label = "Editor", tagKey = "editor"),
    SelectOption(value = "viewer", label = "Viewer", tagKey = "viewer")
)

Its own colours

colors = SelectColors(…) changes the list's ground, its text and its border; fieldColors = TextFieldColors(…) changes the box's focused border. Here the list is in the soft fill of the app's own purple.

The check boxes stay in the theme's colour. The selected row's fill (selectedContainer) is not used here: a list of several choices shows a choice by its check box and fills no row.

val own = ownColors()
var chosen by rememberSaveable { mutableStateOf(setOf("compose")) }
val report = LocalExampleEvents.current
PantrixMultiSelect(
    options = PROJECTS,
    selected = chosen,
    onSelectionChange = {
        chosen = it
        report("Projects selected: ${it.size}")
    },
    modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
    label = "Projects",
    placeholder = "All projects (${PROJECTS.size})",
    colors = SelectColors(container = own.container, content = own.onContainer, border = own.color),
    fieldColors = TextFieldColors(focusedBorder = own.color),
    tagKey = "own_projects"
)

private val PROJECTS = listOf(
    SelectOption(value = "compose", label = "Rorty Compose Android", icon = PantrixIcons.Folder, tagKey = "compose"),
    SelectOption(value = "android", label = "Rorty Android", icon = PantrixIcons.Folder, tagKey = "android"),
    SelectOption(value = "swiftui", label = "Rorty SwiftUI iOS", icon = PantrixIcons.Folder, tagKey = "swiftui"),
    SelectOption(value = "uikit", label = "Rorty UIKIT iOS", icon = PantrixIcons.Folder, tagKey = "uikit")
)

Style

The style is a select's too: style is the geometry of the list (SelectStyle), fieldStyle that of the box (TextFieldStyle). Here the list's rows are 56dp tall (itemHeight), and the list is 240dp at the most, with the options that do not fit scrolling inside it (maxHeight). The box's corners are fully round: TextFieldStyle(shape = ShapeRole.Full).

Only the fields that are given change; the rest comes from the theme's and the platform's style.

var chosen by rememberSaveable { mutableStateOf(setOf("sat", "sun")) }
PantrixMultiSelect(
    options = DAYS,
    selected = chosen,
    onSelectionChange = { chosen = it },
    modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
    label = "Delivery days",
    placeholder = "Choose days",
    style = SelectStyle(maxHeight = 240.dp, itemHeight = 56.dp),
    fieldStyle = TextFieldStyle(shape = ShapeRole.Full),
    tagKey = "styled_days"
)

private val DAYS = listOf(
    SelectOption(value = "mon", label = "Monday", tagKey = "mon"),
    SelectOption(value = "tue", label = "Tuesday", tagKey = "tue"),
    SelectOption(value = "wed", label = "Wednesday", tagKey = "wed"),
    SelectOption(value = "thu", label = "Thursday", tagKey = "thu"),
    SelectOption(value = "fri", label = "Friday", tagKey = "fri"),
    SelectOption(value = "sat", label = "Saturday", tagKey = "sat"),
    SelectOption(value = "sun", label = "Sunday", tagKey = "sun")
)

API

options

Several choices out of a list the control opens: the projects a page is narrowed to, the days a shop is open.

ParameterTypeDefaultWhat it is
optionsList<SelectOption<T>>the choices, in the order they are shown. With none, the list says so.
selectedSet<T>the values that are chosen now: every option whose value is among them is ticked.
onSelectionChange(Set<T>) -> Unitasked for another set, with the whole of it: the one before with the pressed option's value put in or taken out.
modifierModifierModifierthe modifier of the whole field: its label, its box and what is under it.
enabledBooleantruewhether the list can be opened. One that cannot be takes no focus, is drawn in the disabled colours and says so.
labelString?nullwhat the choice is, written above the box: the field's name for a screen reader, and what its list is announced as.
placeholderString?nullshown in the box while nothing is chosen.
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 choice is wrong: the border, the label and the supporting text turn to the danger role, and a screen reader is told.
valueTextString?nullwhat the box shows while something is chosen, in place of the chosen labels.
searchableBooleanfalsewhether the list has a field to search its options by.
searchPlaceholderString?nullwhat the search field shows while it is empty, and what it is called. The theme's word for a search where it is null.
styleSelectStyle?nullthis call's own SelectStyle, the one its list is drawn with: over every enclosing one and under the density. Only the fields it sets change, on every platform.
fieldStyleTextFieldStyle?nullthis call's own TextFieldStyle, the one its box is drawn with: over every enclosing one and under the density. Only the fields it sets change, on every platform.
colorsSelectColors?nullits list's own colours, in place of the theme's roles: the ones it sets are used, and the rest stay the theme's. The check boxes are the theme's.
fieldColorsTextFieldColors?nullits box's own colours, as a text field takes them. A disabled one 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.
tagKeyString?nullthe field's automation key: the field is MultiSelect_<tagKey>, its list MultiSelectList_<tagKey>, the list's search field MultiSelectSearch_<tagKey>, and its options are named inside it, MultiSelect_<tagKey>.MultiSelectOption_<option's key>. It reports that its list opened and that an option was ticked or cleared, by the option's key and never by its label.
interactionSourceMutableInteractionSource?nullthe field's interactions, hoisted: its focus, its hover and its presses. It keeps a source of its own when this is null.
presentationOverlayPresentationOverlayPresentation.Popoverhow the list is shown: under the field, by default; as a sheet from the bottom of the window, its title the field's label; or the one in a narrow window and the other in a wider one. Chosen as the list opens, it stays until the list has closed.

field

A multi select bound to a form's field: its choices, its error and its name come from field.

ParameterTypeDefaultWhat it is
fieldFormField<Set<T>>the field the control shows and changes: a set of the options' values.
optionsList<SelectOption<T>>the choices, in the order they are shown.
modifierModifierModifierthe modifier of the whole field: its label, its box and what is under it.
enabledBooleantruewhether the list can be opened.
labelString?nullwhat the choice is, written above the box: the field's name for a screen reader.
placeholderString?nullshown in the box while nothing is chosen.
supportingTextString?nullhelp under the box, while the field has no error to show there.
valueTextString?nullwhat the box shows while something is chosen, in place of the chosen labels.
searchableBooleanfalsewhether the list has a field to search its options by.
searchPlaceholderString?nullwhat the search field shows while it is empty, and what it is called.
styleSelectStyle?nullthis call's own SelectStyle, the one its list is drawn with.
fieldStyleTextFieldStyle?nullthis call's own TextFieldStyle, the one its box is drawn with.
colorsSelectColors?nullits list's own colours: the ones it sets, with the theme's for the rest.
fieldColorsTextFieldColors?nullits box's own colours, as a text field takes them.
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.
presentationOverlayPresentationOverlayPresentation.Popoverhow the list is shown: under the field, as a sheet, or by the window's width.

SelectStyle

The list a PantrixSelect opens: the panel it is drawn in, how far that sits from the box it belongs to and from the edges of the window, and its rows.

ParameterTypeDefaultWhat it is
shapeShapeSpecShapeSpec.UnspecifiedThe panel's corners: the medium step of the shape scale on every platform, as the box's are.
maxHeightDpDp.UnspecifiedThe tallest the panel gets: more options than fit scroll.
elevationDpDp.UnspecifiedHow high the panel floats over the app, which is how large its shadow is.
gapDpDp.UnspecifiedBetween the box and its list.
marginDpDp.UnspecifiedKept clear between the panel and the edges of the window, and of what covers the window there: a system bar, a cutout, the keyboard.
verticalPaddingDpDp.UnspecifiedAbove the first row and below the last.
itemHeightDpDp.UnspecifiedThe least a row is tall, where the platform's touch target is not taller.
horizontalPaddingDpDp.UnspecifiedFrom the panel's edge to what a row starts with, and from what it ends with to the other edge. Each preset has the text field's own, so that an option's label starts where the box's text does.
iconSizeDpDp.UnspecifiedAn option's icon, and the tick beside the chosen one.
iconGapDpDp.UnspecifiedBetween a row's parts: its check box, its icon, its label and its tick.

SelectColors

What the list of a PantrixSelect is painted with, for a call that does not take the theme's colours: a colour that is set is used, and one that is left out stays the theme's. The box's colours are TextFieldColors.

ParameterTypeDefaultWhat it is
containerColorColor.UnspecifiedThe panel's fill. The theme's surface.
contentColorColor.UnspecifiedAn option's label and icon. The theme's textPrimary.
selectedContainerColorColor.UnspecifiedThe fill of the chosen option's row, in a select of one choice. The primary role's container. A select of several marks its choices with check boxes, and fills no row.
selectedContentColorColor.UnspecifiedThe chosen option's label, its icon and the tick beside it, in a select of one choice. The primary role's onContainer.
borderColorColor.UnspecifiedThe hairline around the panel, which tells it from a surface of its own colour under it, as in the dark theme, where a shadow shows little, and the line between the rows and the field a list is searched by. The theme's divider on the theme's own grounds. On a fill, where that line would not read, a share of content: the edge a subtle button has. A transparent colour draws none.

Accessibility