1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
|
#' Add a popover to a UI element
#'
#' @description
#' Display additional information when clicking on a UI element (typically a
#' button).
#'
#' @param trigger The UI element to serve as the popover trigger (typically a
#' [shiny::actionButton()] or similar). If `trigger` renders as multiple HTML
#' elements (e.g., it's a `tagList()`), the last HTML element is used for the
#' trigger. If the `trigger` should contain all of those elements, wrap the
#' object in a [htmltools::div()] or [htmltools::span()].
#' @param ... UI elements for the popover's body. Character strings are
#' [automatically escaped][htmltools::htmlEscape()] unless marked as
#' [htmltools::HTML()].
#' @param title A title (header) for the popover. To remove a header
#' with `update_popover()`, provide a either an empty string or `character(0)`.
#' @param id A character string. Required to re-actively respond to the
#' visibility of the popover (via the `input[[id]]` value) and/or update the
#' visibility/contents of the popover.
#' @param placement The placement of the popover relative to its trigger.
#' @param options A list of additional
#' [options](https://getbootstrap.com/docs/5.3/components/popovers/#options).
#'
#' @section Closing popovers:
#'
#' In addition to clicking the `close_button`, popovers can be closed by
#' pressing the Esc/Space key when the popover (and/or its trigger) is
#' focused.
#'
#' @section Theming/Styling:
#'
#' ```{r child="man/fragments/tooltip-popover_theming.Rmd", el = "popover"}
#' ```
#'
#' ```
#' popover(
#' "Trigger", "Popover message",
#' options = list(customClass = "my-pop")
#' )
#' ```
#'
#' And then add relevant rules to [bs_theme()] via [bs_add_rules()]:
#'
#' ```
#' bs_theme() |> bs_add_rules(".my-pop { max-width: none; }")
#' ```
#'
#' @section Accessibility of Popover Triggers:
#'
#' ```{r child="man/fragments/tooltip-popover_a11y-trigger.Rmd", el = "popover"}
#' ```
#'
#' ```r
#' popover(
#' bsicons::bs_icon("gear", title = "Settings"),
#' title = "Settings",
#' sliderInput("n", "Number of points", 1, 100, 50)
#' )
#' ```
#'
#' ```r
#' popover(
#' fontawesome::fa("gear", a11y = "sem", title = "Settings"),
#' title = "Settings",
#' sliderInput("n", "Number of points", 1, 100, 50)
#' )
#' ```
#'
#' @describeIn popover Add a popover to a UI element
#'
#' @export
#' @family Components
#'
#' @references Popovers are based on [Bootstrap's Popover
#' component](https://getbootstrap.com/docs/5.3/components/popovers/). See the
#' bslib website for an [interactive introduction to tooltips and
#' popovers](https://rstudio.github.io/bslib/articles/tooltips-popovers/index.html).
#'
#' @seealso [tooltip()] provides an alternative way to display informational
#' text on demand, typically when focusing or hovering over a trigger element.
#'
#' @examplesIf rlang::is_interactive()
#'
#' popover(
#' shiny::actionButton("btn", "A button"),
#' "Popover body content...",
#' title = "Popover title"
#' )
#'
#' library(shiny)
#'
#' ui <- page_fixed(
#' card(class = "mt-5",
#' card_header(
#' popover(
#' uiOutput("card_title", inline = TRUE),
#' title = "Provide a new title",
#' textInput("card_title", NULL, "An editable title")
#' )
#' ),
#' "The card body..."
#' )
#' )
#'
#' server <- function(input, output) {
#' output$card_title <- renderUI({
#' list(input$card_title, bsicons::bs_icon("pencil-square"))
#' })
#' }
#'
#' shinyApp(ui, server)
popover <- function(
trigger,
...,
title = NULL,
id = NULL,
placement = c("auto", "top", "right", "bottom", "left"),
options = list()
) {
args <- separate_arguments(...)
children <- args$children
attribs <- args$attribs
if (length(children) == 0) {
abort("At least one value must be provided to `...`.")
}
bad_opts <- intersect(c("content", "title", "placement"), names(options))
if (length(bad_opts) > 0) {
rlang::abort(
sprintf("The `%s` option cannot be specified directly.", bad_opts[1])
)
}
res <- web_component(
"bslib-popover",
id = id,
placement = rlang::arg_match(placement),
bsOptions = to_json(options),
!!!attribs,
# Use <template> as a way to protect these children from potentially being
# pulled outside this element (the browser's parser does this to, for
# example, block elements inside a <p> tag)
tags$template(
div(!!!children, style = "display:contents;"),
div(title, style = "display:contents;")
),
trigger
)
res <- tag_require(res, version = 5, caller = "popover()")
as_fragment(res)
}
#' @describeIn popover Programmatically show/hide a popover.
#'
#' @param show Whether to show (`TRUE`) or hide (`FALSE`) the popover. The
#' default (`NULL`) will show if currently hidden and hide if currently shown.
#' Note that a popover will not be shown if the trigger is not visible (e.g.,
#' it's hidden behind a tab).
#' @param session A Shiny session object (the default should almost always be
#' used).
#'
#' @export
toggle_popover <- function(id, show = NULL, session = get_current_session()) {
show <- normalize_show_value(show)
msg <- list(method = "toggle", value = show)
force(id)
callback <- function() {
session$sendInputMessage(id, msg)
}
session$onFlush(callback, once = TRUE)
}
#' @describeIn popover Update the contents of a popover.
#' @export
update_popover <- function(
id,
...,
title = NULL,
session = get_current_session()
) {
body <- tagList(...)
msg <- dropNulls(
list(
method = "update",
content = if (length(body) > 0) processDeps(body, session),
header = if (!is.null(title)) processDeps(title, session)
)
)
force(id)
callback <- function() {
session$sendInputMessage(id, msg)
}
session$onFlush(callback, once = TRUE)
}
|