Overlays (dialogs, drawers, menus, popovers) have an open state. In
React, it lives in a useState() hook of the parent
component, which R code cannot write. muiMaterial offers several ways to
handle it:
| Approach | Open state lives in | Server code | Works in Quarto / static HTML |
|---|---|---|---|
.triggerId() wrappers (this page) |
the browser | none | yes |
| reactRouter routes (see Dialog) | the URL | none | yes |
.shinyInput() wrappers +
update*.shinyInput()
|
the Shiny server | yes | no |
The .triggerId() wrappers cover the most common case:
open an overlay when an element is clicked. They are
available for six components:
| Function | Opens | Closes |
|---|---|---|
Dialog.triggerId() |
a dialog | backdrop click, Escape |
Modal.triggerId() |
a modal | backdrop click, Escape |
Drawer.triggerId() |
a temporary drawer | backdrop click, Escape, a click on a link inside |
SwipeableDrawer.triggerId() |
a swipeable drawer | same as Drawer.triggerId(), and swiping |
Menu.triggerId() |
a menu, next to the trigger | click outside, Escape, a click on an item |
Popover.triggerId() |
a popover, next to the trigger | click outside, Escape |
How it works
Give any element an id, and pass the same id as the
triggerId of the overlay. A click on the element opens the
overlay:
muiMaterialPage(
CssBaseline(),
Button(id = "open-dialog", variant = "outlined", "Open dialog"),
Dialog.triggerId(
triggerId = "open-dialog",
DialogTitle("Hello"),
DialogContent(DialogContentText("Open/close is managed entirely in the browser."))
)
)- The trigger can be any element with an
id: a muiMaterial component, an HTML tag, or a Shiny widget such asshiny::actionButton(). - The overlay can be anywhere on the page. It does not need to be next to its trigger.
- The binding survives re-renders: a trigger rendered later with
renderUI()/uiOutput()(or replaced by it) keeps working. - Several triggers cannot share one id, but several overlays can each have their own trigger.
The six wrappers
muiMaterialPage(
useMaterialIconsFilled = TRUE,
CssBaseline(),
Stack(
direction = "row",
spacing = 1,
sx = list(flexWrap = "wrap"),
Button(id = "t-dialog", variant = "outlined", "Dialog"),
Button(id = "t-modal", variant = "outlined", "Modal"),
Button(id = "t-drawer", variant = "outlined", "Drawer"),
Button(id = "t-swipeable", variant = "outlined", "Swipeable drawer"),
Button(id = "t-menu", variant = "outlined", "Menu"),
Button(id = "t-popover", variant = "outlined", "Popover")
),
Dialog.triggerId("t-dialog", DialogTitle("Dialog"), DialogContent("Press Escape to close.")),
Modal.triggerId(
"t-modal",
Box(
sx = list(position = "absolute", top = "50%", left = "50%", transform = "translate(-50%, -50%)",
width = 300, bgcolor = "background.paper", boxShadow = 24, p = 4),
Typography("Modal content")
)
),
Drawer.triggerId("t-drawer", anchor = "left", width = 250, Box(sx = list(p = 2), Typography("Drawer content"))),
SwipeableDrawer.triggerId("t-swipeable", anchor = "bottom", width = "auto", Box(sx = list(p = 2), Typography("Swipe down to close"))),
Menu.triggerId("t-menu", MenuItem("Profile"), MenuItem("My account"), MenuItem("Logout")),
Popover.triggerId(
"t-popover",
anchorOrigin = list(vertical = "bottom", horizontal = "left"),
Typography(sx = list(p = 2), "Popover content")
)
)Options
All the other arguments are passed to the MUI component as props, so
the options of the MUI documentation apply: anchor,
maxWidth, anchorOrigin,
slotProps, sx, … A few arguments are specific
to the wrappers:
-
Drawer.triggerId()andSwipeableDrawer.triggerId():-
widthsizes the drawer paper (280 by default). Usewidth = "auto"for top and bottom drawers. -
closeOnLinkClick = FALSEkeeps the drawer open when a link (<a>) inside it is clicked. By default, the drawer closes, which suits navigation drawers. -
sxstyles the drawer root, like on any other component. Style the paper withslotProps = list(paper = list(sx = ...)).
-
-
Menu.triggerId():closeOnItemClick = FALSEkeeps the menu open after a click, for example for a menu of checkboxes.
muiMaterialPage(
CssBaseline(),
Button(id = "filters-menu", variant = "outlined", "Filters"),
Menu.triggerId(
triggerId = "filters-menu",
closeOnItemClick = FALSE,
lapply(c("Open", "In progress", "Closed"), function(status) {
MenuItem(FormControlLabel(control = Checkbox(defaultChecked = TRUE), label = status))
})
)
)Callbacks
The open state belongs to the wrapper: passing open (or
anchorEl) has no effect. Callbacks you pass are
composed with the wrapper’s own handlers: the wrapper
updates its state first, then calls yours. For example, send the closing
of a dialog to the server with triggerEvent():
Dialog.triggerId(
triggerId = "open-dialog",
onClose = triggerEvent("dialog_closed"),
DialogTitle("Hello")
)The same applies to onClick and onClose of
Menu.triggerId(), and to
onOpen/onClose of
SwipeableDrawer.triggerId().
Limitations
- A
.triggerId()overlay closes on its built-in events only (see the tables above). A button inside a dialog does not close it. For dialogs with “Cancel”/“Confirm” buttons, keep the open state in the URL with reactRouter or on the server withDialog.shinyInput(). - The server cannot open or close a
.triggerId()overlay. Use the.shinyInput()variant when the server must decide.
In a Shiny app
The trigger can be a Shiny widget, and it can be rendered by the server:
library(shiny)
library(muiMaterial)
ui <- muiMaterialPage(
CssBaseline(),
Box(sx = list(p = 2), uiOutput("toolbar")),
Drawer.triggerId(
triggerId = "open-settings",
anchor = "right",
Box(sx = list(p = 2), Typography(variant = "h6", "Settings"), Switch.shinyInput("compact", value = FALSE))
)
)
server <- function(input, output, session) {
# The trigger is rendered (and re-rendered) by the server
output$toolbar <- renderUI({
IconButton(id = "open-settings", `aria-label` = "settings", shiny::icon("gear"))
})
}
shinyApp(ui, server)Run complete examples with
muiMaterialExample("DrawerTriggerId") and
muiMaterialExample("Menu").
