diff --git a/DESCRIPTION b/DESCRIPTION index e502fdca3..4aff9ae3f 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, @@ -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 6713dea1d..d475ea23b 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,6 +1,8 @@ # Generated by roxygen2: do not edit by hand S3method(as.tags,bslib_sidebar) +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) @@ -24,6 +26,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) @@ -78,6 +81,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) @@ -142,6 +146,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 15b33a462..36ffb1a3c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,6 +6,10 @@ * A `sidebar()` passed to `page_sidebar()`/`page_navbar()` is now always open (and not collapsible) by default on mobile screens. To revert to the old behavior, set `open = "desktop"` in the `sidebar`. (#943) +## 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()` 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 `