Modal
A modal layer of the app's own: a scrim over the whole window, and over it content the app draws, with everything a modal of Pantrix does. It is what PantrixDialog and PantrixCommandPalette stand on, for a layer neither of them draws: a search with filters of its own, a picture shown large, an editor that fills a phone.
import com.pantrixui.components.overlay.modal.PantrixModal
- It never closes itself. Back, Escape and a press on the scrim call
onDismissRequest; the caller turnsvisibleoff. The content enters and leaves with the theme's motion; once it has closed, nothing of the modal is drawn. - Its place.
positionputs the content in the middle of the window or at its top:Top, for a search whose field stays where it is.marginis the room left between the content and the window's safe area; it is part of the scrim. - A page of its own. A
marginof none, content that fills itself and ascrimColorthat is the theme's background make the modal a page on a phone. - A fold. Where a fold divides the window in two, the content keeps to the half it was opened from; a modal opened inside a picture of a window takes that picture for its window.
Examples
A search at the top
A search of your own, at the top of the window: the field stays where it is, and the results grow and shrink under it. Escape, back or a press on the scrim closes it; so does a press on a result.
var searching by remember { mutableStateOf(false) }
var query by remember { mutableStateOf("") }
PantrixButton(
text = "Open the search",
onClick = { searching = true },
variant = ButtonVariant.Outline,
tagKey = "search"
)
PantrixModal(
visible = searching,
onDismissRequest = { searching = false },
position = ModalPosition.Top,
paneTitle = "Search",
tagKey = "search"
) {
PantrixSurface(Modifier.widthIn(max = 560.dp), shape = PantrixTheme.shapes.lg, elevation = 8.dp) {
Column(Modifier.padding(PantrixTheme.spacing.md)) {
PantrixTextField(
value = query,
onValueChange = { query = it },
modifier = Modifier.fillMaxWidth(),
placeholder = "Search for a page",
// The name a screen reader gives a field that has no label.
contentDescription = "Search pages",
tagKey = "search_query"
)
// Each result's key is its place in the list, never its words.
PAGES.withIndex().filter { it.value.contains(query, ignoreCase = true) }.forEach { (index, page) ->
PantrixListItem(
headline = page,
leading = { PantrixIcon(PantrixIcons.File, contentDescription = null) },
onClick = { searching = false },
tagKey = "result_$index"
)
}
}
}
}
private val PAGES = listOf("Installation", "Themes", "Components", "Guides", "Changelog")
A page of its own
A page on a phone: a margin of none, a scrim in the theme's background and content that fills itself. Behind the window's bars stays the background's colour too.
var editing by remember { mutableStateOf(false) }
var note by remember { mutableStateOf("Monday: go over the design") }
PantrixButton(text = "Edit the note", onClick = { editing = true }, variant = ButtonVariant.Outline, tagKey = "edit")
PantrixModal(
visible = editing,
onDismissRequest = { editing = false },
margin = PaddingValues(),
paneTitle = "Note",
scrimColor = PantrixTheme.colors.background,
tagKey = "editor"
) {
Column(
Modifier.fillMaxSize().padding(PantrixTheme.spacing.lg),
verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.md)
) {
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
PantrixText(text = "Note", style = PantrixTheme.typography.title, modifier = Modifier.weight(1f))
PantrixButton(text = "Done", onClick = { editing = false }, tagKey = "done")
}
PantrixTextArea(value = note, onValueChange = { note = it }, label = "Note", tagKey = "note")
}
}
One that asks for a decision
A modal that does not close before a decision is made: a press on the scrim and back do nothing (dismissOnScrimClick and dismissOnBack are off); one of the two buttons in it is chosen.
var asking by remember { mutableStateOf(false) }
var answer by remember { mutableStateOf("Not chosen yet") }
PantrixButton(
text = "Show the terms",
onClick = { asking = true },
variant = ButtonVariant.Outline,
tagKey = "terms"
)
PantrixText(text = answer, style = PantrixTheme.typography.footnote, tagKey = "answer")
PantrixModal(
visible = asking,
onDismissRequest = { asking = false },
paneTitle = "Terms of use",
dismissOnScrimClick = false,
dismissOnBack = false,
tagKey = "decision"
) {
PantrixSurface(Modifier.widthIn(max = 420.dp), shape = PantrixTheme.shapes.lg) {
Column(
Modifier.padding(PantrixTheme.spacing.lg),
verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.md)
) {
PantrixText(text = "Accept the terms of use to go on.")
Row(horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
PantrixButton(
text = "Accept",
onClick = {
answer = "Accepted"
asking = false
},
tagKey = "accept"
)
PantrixButton(
text = "Decline",
onClick = {
answer = "Declined"
asking = false
},
variant = ButtonVariant.Outline,
tagKey = "decline"
)
}
}
}
}
API
visible
A modal layer of the app's own: a scrim over the whole window, and over it content the app draws, with everything a modal of Pantrix does. It is what PantrixDialog and PantrixCommandPalette stand on, for a layer neither of them draws: a search with filters of its own, a picture shown large, an editor that fills a phone.
| Parameter | Type | Default | What it is |
|---|---|---|---|
visible | Boolean | whether the modal is shown. Set to false, the content leaves with its motion, and then nothing is composed. | |
onDismissRequest | () -> Unit | called when the user asks to close the modal: back, Escape, or a press on the scrim. | |
modifier | Modifier | Modifier | applied to the content, inside margin: the node it tags is the content as it is drawn, and a press anywhere on it is the content's, never the scrim's. |
position | ModalPosition | ModalPosition.Center | where the content sits: in the middle of the window, or near its top. |
margin | PaddingValues | PaddingValues(PantrixTheme.spacing.xl) | the room kept between the content and the window's safe area, side by side. It is part of the scrim: a press in it, right beside the content, dismisses as one anywhere else on the scrim does. None, with content that fills what it is given, makes the content the whole safe area, as an editor on a phone is. |
paneTitle | String? | null | what a screen reader announces as the modal appears: what it is, in a word or two. |
dismissOnScrimClick | Boolean | true | whether a press on the scrim asks to dismiss. Where it does not, the scrim is a fill a screen reader passes over. |
dismissOnBack | Boolean | true | whether back and Escape ask to dismiss. |
scrimColor | Color | PantrixTheme.colors.scrim | what is drawn over the app while the modal is shown, fading with the content: the theme's scrim unless given. The theme's background makes the modal a page of its own, the window's bars and all. |
tagKey | String? | null | the modal's automation key: the content is Modal_<tagKey>. A dismissal is reported as a Modal event, dismiss, which says whether it came by the scrim or by back. |
content | @Composable () -> Unit | what the modal shows, drawn by the app. |
Accessibility
- The focus stays inside. Opened from the keyboard, the focus moves to the content's first control, and Tab goes round inside it and never reaches the app under it; opened by touch, the focus stays where it was, and no software keyboard opens unasked. Once it closes, the focus is back where it was.
- Screen reader.
paneTitleis read as the modal opens; the scrim is one "Close" action, after the content.