Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
97016b8
Add design spec for toolbar_download_button()
elnelson575 Mar 16, 2026
8c14262
feat: Add toolbar_download_button() R functions
elnelson575 Mar 16, 2026
dd503b5
feat: Add TypeScript binding for toolbar_download_button
elnelson575 Mar 16, 2026
a0c9fc7
feat: Add SCSS styles for toolbar_download_button
elnelson575 Mar 16, 2026
9891624
test: Add tests for toolbar_download_button
elnelson575 Mar 16, 2026
4260dd3
`air format` (GitHub Actions)
elnelson575 Mar 16, 2026
c2105d8
`devtools::document()` (GitHub Actions)
elnelson575 Mar 16, 2026
f280ab4
Resave distributed files (GitHub Action)
elnelson575 Mar 16, 2026
915bc4b
Resave data (GitHub Action)
elnelson575 Mar 16, 2026
5ff6cce
Address PR review feedback
elnelson575 Mar 17, 2026
af81d20
Use custom message handler instead of InputBinding for download button
elnelson575 Apr 13, 2026
6865fba
feat: expand toolbarDownloadButton message handler to support label, …
elnelson575 Apr 27, 2026
29ddfe9
fix: correct imports, add null guards, use hasDefinedProperty in tool…
elnelson575 Apr 27, 2026
9048f74
fix: gate toolbar download button hover/focus behind btn-default to r…
elnelson575 Apr 27, 2026
a1f4de2
test: add failing tests for toolbar_download_button() enabled param
elnelson575 Apr 27, 2026
d42a592
feat: wrap shiny::downloadButton() and replace disabled with tristate…
elnelson575 Apr 27, 2026
4315c63
fix: use dev shiny downloadButton(enabled=) directly, remove post-pro…
elnelson575 Apr 27, 2026
116de3c
feat: expand update_toolbar_download_button() with label, icon, show_…
elnelson575 Apr 27, 2026
4889e6e
chore: rebuild components and regenerate docs
elnelson575 Apr 27, 2026
2591133
test: update toolbar_download_button() snapshots for enabled param
elnelson575 Apr 28, 2026
9e83859
docs: rewrite download-button demo for three enabled modes
elnelson575 Apr 28, 2026
b661049
fix: align with Shiny PR #4371 — rename data-ignore-update to data-sh…
elnelson575 May 7, 2026
b0181eb
feat: warn on invalid enabled values in toolbar_download_button()
elnelson575 May 7, 2026
b8b4cdb
Updated to add commented gate + tests
elnelson575 May 22, 2026
e5b45f6
chore: remove download-button design doc and autoenable demo
elnelson575 May 22, 2026
8ea8e24
feat: add toolbar_download_button with class support and fix hover st…
elnelson575 May 27, 2026
95b63b1
refactor: address PR review comments on toolbar_download_button
elnelson575 May 28, 2026
d19b45c
refactor: replace toolbar SCSS mixin with explicit shared selector block
elnelson575 May 28, 2026
70c8778
fix: align toolbar_download_button border class with toolbar_input_bu…
elnelson575 May 29, 2026
065775f
chore: commit compiled JS dist for toolbarDownloadButton.ts changes
elnelson575 May 29, 2026
162c6a4
docs: explain why action-label/action-icon wrappers are built manually
elnelson575 May 29, 2026
50b9bfa
docs: clarify LibSass comment and note download-button color override…
elnelson575 May 29, 2026
2ba24a6
refactor: simplify shared toolbar button styles to plain combined sel…
elnelson575 May 29, 2026
4f8eb18
refactor: consolidate shared toolbar button layout and interaction st…
elnelson575 May 29, 2026
7fe0fc4
fix: restore text-decoration suppression for all download button vari…
elnelson575 May 29, 2026
46d9ebd
test: toolbar test review — fix false-green, add 5 uncovered paths, t…
elnelson575 May 29, 2026
7ac2b78
docs: use @inheritParams for toolbar_download_button, expand example …
elnelson575 May 29, 2026
eef37dd
fix: guard label_text check with !is.null(label) in update functions;…
elnelson575 May 29, 2026
c6f0172
docs: remove renderUI note from enabled param — general Shiny behavio…
elnelson575 May 29, 2026
58e486f
chore: restore height comment in toolbar button block
elnelson575 May 29, 2026
741bfeb
chore: expand hover/active rules, clarify color comment in toolbar bu…
elnelson575 May 29, 2026
63aa1e0
chore: recompile CSS after rebase on main
elnelson575 May 29, 2026
7f4c636
test: clarify show_label test comment re: icon default
elnelson575 May 29, 2026
e2a889e
fix: update_toolbar_download_button() and test cleanup
elnelson575 May 29, 2026
eecaa9e
docs: inheritParams for update_toolbar_download_button
elnelson575 May 29, 2026
02457f1
`air format` (GitHub Actions)
elnelson575 May 29, 2026
0f4d60a
`devtools::document()` (GitHub Actions)
elnelson575 May 29, 2026
ad39e56
`yarn build` (GitHub Actions)
elnelson575 May 29, 2026
932ff62
Resave data (GitHub Action)
elnelson575 May 29, 2026
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
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export(toggle_switch)
export(toggle_tooltip)
export(toolbar)
export(toolbar_divider)
export(toolbar_download_button)
export(toolbar_input_button)
export(toolbar_input_select)
export(toolbar_spacer)
Expand All @@ -174,6 +175,7 @@ export(update_popover)
export(update_submit_textarea)
export(update_switch)
export(update_task_button)
export(update_toolbar_download_button)
export(update_toolbar_input_button)
export(update_toolbar_input_select)
export(update_tooltip)
Expand Down
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# bslib (development version)

## New features

* Added `toolbar_download_button()` and `update_toolbar_download_button()` for adding a download button to a `toolbar()`, styled consistently with other toolbar inputs. (#1292)

## Bug fixes

* Fixed label-to-options spacing on `shiny::radioButtons()` and `shiny::checkboxGroupInput()` in Bootstrap 5, where a Shiny rule was overriding the bslib fix. (#1308)
Expand Down
Binary file modified R/sysdata.rda
Binary file not shown.
241 changes: 235 additions & 6 deletions R/toolbar.R
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,6 @@ toolbar_input_button <- function(

# Validate that label has text for accessibility
label_text <- paste(unlist(find_characters(label)), collapse = " ")
# Verifies the label contains non-empty text
if (!nzchar(trimws(label_text))) {
warning(
"Consider providing a non-empty string label for accessibility."
Expand Down Expand Up @@ -480,11 +479,13 @@ update_toolbar_input_button <- function(
disabled = NULL,
session = get_current_session()
) {
label_text <- paste(unlist(find_characters(label)), collapse = " ")
if (!nzchar(trimws(label_text))) {
rlang::warn(
"Consider providing a non-empty string label for accessibility."
)
if (!is.null(label)) {
label_text <- paste(unlist(find_characters(label)), collapse = " ")
if (!nzchar(trimws(label_text))) {
rlang::warn(
"Consider providing a non-empty string label for accessibility."
)
}
}

icon <- validateIcon(icon)
Expand Down Expand Up @@ -960,3 +961,231 @@ toolbar_divider <- function(..., width = NULL, gap = NULL) {
toolbar_spacer <- function() {
div(class = "bslib-toolbar-spacer")
}

#' Toolbar Download Button
#'
#' @description
#' A download button designed to fit well in small places such as in a [toolbar()].
#'
#' @param outputId The download output ID (connects to [shiny::downloadHandler()] in server).
#' @inheritParams toolbar_input_button
#' @param enabled Controls the initial enabled/disabled state and whether Shiny
#' manages auto-enabling:
#' * `"auto"` (default) — button starts disabled; Shiny auto-enables it once
#' the [shiny::downloadHandler()] is initialized. Use
#' [update_toolbar_download_button()] to disable again after that point.
#' * `TRUE` — button starts enabled immediately (before the server connects).
#' Sets `data-shiny-disable-auto-enable` so Shiny does not re-enable on
#' render (preserves state set by e.g. `shinyjs::disable()`).
#' * `FALSE` — button starts disabled with `data-shiny-disable-auto-enable`,
#' permanently opting out of Shiny's auto-enable. Use
#' [update_toolbar_download_button()] to manage enabled/disabled state.
#'
#' @return Returns a download button suitable for use in a toolbar.
#'
#' @examplesIf rlang::is_interactive()
#' library(shiny)
#' library(bslib)
#'
#' ui <- page_fluid(
#' card(
#' card_header(
#' "Flower Data",
#' toolbar(
#' align = "right",
#' toolbar_download_button("download_data", label = "Download CSV")
#' )
#' ),
#' tableOutput("table")
#' )
#' )
#'
#' server <- function(input, output, session) {
#' output$table <- renderTable(head(iris))
#'
#' output$download_data <- downloadHandler(
#' filename = function() "iris.csv",
#' content = function(file) write.csv(iris, file, row.names = FALSE)
#' )
#' }
#'
#' shinyApp(ui, server)
#'
#' @family toolbar components
#' @export
toolbar_download_button <- function(
outputId,
label = "Download",
icon = shiny::icon("download"),
show_label = FALSE,
tooltip = !show_label,
...,
enabled = c("auto", TRUE, FALSE),
border = FALSE
) {
check_shiny_supports_download_button_enabled("toolbar_download_button()")
# Normalize the match.arg vector default to "auto", then hard-error on
# anything else invalid (mirrors shiny::downloadButton()'s approach).
if (identical(enabled, c("auto", TRUE, FALSE))) {
enabled <- "auto"
}
if (!isTRUE(enabled) && !isFALSE(enabled) && !identical(enabled, "auto")) {
rlang::abort(
paste0(
'`enabled` must be TRUE, FALSE, or "auto". Got ',
deparse(enabled),
"."
)
)
}

btn_type <-
if (is.null(icon)) {
if (!show_label) {
rlang::abort("If `show_label` is FALSE, `icon` must be provided.")
}
"label"
} else {
if (show_label) "both" else "icon"
}

# Validate that label has text for accessibility
label_text <- paste(unlist(find_characters(label)), collapse = " ")
if (!nzchar(trimws(label_text))) {
warning("Consider providing a non-empty string label for accessibility.")
}

label_id <- paste0("btn-label-", p_randomInt(1000, 10000))

# shiny::downloadButton() doesn't wrap label/icon in span.action-label /
# span.action-icon (unlike shiny::actionButton()), so we do it here to keep
# the DOM structure consistent between the two toolbar button types.

# We hide the label visually if `!show_label` but keep it in the DOM for use
# with `aria-labelledby`. This ensures that ARIA always uses the label text:
# screen readers will read out the icon's `aria-label` even if the icon is a
# descendant of an element with `aria-hidden=true`.
label_elem <- span(
class = "action-label",
Comment thread
elnelson575 marked this conversation as resolved.
span(
id = label_id,
class = "bslib-toolbar-label",
hidden = if (!show_label) NA else NULL,
label
)
)

# Wrap the icon so it is always treated as decorative (`aria-hidden`),
# preventing screen readers from announcing the icon in addition to the label.
icon_elem <- span(
class = "action-icon",
Comment thread
elnelson575 marked this conversation as resolved.
span(
class = "bslib-toolbar-icon",
`aria-hidden` = "true",
style = "pointer-events: none",
icon
)
)

# Unlike shiny::actionButton(), shiny::downloadButton() has `class` as a
# formal, so we extract it from ... and merge it manually to avoid a
# duplicate-argument error.
dots <- rlang::list2(...)
extra_class <- dots[["class"]]
dots[["class"]] <- NULL

button <- rlang::inject(shiny::downloadButton(
outputId,
label = label_elem,
icon = icon_elem,
class = paste(
c(
"bslib-toolbar-download-button btn-sm",
if (!border) "border-0" else "border-1",
extra_class
),
collapse = " "
),
enabled = enabled,
`data-type` = btn_type,
`aria-labelledby` = label_id,
!!!dots
))

# If tooltip is literally TRUE, use the label as the tooltip text.
if (isTRUE(tooltip)) {
tooltip <- label
}
if (isFALSE(tooltip)) {
tooltip <- NULL
}
if (!is.null(tooltip)) {
# Default placement is "bottom" for the toolbar case because otherwise the
# tooltip ends up covering the neighboring buttons in the header/footer.
button <- tooltip(
button,
tooltip,
id = sprintf("%s_tooltip", outputId),
placement = "bottom"
)
}

button
}

#' @inheritParams toolbar_download_button
#' @inheritParams update_toolbar_input_button
#' @param disabled If `TRUE`, disables the button; if `FALSE`, enables it.
Comment thread
elnelson575 marked this conversation as resolved.
#' `NULL` (default) leaves the current state unchanged.
#'
#' @describeIn toolbar_download_button Update a toolbar download button.
#' @export
update_toolbar_download_button <- function(
outputId,
label = NULL,
show_label = NULL,
icon = NULL,
disabled = NULL,
session = get_current_session()
) {
check_shiny_supports_download_button_enabled(
"update_toolbar_download_button()"
)
if (!is.null(label)) {
label_text <- paste(unlist(find_characters(label)), collapse = " ")
if (!nzchar(trimws(label_text))) {
rlang::warn(
"Consider providing a non-empty string label for accessibility."
)
}
}

icon <- validateIcon(icon)
icon_processed <- if (!is.null(icon)) processDeps(icon, session)
label_processed <- if (!is.null(label)) processDeps(label, session)

Comment thread
elnelson575 marked this conversation as resolved.
message <- dropNulls(list(
id = outputId,
label = label_processed,
showLabel = show_label,
icon = icon_processed,
disabled = disabled
))

session$sendCustomMessage("bslib.toolbar-download-button", message)
}

# Gate on shiny > 1.13.0 — needs `downloadButton(enabled =)` from rstudio/shiny#4371.
check_shiny_supports_download_button_enabled <- function(fn) {
if (is_installed("shiny", "1.13.0.9000")) {
return(invisible())
}
rlang::abort(c(
sprintf("`%s` requires a newer version of shiny than is installed.", fn),
i = sprintf(
"Installed shiny version: %s. Need shiny > 1.13.0 (for `downloadButton(enabled =)`, rstudio/shiny#4371).",
utils::packageVersion("shiny")
),
i = "Install the development version with `pak::pak(\"rstudio/shiny\")` or wait for the next CRAN release."
))
}
2 changes: 1 addition & 1 deletion inst/components/dist/components.css

Large diffs are not rendered by default.

Loading