Skip to content

Commit 8f255e5

Browse files
cpsievertgadenbuie
andauthored
Add tooltip() API (#662)
Co-authored-by: Garrick Aden-Buie <[email protected]> Co-authored-by: cpsievert <[email protected]>
1 parent 494923b commit 8f255e5

28 files changed

+1801
-76
lines changed

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ Collate:
8686
'shiny-devmode.R'
8787
'sidebar.R'
8888
'staticimports.R'
89+
'tooltip.R'
8990
'utils-deps.R'
9091
'utils-shiny.R'
9192
'utils-tags.R'

NAMESPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,10 @@ export(sidebar)
124124
export(sidebar_toggle)
125125
export(theme_bootswatch)
126126
export(theme_version)
127+
export(toggle_tooltip)
128+
export(tooltip)
127129
export(update_switch)
130+
export(update_tooltip)
128131
export(value_box)
129132
export(version_default)
130133
export(versions)

R/card.R

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -271,13 +271,13 @@ is.card_item <- function(x) {
271271

272272

273273
full_screen_toggle <- function() {
274-
tags$span(
275-
class = "bslib-full-screen-enter",
276-
class = "badge rounded-pill bg-dark",
277-
"data-bs-toggle" = "tooltip",
278-
"data-bs-placement" = "bottom",
279-
title = "Expand",
280-
full_screen_toggle_icon()
274+
tooltip(
275+
tags$span(
276+
class = "bslib-full-screen-enter",
277+
class = "badge rounded-pill bg-dark",
278+
full_screen_toggle_icon()
279+
),
280+
"Expand"
281281
)
282282
}
283283

R/layout.R

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ layout_column_wrap <- function(
5555

5656
heights_equal <- match.arg(heights_equal)
5757

58-
args <- list_split_named(rlang::list2(...))
59-
attribs <- args[["named"]]
60-
children <- dropNulls(args[["unnamed"]])
58+
args <- separate_arguments(...)
59+
attribs <- args$attribs
60+
children <- args$children
6161

6262
if (length(width) > 1) {
6363
stop("`width` of length greater than 1 is not currently supported.")
@@ -189,9 +189,9 @@ layout_columns <- function(
189189
class = NULL,
190190
height = NULL
191191
) {
192-
args <- list_split_named(rlang::list2(...))
193-
attribs <- args[["named"]]
194-
children <- dropNulls(args[["unnamed"]])
192+
args <- separate_arguments(...)
193+
attribs <- args$attribs
194+
children <- args$children
195195
n_kids <- length(children)
196196

197197
# Resolve missing value(s) for col_widths, etc.

R/tooltip.R

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#' Add a tooltip to a UI element
2+
#'
3+
#' Display additional information when focusing (or hovering over) a UI element.
4+
#'
5+
#' @param trigger A UI element (i.e., [htmltools tag][htmltools::tags]) to serve
6+
#' as the tooltips trigger. It's good practice for this element to be a
7+
#' keyboard-focusable and interactive element (e.g., `actionButton()`,
8+
#' `actionLink()`, etc) so that the tooltip is accessible to keyboard and
9+
#' assistive technology users.
10+
#' @param ... UI elements for the tooltip. Character strings are [automatically
11+
#' escaped][htmlEscape()] unless marked as [HTML()].
12+
#' @param id A character string. Required to re-actively respond to the
13+
#' visibility of the tooltip (via the `input[[id]]` value) and/or update the
14+
#' visibility/contents of the tooltip.
15+
#' @param placement The placement of the tooltip relative to its trigger.
16+
#' @param options A list of additional [Bootstrap
17+
#' options](https://getbootstrap.com/docs/5.3/components/tooltips/#options).
18+
#'
19+
#' @details If `trigger` yields multiple HTML elements (e.g., a `tagList()` or
20+
#' complex `{htmlwidgets}` object), the last HTML element is used as the
21+
#' trigger. If the `trigger` should contain all of those elements, wrap the
22+
#' object in a [div()] or [span()].
23+
#'
24+
#' @describeIn tooltip Add a tooltip to a UI element
25+
#' @references <https://getbootstrap.com/docs/5.3/components/tooltips/>
26+
#' @export
27+
#' @examplesIf interactive()
28+
#'
29+
#' tooltip(
30+
#' shiny::actionButton("btn", "A button"),
31+
#' "A message"
32+
#' )
33+
#'
34+
#' card(
35+
#' card_header(
36+
#' tooltip(
37+
#' span("Card title ", bsicons::bs_icon("question-circle-fill")),
38+
#' "Additional info",
39+
#' placement = "right"
40+
#' )
41+
#' ),
42+
#' "Card body content..."
43+
#' )
44+
tooltip <- function(
45+
trigger,
46+
...,
47+
id = NULL,
48+
placement = c("auto", "top", "right", "bottom", "left"),
49+
options = list()
50+
) {
51+
52+
args <- separate_arguments(...)
53+
children <- args$children
54+
attribs <- args$attribs
55+
56+
if (length(children) == 0) {
57+
abort("At least one value must be provided to `...`.")
58+
}
59+
60+
res <- web_component(
61+
"bslib-tooltip",
62+
id = id,
63+
placement = rlang::arg_match(placement),
64+
options = jsonlite::toJSON(options, auto_unbox = TRUE),
65+
!!!attribs,
66+
# Use display:none instead of <template> since shiny.js
67+
# doesn't bind to the contents of the latter
68+
div(!!!children, style = "display:none;"),
69+
trigger
70+
)
71+
72+
res <- tag_require(res, version = 5, caller = "tooltip()")
73+
as_fragment(res)
74+
}
75+
76+
#' @describeIn tooltip Programmatically show/hide a tooltip.
77+
#'
78+
#' @param id a character string that matches an existing tooltip id.
79+
#' @param show Whether to show (`TRUE`) or hide (`FALSE`) the tooltip. The
80+
#' default (`NULL`) will show if currently hidden and hide if currently shown.
81+
#' Note that a tooltip will not be shown if the trigger is not visible (e.g.,
82+
#' it's hidden behind a tab).
83+
#' @param session A Shiny session object (the default should almost always be
84+
#' used).
85+
#'
86+
#' @export
87+
toggle_tooltip <- function(id, show = NULL, session = get_current_session()) {
88+
show <- normalize_show_value(show)
89+
90+
msg <- list(method = "toggle", value = show)
91+
force(id)
92+
callback <- function() {
93+
session$sendInputMessage(id, msg)
94+
}
95+
session$onFlush(callback, once = TRUE)
96+
}
97+
98+
99+
#' @describeIn tooltip Update the contents of a tooltip.
100+
#' @export
101+
update_tooltip <- function(id, ..., session = get_current_session()) {
102+
103+
title <- tagList(...)
104+
105+
msg <- dropNulls(list(
106+
method = "update",
107+
title = if (length(title) > 0) processDeps(title, session)
108+
))
109+
110+
force(id)
111+
callback <- function() {
112+
session$sendInputMessage(id, msg)
113+
}
114+
session$onFlush(callback, once = TRUE)
115+
}
116+
117+
normalize_show_value <- function(show) {
118+
if (is.null(show)) return("toggle")
119+
120+
if (length(show) != 1 || !is.logical(show)) {
121+
abort("`show` must be `TRUE`, `FALSE`, or `NULL`.")
122+
}
123+
124+
if (show) "show" else "hide"
125+
}

R/utils-deps.R

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
1-
component_dependency_js <- function(name) {
1+
web_component <- function(tagName, ...) {
2+
js_dep <- component_dependency_js("webComponents", type = "module")
3+
args <- c(list(js_dep), rlang::list2(...))
4+
tag(tagName, args)
5+
}
6+
7+
component_dependency_js <- function(name, ...) {
28
minified <- get_shiny_devmode_option("shiny.minified", default = TRUE)
39

410
htmlDependency(
511
name = paste0("bslib-", name, "-js"),
612
version = get_package_version("bslib"),
713
package = "bslib",
814
src = file.path("components", "dist", name),
9-
script = paste0(name, if (minified) ".min", ".js"),
15+
script = list(
16+
src = paste0(name, if (minified) ".min", ".js"),
17+
...
18+
),
1019
all_files = TRUE
1120
)
1221
}

R/utils.R

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,19 +88,20 @@ any_unnamed <- function(x) {
8888
is.null(nms) || !all(nzchar(nms))
8989
}
9090

91-
list_split_named <- function(x) {
91+
separate_arguments <- function(...) {
92+
x <- rlang::list2(...)
9293
x_names <- rlang::names2(x)
9394
is_named <- nzchar(x_names)
9495

9596
if (all(is_named)) {
96-
return(list(named = x, unnamed = list()))
97+
return(list(attribs = x, children = list()))
9798
}
9899

99100
if (!any(is_named)) {
100-
return(list(named = list(), unnamed = x))
101+
return(list(attribs = list(), children = x))
101102
}
102103

103-
list(named = x[is_named], unnamed = unname(x[!is_named]))
104+
list(attribs = x[is_named], children = unname(dropNulls(x[!is_named])))
104105
}
105106

106107
#' Rename a named list

inst/components/dist/card/card.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

inst/components/dist/card/card.js

Lines changed: 1 addition & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

inst/components/dist/card/card.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)