diff --git a/DESCRIPTION b/DESCRIPTION index e502fdca3..b3f7cabb5 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: bslib Title: Custom 'Bootstrap' 'Sass' Themes for 'shiny' and 'rmarkdown' -Version: 0.6.1.9000 +Version: 0.6.1.9001 Authors@R: c( person("Carson", "Sievert", , "carson@posit.co", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-4958-2844")), @@ -28,9 +28,12 @@ URL: https://rstudio.github.io/bslib/, https://github.com/rstudio/bslib BugReports: https://github.com/rstudio/bslib/issues Depends: R (>= 2.10) +Remotes: + rstudio/shiny Imports: base64enc, cachem, + fastmap (>= 1.1.1), grDevices, htmltools (>= 0.5.7), jquerylib (>= 0.1.3), @@ -44,6 +47,7 @@ Suggests: bsicons, curl, fontawesome, + future, ggplot2, knitr, magrittr, @@ -107,7 +111,7 @@ Config/testthat/parallel: true Config/testthat/start-first: zzzz-bs-sass, fonts, zzz-precompile, theme-*, rmd-* Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.1 Collate: 'accordion.R' 'breakpoints.R' @@ -124,6 +128,7 @@ Collate: 'bs-theme-update.R' 'bs-theme.R' 'bslib-package.R' + 'buttons.R' 'card.R' 'deprecated.R' 'files.R' diff --git a/NAMESPACE b/NAMESPACE index 876d237a7..0d13b9606 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,7 @@ # Generated by roxygen2: do not edit by hand +S3method(bind_task_button,ExtendedTask) +S3method(bind_task_button,default) S3method(is_fill_item,default) S3method(is_fill_item,htmlwidget) S3method(is_fillable_container,default) @@ -23,6 +25,7 @@ export(as.card_item) export(as_fill_carrier) export(as_fill_item) export(as_fillable_container) +export(bind_task_button) export(bootstrap) export(bootstrap_sass) export(bootswatch_themes) @@ -77,6 +80,7 @@ export(font_google) export(font_link) export(input_dark_mode) export(input_switch) +export(input_task_button) export(is.card_item) export(is_bs_theme) export(is_fill_carrier) @@ -141,6 +145,7 @@ export(toggle_tooltip) export(tooltip) export(update_popover) export(update_switch) +export(update_task_button) export(update_tooltip) export(value_box) export(value_box_theme) diff --git a/NEWS.md b/NEWS.md index 907faad3c..bf0af4ffd 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,11 @@ # bslib (development version) +## New features + +* Added `input_task_button()`, a replacement for `shiny::actionButton()` that automatically prevents an operation from being submitted multiple times. It does this by, upon click, immediately transitioning to a "Processing..." visual state that does not let the button be clicked again. The button resets to its clickable state automatically after the reactive flush it causes is complete; or, for advanced scenarios, `update_task_button()` can be used to manually control when the button resets. + +## Improvements + * `layout_columns()` now always uses a 12-unit grid when the user provides an integer value to `col_widths` for any breakpoint. For example, `breakpoints(md = 3, lg = NA)` will pick a best-fitting layout for large screen sizes using the 12-column grid. Previously, the best fit algorithm might adjust the number of columns as a shortcut to an easy solution. That shortcut is only taken when an auto-fit layout is requested for every breakpoint, e.g. `col_widths = breakpoints(md = NA, lg = NA)` or `col_widths = NA`. (#928) * `layout_columns()` was rewritten in Typescript as a custom element to improve the portability of the component. (#931) diff --git a/R/buttons.R b/R/buttons.R new file mode 100644 index 000000000..4efd17086 --- /dev/null +++ b/R/buttons.R @@ -0,0 +1,301 @@ +#' Button for launching longer-running operations +#' +#' @description +#' `input_task_button` is a button that can be used in conjuction with +#' [shiny::bindEvent()] (or the older [shiny::eventReactive()] and +#' [shiny::observeEvent()] functions) to trigger actions or recomputation. +#' +#' It is similar to [shiny::actionButton()], except it prevents the user from +#' clicking when its operation is already in progress. +#' +#' Upon click, it automatically displays a customizable progress message and +#' disables itself; and after the server has dealt with whatever reactivity is +#' triggered from the click, the button automatically reverts to its original +#' appearance and re-enables itself. +#' +#' @section Manual button reset: +#' In some advanced use cases, it may be necessary to keep a task button in its +#' busy state even after the normal reactive processing has completed. Calling +#' `update_task_button(id, state = "busy")` from the server will opt out of any +#' currently pending reset for a specific task button. After doing so, the +#' button can be re-enabled by calling `update_task_button(id, state = "ready")` +#' after each click's work is complete. +#' +#' You can also pass an explicit `auto_reset = FALSE` to `input_task_button()`, +#' which means that button will _never_ be automatically re-enabled and will +#' require `update_task_button(id, state = "ready")` to be called each time. +#' +#' Note that, as a general rule, Shiny's `update` family of functions do not +#' take effect at the instant that they are called, but are held until the end +#' of the current reactive cycle. So if you have many different reactive +#' calculations and outputs, you don't have to be too careful about when you +#' call `update_task_button(id, state = "ready")`, as the button on the client +#' will not actually re-enable until the same moment that all of the updated +#' outputs simultaneously sent to the client. +#' +#' @param id The `input` slot that will be used to access the value. +#' @param label The label of the button while it is in ready (clickable) state; +#' usually a string. +#' @param icon An optional icon to display next to the label while the button is +#' in ready state. See [fontawesome::fa_i()]. +#' @param label_busy The label of the button while it is busy. +#' @param icon_busy The icon to display while the button is busy. By default, +#' `fontawesome::fa_i("refresh", class = "fa-spin", "aria-hidden" = "true")` +#' is used, which displays a spinning "chasing arrows" icon. You can create +#' spinning icons out of other Font Awesome icons by using the same +#' expression, but replacing `"refresh"` with a different icon name. See +#' [fontawesome::fa_i()]. +#' @param type One of the Bootstrap theme colors (`"primary"`, `"default"`, +#' `"secondary"`, `"success"`, `"danger"`, `"warning"`, `"info"`, `"light"`, +#' `"dark"`), or `NULL` to leave off the Bootstrap-specific button CSS classes +#' altogether. +#' @param ... Named arguments become attributes to include on the `