Skip to content

Commit f4219c0

Browse files
committed
Code review; fix tooltips inside a card
1 parent 52f6b9b commit f4219c0

File tree

4 files changed

+58
-54
lines changed

4 files changed

+58
-54
lines changed

R/card.R

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,10 +274,9 @@ full_screen_toggle <- function() {
274274
tags$span(
275275
class = "bslib-full-screen-enter",
276276
class = "badge rounded-pill bg-dark",
277-
title = "Expand",
278277
full_screen_toggle_icon()
279278
),
280-
placement = "bottom"
279+
"Expand"
281280
)
282281
}
283282

R/tooltip.R

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,18 @@
33
#' Tooltips are useful for showing additional information when focusing (or
44
#' hovering over) a UI element.
55
#'
6-
#' @param ... Unnamed arguments can be any valid child of an [htmltools
7-
#' tag][htmltools::tags]. Named arguments become HTML attributes on returned
8-
#' UI element. Attributes starting with `data-bs-*` may be supplied to further
9-
#' customize [tooltip
10-
#' options](https://getbootstrap.com/docs/5.3/components/tooltips/).
11-
#' @param body Content to display in the tooltip.
12-
#' @param placement The placement of the tooltip relative to the target element.
13-
#' @param html Whether to treat `body` as HTML. WARNING: setting this `TRUE`
14-
#' when the `body` is a function of user `inputs` is dangerous and not
15-
#' recommended.
16-
#' @param sanitize Whether to sanitize HTML (only relevant when `html = TRUE`).
17-
#' This can be useful if `body` is a function of user `inputs`, but should
18-
#' also be treated as HTML.
6+
#' @param trigger A UI element (i.e., [htmltools tag][htmltools::tags]) to serve as the
7+
#' tooltips trigger. It's good practice for this element to be a keyboard-focusable
8+
#' and interactive element (e.g., `actionButton()`, `actionLink()`, etc) so that
9+
#' the tooltip is accessible to keyboard and assistive technology users.
10+
#' @param ... UI elements for the tooltip. Character strings are automatically
11+
#' [htmlEscape()]d unless marked as [HTML()].
1912
#' @param id If provided, you can use `input$id` in your server logic to
2013
#' determine whether the tooltip is currently shown/hidden.
14+
#' @param placement The placement of the tooltip relative to its trigger.
15+
#' @param options A list of additional [Bootstrap options](https://getbootstrap.com/docs/5.3/components/tooltips/#options).
2116
#'
22-
#' @details If multiple UI elements are provided to `...`, the last element is
17+
#' @details If `x` yields multiple HTML elements, the last element is
2318
#' used as the tooltip's target. If the target should contain multiple elements,
2419
#' then wrap those elements in a [span()] or [div()].
2520
#'
@@ -30,38 +25,48 @@
3025
#'
3126
#' tooltip(
3227
#' shiny::actionButton("btn", "A button"),
33-
#' body = "A message"
28+
#' "A message"
3429
#' )
3530
#'
3631
#' card(
3732
#' card_header(
3833
#' tooltip(
3934
#' span("Card title ", bsicons::bs_icon("question-circle-fill")),
40-
#' body = "Additional info",
35+
#' "Additional info",
4136
#' placement = "right"
4237
#' )
4338
#' ),
4439
#' "Card body content..."
4540
#' )
4641
tooltip <- function(
47-
...,
48-
body = "Tooltip",
49-
placement = c("auto", "top", "right", "bottom", "left"),
50-
html = FALSE,
51-
sanitize = FALSE,
52-
id = NULL
53-
) {
42+
trigger, ..., id = NULL,
43+
placement = c("auto", "top", "right", "bottom", "left"),
44+
options = list()
45+
) {
46+
47+
args <- rlang::list2(...)
48+
argnames <- rlang::names2(args)
49+
50+
children <- args[!nzchar(argnames)]
51+
attribs <- args[nzchar(argnames)]
52+
53+
#if (length(attribs) > 0) {
54+
# abort(c(
55+
# paste0("Unknown named argument: '", names(attribs)[1], "'."),
56+
# "i" = "Did you intend to pass it to `options`?"
57+
# ))
58+
#}
5459

5560
res <- web_component(
5661
"bslib-tooltip",
57-
placement = rlang::arg_match(placement),
58-
html = if (html) NA,
59-
sanitize = if (sanitize) NA,
6062
id = id,
63+
placement = rlang::arg_match(placement),
64+
options = jsonlite::toJSON(options),
65+
!!!attribs,
6166
# Use display:none instead of <template> since shiny.js
6267
# doesn't bind to the contents of the latter
63-
div(body, style = "display:none;"),
64-
...
68+
div(!!!children, style = "display:none;"),
69+
trigger
6570
)
6671

6772
res <- tag_require(res, version = 5, caller = "tooltip()")
@@ -89,13 +94,15 @@ tooltip_toggle <- function(id, show = NULL, session = get_current_session()) {
8994
}
9095

9196

92-
#' @describeIn tooltip Update the `body` of a tooltip.
97+
#' @describeIn tooltip Update the contents of a tooltip.
9398
#' @export
94-
tooltip_update <- function(id, body = NULL, session = get_current_session()) {
99+
tooltip_update <- function(id, ..., session = get_current_session()) {
100+
101+
title <- tagList(...)
95102

96103
msg <- dropNulls(list(
97104
method = "update",
98-
title = if (!is.null(body)) processDeps(body, session)
105+
title = if (length(title) > 0) processDeps(title, session)
99106
))
100107

101108
force(id)
@@ -105,6 +112,10 @@ tooltip_update <- function(id, body = NULL, session = get_current_session()) {
105112
session$onFlush(callback, once = TRUE)
106113
}
107114

115+
# TODO: worth it?
116+
# tooltip_disable <- function(id) {}
117+
# tooltip_enable <- function(id) {}
118+
108119

109120
normalize_show_value <- function(show) {
110121
if (is.null(show)) return("toggle")

srcts/src/components/card.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ class Card {
157157
*/
158158
private _addEventListeners(): void {
159159
const btnFullScreen = this.card.querySelector(
160-
`:scope > .${Card.attr.CLASS_FULL_SCREEN_ENTER}`
160+
`:scope > * > .${Card.attr.CLASS_FULL_SCREEN_ENTER}`
161161
);
162162
if (!btnFullScreen) return;
163163
btnFullScreen.addEventListener("click", (ev) => this.enterFullScreen(ev));

srcts/src/components/tooltip.ts

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,27 +34,18 @@ export class BslibTooltip extends LightElement {
3434
_tooltip!: TooltipType;
3535

3636
@property({ type: String }) placement: TooltipOptions["placement"] = "auto";
37-
@property({ type: Boolean }) html = false;
38-
@property({ type: Boolean }) sanitize = false;
37+
@property({ type: String }) options = "{}";
3938

40-
get options(): TooltipOptions {
41-
// Offical 'public' API
42-
const opts: TooltipOptions = {
39+
get allOptions(): TooltipOptions {
40+
const opts = JSON.parse(this.options);
41+
return {
4342
title: this.title,
4443
placement: this.placement,
45-
html: this.html,
46-
sanitize: this.sanitize,
44+
// Bootstrap defaults to false, but we have our own HTML escaping
45+
html: true,
46+
sanitize: true,
47+
...opts,
4748
};
48-
49-
// Support 'unofficial' `data-bs-*` attributes
50-
for (const attr of this.attributes) {
51-
if (attr.name.startsWith("data-bs-")) {
52-
const key = attr.name.replace("data-bs-", "");
53-
(opts as any)[key] = attr.value;
54-
}
55-
}
56-
57-
return opts;
5849
}
5950

6051
get title(): string {
@@ -71,7 +62,7 @@ export class BslibTooltip extends LightElement {
7162
connectedCallback(): void {
7263
super.connectedCallback();
7364
this.reference.setAttribute("data-bs-toggle", "tooltip");
74-
this._tooltip = new Tooltip(this.reference, this.options);
65+
this._tooltip = new Tooltip(this.reference, this.allOptions);
7566

7667
this.reference.addEventListener("shown.bs.tooltip", this._onShown);
7768
this.reference.addEventListener("hidden.bs.tooltip", this._onHidden);
@@ -98,12 +89,15 @@ export class BslibTooltip extends LightElement {
9889
// Note: a child template will always be present as the first child,
9990
// so ignore the 1st child
10091
if (this.children.length > 1) {
101-
return this.children[this.children.length - 1];
92+
const ref = this.children[this.children.length - 1];
93+
ref.setAttribute("tabindex", "0");
94+
return ref;
10295
}
10396
// If there are childNodes (i.e., a text node), then wrap the last one in a
10497
// span and use that as the reference
10598
if (this.childNodes.length > 1) {
10699
const ref = document.createElement("span");
100+
ref.setAttribute("tabindex", "0");
107101
ref.append(this.childNodes[this.childNodes.length - 1]);
108102
this.appendChild(ref);
109103
return ref;
@@ -119,7 +113,6 @@ export class BslibTooltip extends LightElement {
119113

120114
private _onShown(): void {
121115
this.visible = true;
122-
// TODO: do we need to trigger a shown/hidden for Shiny?
123116
this.onChangeCallback(true);
124117
}
125118

@@ -143,6 +136,7 @@ export class BslibTooltip extends LightElement {
143136
this._tooltip[data.value]();
144137
} else if (method === "update") {
145138
if (data.title) {
139+
Shiny.renderDependencies(data.title.deps);
146140
// eslint-disable-next-line @typescript-eslint/naming-convention
147141
this._tooltip.setContent({ ".tooltip-inner": data.title.html });
148142
}

0 commit comments

Comments
 (0)