Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add popover() API #702

Merged
merged 43 commits into from
Jul 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a3c98e9
First stab at popover()
cpsievert Jul 14, 2023
d5a7b92
`devtools::document()` (GitHub Actions)
cpsievert Jul 20, 2023
deab36d
`yarn build` (GitHub Actions)
cpsievert Jul 20, 2023
fe48eb7
Resave distributed files (GitHub Action)
cpsievert Jul 20, 2023
02d42d4
Fix tooltip() options
cpsievert Jul 20, 2023
ca9470a
Fix careful updating logic; sync-up tooltip/popover feature-set (i.e.…
cpsievert Jul 20, 2023
fc50570
Better naming
cpsievert Jul 20, 2023
05c1b03
Add a ShinyResizeObserver (for better resizing of output bindings tha…
cpsievert Jul 20, 2023
9283c28
`yarn build` (GitHub Actions)
cpsievert Jul 20, 2023
d5356c7
Rd doc improvementsl update _pkgdown.yml
cpsievert Jul 20, 2023
53973ec
`devtools::document()` (GitHub Actions)
cpsievert Jul 20, 2023
0a408cb
Resave data (GitHub Action)
cpsievert Jul 20, 2023
82cc626
Rd doc improvements; update _pkgdown.yml
cpsievert Jul 20, 2023
045e2a1
Move popover-header css to js
cpsievert Jul 20, 2023
d24fc93
`devtools::document()` (GitHub Actions)
cpsievert Jul 20, 2023
b5d5a09
`yarn build` (GitHub Actions)
cpsievert Jul 20, 2023
d34a921
Resave distributed files (GitHub Action)
cpsievert Jul 20, 2023
7229245
Be more careful about the possibility of content/header not existing …
cpsievert Jul 20, 2023
7741c73
`yarn build` (GitHub Actions)
cpsievert Jul 20, 2023
e0b278d
Better handling of NULL options
cpsievert Jul 21, 2023
30dd252
Better focus management
cpsievert Jul 21, 2023
8e31525
Revert addition of getFirstFocusableChild helper (since we no longer …
cpsievert Jul 21, 2023
b3d4874
Get rid of additional content wrapper in tooltip()
cpsievert Jul 21, 2023
961fffa
Always include a close button, and put it in the body if no title is …
cpsievert Jul 21, 2023
e0af44a
Update themer demo to use tooltip() & popover()
cpsievert Jul 21, 2023
dc54ca6
`devtools::document()` (GitHub Actions)
cpsievert Jul 21, 2023
760a607
`yarn build` (GitHub Actions)
cpsievert Jul 21, 2023
6168a2c
Fixes for update_popover(). Also, simplify logic and improve styling …
cpsievert Jul 24, 2023
7ab9dd1
`yarn build` (GitHub Actions)
cpsievert Jul 24, 2023
87dee80
Resave data (GitHub Action)
cpsievert Jul 24, 2023
d839235
Close button should have type='button'
cpsievert Jul 25, 2023
bbeca7c
Code review
cpsievert Jul 25, 2023
5716ee6
`devtools::document()` (GitHub Actions)
cpsievert Jul 25, 2023
300a74c
`yarn build` (GitHub Actions)
cpsievert Jul 25, 2023
f176b4f
Remove file committed by mistake
cpsievert Jul 26, 2023
89f99f2
Better support for hyperlinks/anchors
cpsievert Jul 26, 2023
1743e69
Update aria-pressed in shown/hidden events (important when the show/h…
cpsievert Jul 26, 2023
83d5e85
Simplify/improve close button styles
cpsievert Jul 26, 2023
a007e29
Merge branch 'main' into popovers
cpsievert Jul 26, 2023
cefd8aa
`devtools::document()` (GitHub Actions)
cpsievert Jul 26, 2023
89dac5c
`yarn build` (GitHub Actions)
cpsievert Jul 26, 2023
2ad7b36
Update NEWS
cpsievert Jul 26, 2023
865b844
Tweak NEWS wording
cpsievert Jul 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ Collate:
'navs.R'
'onLoad.R'
'page.R'
'popover.R'
'precompiled.R'
'print.R'
'shiny-devmode.R'
Expand Down
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export(page_fixed)
export(page_fluid)
export(page_navbar)
export(page_sidebar)
export(popover)
export(precompiled_css_path)
export(remove_all_fill)
export(run_with_themer)
Expand All @@ -124,10 +125,12 @@ export(sidebar)
export(sidebar_toggle)
export(theme_bootswatch)
export(theme_version)
export(toggle_popover)
export(toggle_sidebar)
export(toggle_switch)
export(toggle_tooltip)
export(tooltip)
export(update_popover)
export(update_switch)
export(update_tooltip)
export(value_box)
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## New features

* Added `tooltip()`, `update_tooltip()`, and `toggle_tooltip()` for easy creation (and server-side updating) of [Bootstrap tooltips](https://getbootstrap.com/docs/5.2/components/tooltips/) (a way to display additional information when focusing (or hovering over) a UI element). (#662)
* Added `popover()`, `update_popover()`, and `toggle_popover()` for easy creation (and server-side updating) of [Bootstrap popovers](https://getbootstrap.com/docs/5.2/components/popovers/). Popovers are similar to tooltips, but are more persistent, and should primarily be used with button-like UI elements (e.g., `actionButton()`, `bsicons::bs_icon()`, etc). (#702)
* Added `input_switch()` and `update_switch()` for easy creation (and server-side updating) of a [Bootstrap's switch input](https://getbootstrap.com/docs/5.2/forms/checks-radios/#switches) (an on-off toggle for binary input values). (#483)
* Added two new toggle functions: `toggle_switch()` for toggling the state of an `input_switch()` element and `toggle_sidebar()` for toggling the state of a `sidebar()` element (`sidebar_toggle()` remains as an alias of `toggle_sidebar()`). (#709)

Expand Down
165 changes: 165 additions & 0 deletions R/popover.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#' Add a popover to a UI element
#'
#' 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 [div()] or [span()].
#' @param ... UI elements for the popover's body. Character strings are
#' [automatically escaped][htmlEscape()] unless marked as [HTML()].
#' @param title A title (header) for the popover.
#' @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:
#'
#' Like other bslib components, popovers can be themed by supplying [relevant
#' theming
#' variables](https://rstudio.github.io/bslib/articles/bs5-variables.html#popover-bg)
#' to [bs_theme()], which effects styling of every popover on the page. To
#' style a _specific_ popover differently from other popovers, utilize the
#' `customClass` option:
#'
#' ```
#' 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; }")
#' ```
cpsievert marked this conversation as resolved.
Show resolved Hide resolved
#'
#' @describeIn popover Add a popover to a UI element
#' @references <https://getbootstrap.com/docs/5.3/components/popovers/>
#' @export
#' @seealso [tooltip()]
#' @examplesIf 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])
)
cpsievert marked this conversation as resolved.
Show resolved Hide resolved
}

res <- web_component(
"bslib-popover",
id = id,
placement = rlang::arg_match(placement),
bsOptions = to_json(options),
!!!attribs,
# Use display:none instead of <template> since shiny.js
# doesn't bind to the contents of the latter
div(
style = "display:none;",
div(!!!children),
div(title)
),
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 (length(title) > 0) processDeps(title, session)
))

force(id)
callback <- function() {
session$sendInputMessage(id, msg)
}
session$onFlush(callback, once = TRUE)
}
51 changes: 40 additions & 11 deletions R/tooltip.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,44 @@
#' Display additional information when focusing (or hovering over) a UI element.
#'
#' @param trigger A UI element (i.e., [htmltools tag][htmltools::tags]) to serve
#' as the tooltips trigger. It's good practice for this element to be a
#' keyboard-focusable and interactive element (e.g., `actionButton()`,
#' `actionLink()`, etc) so that the tooltip is accessible to keyboard and
#' assistive technology users.
#' as the tooltip trigger. 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 [div()] or [span()].
#' @param ... UI elements for the tooltip. Character strings are [automatically
#' escaped][htmlEscape()] unless marked as [HTML()].
#' @param id A character string. Required to re-actively respond to the
#' visibility of the tooltip (via the `input[[id]]` value) and/or update the
#' visibility/contents of the tooltip.
#' @param placement The placement of the tooltip relative to its trigger.
#' @param options A list of additional [Bootstrap
#' options](https://getbootstrap.com/docs/5.3/components/tooltips/#options).
#' @param options A list of additional [options](https://getbootstrap.com/docs/5.3/components/tooltips/#options).
#'
#' @details If `trigger` yields multiple HTML elements (e.g., a `tagList()` or
#' complex `{htmlwidgets}` object), the last HTML element is used as the
#' trigger. If the `trigger` should contain all of those elements, wrap the
#' object in a [div()] or [span()].
#' @section Theming/Styling:
#'
#' Like other bslib components, tooltips can be themed by supplying [relevant
#' theming
#' variables](https://rstudio.github.io/bslib/articles/bs5-variables.html#tooltip-bg)
#' to [bs_theme()], which effects styling of every popover on the page. To
#' style a _specific_ popover differently from other popovers, utilize the
#' `customClass` option:
#'
#' ```
#' tooltip(
#' "Trigger", "Tooltip message",
#' options = list(customClass = "my-tip")
#' )
#' ```
#'
#' And then add relevant rules to [bs_theme()] via [bs_add_rules()]:
#'
#' ```
#' bs_theme() |> bs_add_rules(".my-tip { max-width: none; }")
#' ```
#'
#' @describeIn tooltip Add a tooltip to a UI element
#' @references <https://getbootstrap.com/docs/5.3/components/tooltips/>
#' @export
#' @seealso [popover()]
#' @examplesIf interactive()
#'
#' tooltip(
Expand Down Expand Up @@ -57,11 +74,18 @@ tooltip <- function(
abort("At least one value must be provided to `...`.")
}

bad_opts <- intersect(c("title", "placement"), names(options))
if (length(bad_opts) > 0) {
rlang::abort(
sprintf("The `%s` option cannot be specified directly.", bad_opts[1])
)
}
cpsievert marked this conversation as resolved.
Show resolved Hide resolved

res <- web_component(
"bslib-tooltip",
id = id,
placement = rlang::arg_match(placement),
options = jsonlite::toJSON(options, auto_unbox = TRUE),
bsOptions = to_json(options),
cpsievert marked this conversation as resolved.
Show resolved Hide resolved
!!!attribs,
# Use display:none instead of <template> since shiny.js
# doesn't bind to the contents of the latter
Expand Down Expand Up @@ -123,3 +147,8 @@ normalize_show_value <- function(show) {

if (show) "show" else "hide"
}


to_json <- function(..., auto_unbox = TRUE, null = "null") {
jsonlite::toJSON(..., auto_unbox = auto_unbox, null = null)
}
5 changes: 3 additions & 2 deletions _pkgdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,12 @@ reference:
Highlight important findings
contents:
- value_box
- title: Tooltips
- title: Tooltips & Popovers
description: |
Provide additional context
Provide details on demand
contents:
- tooltip
- popover
- title: Input controls
description: |
UI controls for capturing user input
Expand Down
Loading