diff --git a/NEWS.md b/NEWS.md index 376ccb9c1..51be5d76b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,13 +1,19 @@ # shiny (development version) +## New busy indication feature + +Shiny now includes busy indication by default, which more specifically means: + 1. Calculating/recalculating outputs now have a spinner overlay. + 2. When no outputs are calculating, but Shiny is busy calculating something (e.g., a download, side-effect, etc), a page-level pulsing banner is shown. + +If either 1 or 2 leads to undesirable behavior in your app, you can disable them entirely with `useBusyIndicators(spinners = FALSE, pulse = FALSE)`. In addition, various properties of the spinners and pulse can be customized with `busyIndicatorOptions()`. For more details, see `?busyIndicatorOptions`. (#4040, #4104) + ## New features and improvements * The client-side TypeScript code for Shiny has been refactored so that the `Shiny` object is now an instance of class `ShinyClass`. (#4063) * In TypeScript, the `Shiny` object has a new property `initializedPromise`, which is a Promise-like object that can be `await`ed or chained with `.then()`. This Promise-like object corresponds to the `shiny:sessioninitialized` JavaScript event, but is easier to use because it can be used both before and after the events have occurred. (#4063) -* Added new functions, `useBusyIndicators()` and `busyIndicatorOptions()`, for enabling and customizing busy indication. Busy indicators provide a visual cue to users when the server is busy calculating outputs or otherwise serving requests to the client. When enabled, a spinner is shown on each calculating/recalculating output, and a pulsing banner is shown at the top of the page when the app is otherwise busy. (#4040) - * Output bindings now include the `.recalculating` CSS class when they are first bound, up until the first render. This makes it possible/easier to show progress indication when the output is calculating for the first time. (#4039) * A new `shiny.client_devmode` option controls client-side devmode features, in particular the client-side error console introduced in shiny 1.8.1, independently of the R-side features of `shiny::devmode()`. This usage is primarily intended for automatic use in Shinylive. (#4073) diff --git a/R/busy-indicators.R b/R/busy-indicators.R index f721dddb4..61fe330af 100644 --- a/R/busy-indicators.R +++ b/R/busy-indicators.R @@ -19,6 +19,8 @@ #' output. #' @param pulse Whether to show a pulsing banner at the top of the page when the #' app is busy. +#' @param fade Whether to fade recalculating outputs. A value of `FALSE` is +#' equivalent to `busyIndicatorOptions(fade_opacity=1)`. #' #' @export #' @seealso [busyIndicatorOptions()] for customizing the appearance of the busy @@ -48,7 +50,7 @@ #' } #' #' shinyApp(ui, server) -useBusyIndicators <- function(..., spinners = TRUE, pulse = TRUE) { +useBusyIndicators <- function(..., spinners = TRUE, pulse = TRUE, fade = TRUE) { rlang::check_dots_empty() @@ -62,20 +64,33 @@ useBusyIndicators <- function(..., spinners = TRUE, pulse = TRUE) { } }) - js <- HTML(paste(js, collapse = "\n")) - # TODO: it'd be nice if htmltools had something like a page_attrs() that allowed us # to do this without needing to inject JS into the head. - tags$script(js) + res <- tags$script(HTML(paste(js, collapse = "\n"))) + + if (!fade) { + res <- tagList(res, fadeOptions(opacity = 1)) + } + + res } #' Customize busy indicator options #' -#' When busy indicators are enabled (see [useBusyIndicators()]), a spinner is -#' shown on each calculating/recalculating output, and a pulsing banner is shown -#' at the top of the page when the app is otherwise busy. This function allows -#' you to customize the appearance of those busy indicators. To apply the -#' customization, include the result of this function inside the app's UI. +#' @description +#' Shiny automatically includes busy indicators, which more specifically means: +#' 1. Calculating/recalculating outputs have a spinner overlay. +#' 2. Outputs fade out/in when recalculating. +#' 3. When no outputs are calculating/recalculating, but Shiny is busy +#' doing something else (e.g., a download, side-effect, etc), a page-level +#' pulsing banner is shown. +#' +#' This function allows you to customize the appearance of these busy indicators +#' by including the result of this function inside the app's UI. Note that, +#' unless `spinner_selector` (or `fade_selector`) is specified, the spinner/fade +#' customization applies to the parent element. If the customization should +#' instead apply to the entire page, set `spinner_selector = 'html'` and +#' `fade_selector = 'html'`. #' #' @param ... Currently ignored. #' @param spinner_type The type of spinner. Pre-bundled types include: @@ -97,6 +112,11 @@ useBusyIndicators <- function(..., spinners = TRUE, pulse = TRUE) { #' @param spinner_selector A character string containing a CSS selector for #' scoping the spinner customization. The default (`NULL`) will apply the #' spinner customization to the parent element of the spinner. +#' @param fade_opacity The opacity (a number between 0 and 1) for recalculating +#' output. Set to 1 to "disable" the fade. +#' @param fade_selector A character string containing a CSS selector for +#' scoping the spinner customization. The default (`NULL`) will apply the +#' spinner customization to the parent element of the spinner. #' @param pulse_background A CSS background definition for the pulse. The #' default uses a #' [linear-gradient](https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient) @@ -107,7 +127,7 @@ useBusyIndicators <- function(..., spinners = TRUE, pulse = TRUE) { #' time. #' #' @export -#' @seealso [useBusyIndicators()] for enabling/disabling busy indicators. +#' @seealso [useBusyIndicators()] to disable/enable busy indicators. #' @examplesIf rlang::is_interactive() #' #' library(bslib) @@ -162,6 +182,8 @@ busyIndicatorOptions <- function( spinner_size = NULL, spinner_delay = NULL, spinner_selector = NULL, + fade_opacity = NULL, + fade_selector = NULL, pulse_background = NULL, pulse_height = NULL, pulse_speed = NULL @@ -177,6 +199,7 @@ busyIndicatorOptions <- function( delay = spinner_delay, selector = spinner_selector ), + fadeOptions(opacity = fade_opacity, selector = fade_selector), pulseOptions( background = pulse_background, height = pulse_height, @@ -224,6 +247,26 @@ spinnerOptions <- function(type = NULL, color = NULL, size = NULL, delay = NULL, tags$style(css, id = id) } +fadeOptions <- function(opacity = NULL, selector = NULL) { + if (is.null(opacity) && is.null(selector)) { + return(NULL) + } + + css_vars <- htmltools::css( + "--shiny-fade-opacity" = opacity + ) + + id <- NULL + if (is.null(selector)) { + id <- paste0("fade-options-", p_randomInt(100, 1000000)) + selector <- sprintf(":has(> #%s)", id) + } + + css <- HTML(paste0(selector, " {", css_vars, "}")) + + tags$style(css, id = id) +} + pulseOptions <- function(background = NULL, height = NULL, speed = NULL) { if (is.null(background) && is.null(height) && is.null(speed)) { return(NULL) @@ -244,6 +287,7 @@ busyIndicatorDependency <- function() { version = get_package_version("shiny"), src = "www/shared/busy-indicators", package = "shiny", - stylesheet = "busy-indicators.css" + stylesheet = "busy-indicators.css", + head = as.character(useBusyIndicators()) ) } diff --git a/inst/www/shared/busy-indicators/busy-indicators.css b/inst/www/shared/busy-indicators/busy-indicators.css index 3165efb7f..dc4159915 100644 --- a/inst/www/shared/busy-indicators/busy-indicators.css +++ b/inst/www/shared/busy-indicators/busy-indicators.css @@ -1,2 +1,2 @@ /*! shiny 1.8.1.9001 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */ -:where([data-shiny-busy-spinners] .recalculating){position:relative}[data-shiny-busy-spinners] .recalculating{opacity:1}[data-shiny-busy-spinners] .recalculating:after{position:absolute;content:"";--_shiny-spinner-url: var(--shiny-spinner-url, url(spinners/ring.svg));--_shiny-spinner-color: var(--shiny-spinner-color, var(--bs-primary, #007bc2));--_shiny-spinner-size: var(--shiny-spinner-size, 32px);--_shiny-spinner-delay: var(--shiny-spinner-delay, 1s);background:var(--_shiny-spinner-color);width:var(--_shiny-spinner-size);height:var(--_shiny-spinner-size);inset:calc(50% - var(--_shiny-spinner-size) / 2);mask-image:var(--_shiny-spinner-url);-webkit-mask-image:var(--_shiny-spinner-url);opacity:0;animation-delay:var(--_shiny-spinner-delay);animation-name:fade-in;animation-duration:.25s;animation-fill-mode:forwards}[data-shiny-busy-spinners] .recalculating>*:not(.recalculating){opacity:.2;transition:opacity .25s ease var(--shiny-spinner-delay, 1s)}[data-shiny-busy-spinners] .recalculating.shiny-html-output:after{display:none}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:after{--_shiny-pulse-background: var( --shiny-pulse-background, linear-gradient( 120deg, var(--bs-body-bg, #fff), var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), var(--bs-body-bg, #fff) ) );--_shiny-pulse-height: var(--shiny-pulse-height, 5px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.85s);position:fixed;top:0;left:0;height:var(--_shiny-pulse-height);background:var(--_shiny-pulse-background);z-index:9999;animation-name:busy-page-pulse;animation-duration:var(--_shiny-pulse-speed);animation-iteration-count:infinite;animation-timing-function:ease-in-out;content:""}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(.recalculating):after{display:none}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(#shiny-disconnected-overlay):after{display:none}[data-shiny-busy-pulse]:not([data-shiny-busy-spinners]).shiny-busy:after{--_shiny-pulse-background: var( --shiny-pulse-background, linear-gradient( 120deg, var(--bs-body-bg, #fff), var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), var(--bs-body-bg, #fff) ) );--_shiny-pulse-height: var(--shiny-pulse-height, 5px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.85s);position:fixed;top:0;left:0;height:var(--_shiny-pulse-height);background:var(--_shiny-pulse-background);z-index:9999;animation-name:busy-page-pulse;animation-duration:var(--_shiny-pulse-speed);animation-iteration-count:infinite;animation-timing-function:ease-in-out;content:""}[data-shiny-busy-pulse]:not([data-shiny-busy-spinners]).shiny-busy:has(#shiny-disconnected-overlay):after{display:none}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes busy-page-pulse{0%{left:-75%;width:75%}50%{left:100%;width:75%}to{left:-75%;width:75%}} +:where([data-shiny-busy-spinners] .recalculating){position:relative}[data-shiny-busy-spinners] .recalculating:after{position:absolute;content:"";--_shiny-spinner-url: var(--shiny-spinner-url, url(spinners/ring.svg));--_shiny-spinner-color: var(--shiny-spinner-color, var(--bs-primary, #007bc2));--_shiny-spinner-size: var(--shiny-spinner-size, 32px);--_shiny-spinner-delay: var(--shiny-spinner-delay, 1s);background:var(--_shiny-spinner-color);width:var(--_shiny-spinner-size);height:var(--_shiny-spinner-size);inset:calc(50% - var(--_shiny-spinner-size) / 2);mask-image:var(--_shiny-spinner-url);-webkit-mask-image:var(--_shiny-spinner-url);opacity:0;animation-delay:var(--_shiny-spinner-delay);animation-name:fade-in;animation-duration:.25s;animation-fill-mode:forwards}[data-shiny-busy-spinners] .recalculating:has(>*),[data-shiny-busy-spinners] .recalculating:empty{opacity:1}[data-shiny-busy-spinners] .recalculating>*:not(.recalculating){opacity:var(--_shiny-fade-opacity);transition:opacity .25s ease var(--shiny-spinner-delay, 1s)}[data-shiny-busy-spinners] .recalculating.shiny-html-output:after{display:none}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:after{--_shiny-pulse-background: var( --shiny-pulse-background, linear-gradient( 120deg, var(--bs-body-bg, #fff), var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), var(--bs-body-bg, #fff) ) );--_shiny-pulse-height: var(--shiny-pulse-height, 5px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.85s);position:fixed;top:0;left:0;height:var(--_shiny-pulse-height);background:var(--_shiny-pulse-background);z-index:9999;animation-name:busy-page-pulse;animation-duration:var(--_shiny-pulse-speed);animation-iteration-count:infinite;animation-timing-function:ease-in-out;content:""}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(.recalculating):after{display:none}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(#shiny-disconnected-overlay):after{display:none}[data-shiny-busy-pulse]:not([data-shiny-busy-spinners]).shiny-busy:after{--_shiny-pulse-background: var( --shiny-pulse-background, linear-gradient( 120deg, var(--bs-body-bg, #fff), var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), var(--bs-body-bg, #fff) ) );--_shiny-pulse-height: var(--shiny-pulse-height, 5px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.85s);position:fixed;top:0;left:0;height:var(--_shiny-pulse-height);background:var(--_shiny-pulse-background);z-index:9999;animation-name:busy-page-pulse;animation-duration:var(--_shiny-pulse-speed);animation-iteration-count:infinite;animation-timing-function:ease-in-out;content:""}[data-shiny-busy-pulse]:not([data-shiny-busy-spinners]).shiny-busy:has(#shiny-disconnected-overlay):after{display:none}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes busy-page-pulse{0%{left:-75%;width:75%}50%{left:100%;width:75%}to{left:-75%;width:75%}}.shiny-spinner-output-container{--shiny-spinner-size: 0px} diff --git a/inst/www/shared/shiny.min.css b/inst/www/shared/shiny.min.css index b42c32bf3..c8e913a24 100644 --- a/inst/www/shared/shiny.min.css +++ b/inst/www/shared/shiny.min.css @@ -1,2 +1,2 @@ /*! shiny 1.8.1.9001 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */ -pre.shiny-text-output:empty:before{content:" "}pre.shiny-text-output.noplaceholder:empty{margin:0;padding:0;border-width:0;height:0}pre.shiny-text-output{word-wrap:normal}.shiny-image-output img.shiny-scalable,.shiny-plot-output img.shiny-scalable{max-width:100%;max-height:100%}#shiny-disconnected-overlay{position:fixed;inset:0;background-color:#999;opacity:.5;overflow:hidden;z-index:99998;pointer-events:none}html.autoreload-enabled #shiny-disconnected-overlay.reloading{opacity:0;animation:fadeIn .25s forwards;animation-delay:1s}@keyframes fadeIn{to{opacity:.1}}.table.shiny-table>thead>tr>th,.table.shiny-table>thead>tr>td,.table.shiny-table>tbody>tr>th,.table.shiny-table>tbody>tr>td,.table.shiny-table>tfoot>tr>th,.table.shiny-table>tfoot>tr>td{padding-right:12px;padding-left:12px}.shiny-table.spacing-xs>thead>tr>th,.shiny-table.spacing-xs>thead>tr>td,.shiny-table.spacing-xs>tbody>tr>th,.shiny-table.spacing-xs>tbody>tr>td,.shiny-table.spacing-xs>tfoot>tr>th,.shiny-table.spacing-xs>tfoot>tr>td{padding-top:3px;padding-bottom:3px}.shiny-table.spacing-s>thead>tr>th,.shiny-table.spacing-s>thead>tr>td,.shiny-table.spacing-s>tbody>tr>th,.shiny-table.spacing-s>tbody>tr>td,.shiny-table.spacing-s>tfoot>tr>th,.shiny-table.spacing-s>tfoot>tr>td{padding-top:5px;padding-bottom:5px}.shiny-table.spacing-m>thead>tr>th,.shiny-table.spacing-m>thead>tr>td,.shiny-table.spacing-m>tbody>tr>th,.shiny-table.spacing-m>tbody>tr>td,.shiny-table.spacing-m>tfoot>tr>th,.shiny-table.spacing-m>tfoot>tr>td{padding-top:8px;padding-bottom:8px}.shiny-table.spacing-l>thead>tr>th,.shiny-table.spacing-l>thead>tr>td,.shiny-table.spacing-l>tbody>tr>th,.shiny-table.spacing-l>tbody>tr>td,.shiny-table.spacing-l>tfoot>tr>th,.shiny-table.spacing-l>tfoot>tr>td{padding-top:10px;padding-bottom:10px}.shiny-table .NA{color:#909090}.shiny-output-error{color:red;white-space:pre-wrap}.shiny-output-error:before{content:"Error: ";font-weight:700}.shiny-output-error-validation{color:#888}.shiny-output-error-validation:before{content:"";font-weight:inherit}@supports (-ms-ime-align:auto){.shiny-bound-output{transition:0}}.recalculating{opacity:.3;transition:opacity .25s ease .5s}.slider-animate-container{text-align:right;margin-top:-9px}.slider-animate-button{position:relative;z-index:1;opacity:.5}.slider-animate-button .pause{display:none}.slider-animate-button.playing .pause,.slider-animate-button .play{display:inline}.slider-animate-button.playing .play{display:none}.progress.shiny-file-input-progress{visibility:hidden}.progress.shiny-file-input-progress .progress-bar.bar-danger{transition:none}.btn-file{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.shiny-input-container input[type=file]{overflow:hidden;max-width:100%}.shiny-progress-container{position:fixed;top:0;width:100%;z-index:2000}.shiny-progress .progress{position:absolute;width:100%;top:0;height:3px;margin:0}.shiny-progress .bar{opacity:.6;transition-duration:.25s}.shiny-progress .progress-text{position:absolute;right:10px;width:240px;background-color:#eef8ff;margin:0;padding:2px 3px;opacity:.85}.shiny-progress .progress-text .progress-message{padding:0 3px;font-weight:700;font-size:90%}.shiny-progress .progress-text .progress-detail{padding:0 3px;font-size:80%}.shiny-progress-notification .progress{margin-bottom:5px;height:10px}.shiny-progress-notification .progress-text .progress-message{font-weight:700;font-size:90%}.shiny-progress-notification .progress-text .progress-detail{font-size:80%}.shiny-label-null{display:none}.crosshair{cursor:crosshair}.grabbable{cursor:grab;cursor:-moz-grab;cursor:-webkit-grab}.grabbing{cursor:grabbing;cursor:-moz-grabbing;cursor:-webkit-grabbing}.ns-resize{cursor:ns-resize}.ew-resize{cursor:ew-resize}.nesw-resize{cursor:nesw-resize}.nwse-resize{cursor:nwse-resize}.qt pre,.qt code{font-family:monospace!important}.qt5 .radio input[type=radio],.qt5 .checkbox input[type=checkbox]{margin-top:0}.qtmac input[type=radio],.qtmac input[type=checkbox]{zoom:1.0000001}.shiny-frame{border:none}.shiny-flow-layout>div{display:inline-block;vertical-align:top;padding-right:12px;width:220px}.shiny-split-layout{width:100%;white-space:nowrap}.shiny-split-layout>div{display:inline-block;vertical-align:top;box-sizing:border-box;overflow:auto}.shiny-input-panel{padding:6px 8px;margin-top:6px;margin-bottom:6px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:2px}.shiny-input-checkboxgroup label~.shiny-options-group,.shiny-input-radiogroup label~.shiny-options-group{margin-top:-10px}.shiny-input-checkboxgroup.shiny-input-container-inline label~.shiny-options-group,.shiny-input-radiogroup.shiny-input-container-inline label~.shiny-options-group{margin-top:-1px}.shiny-input-container:not(.shiny-input-container-inline){width:300px;max-width:100%}.well .shiny-input-container{width:auto}.shiny-input-container>div>select:not(.selectized){width:100%}#shiny-notification-panel{position:fixed;bottom:0;right:0;background-color:#0000;padding:2px;width:300px;max-width:100%;z-index:99999}.shiny-notification{position:relative;background-color:#e8e8e8;color:#333;border:1px solid #ccc;border-radius:3px;opacity:.85;padding:10px 2rem 10px 10px;margin:5px}.shiny-notification-message{color:#31708f;background-color:#d9edf7;border:1px solid #bce8f1}.shiny-notification-warning{color:#8a6d3b;background-color:#fcf8e3;border:1px solid #faebcc}.shiny-notification-error{color:#a94442;background-color:#f2dede;border:1px solid #ebccd1}.shiny-notification-close{position:absolute;width:2rem;height:2rem;top:0;right:0;display:flex;align-items:center;justify-content:center;font-weight:400;font-size:1.125em;padding:.25rem;color:#444;cursor:pointer}.shiny-notification-close:hover{color:#000;font-weight:700}.shiny-notification-content-action a{color:#337ab7;text-decoration:underline;font-weight:700}.shiny-file-input-active{box-shadow:inset 0 1px 1px #00000013,0 0 8px #66afe999}.shiny-file-input-over{box-shadow:inset 0 1px 1px #00000013,0 0 8px #4cae4c99}.datepicker table tbody tr td.disabled,.datepicker table tbody tr td.disabled:hover,.datepicker table tbody tr td span.disabled,.datepicker table tbody tr td span.disabled:hover{color:#aaa;cursor:not-allowed}.nav-hidden{display:none!important} +pre.shiny-text-output:empty:before{content:" "}pre.shiny-text-output.noplaceholder:empty{margin:0;padding:0;border-width:0;height:0}pre.shiny-text-output{word-wrap:normal}.shiny-image-output img.shiny-scalable,.shiny-plot-output img.shiny-scalable{max-width:100%;max-height:100%}#shiny-disconnected-overlay{position:fixed;inset:0;background-color:#999;opacity:.5;overflow:hidden;z-index:99998;pointer-events:none}html.autoreload-enabled #shiny-disconnected-overlay.reloading{opacity:0;animation:fadeIn .25s forwards;animation-delay:1s}@keyframes fadeIn{to{opacity:.1}}.table.shiny-table>thead>tr>th,.table.shiny-table>thead>tr>td,.table.shiny-table>tbody>tr>th,.table.shiny-table>tbody>tr>td,.table.shiny-table>tfoot>tr>th,.table.shiny-table>tfoot>tr>td{padding-right:12px;padding-left:12px}.shiny-table.spacing-xs>thead>tr>th,.shiny-table.spacing-xs>thead>tr>td,.shiny-table.spacing-xs>tbody>tr>th,.shiny-table.spacing-xs>tbody>tr>td,.shiny-table.spacing-xs>tfoot>tr>th,.shiny-table.spacing-xs>tfoot>tr>td{padding-top:3px;padding-bottom:3px}.shiny-table.spacing-s>thead>tr>th,.shiny-table.spacing-s>thead>tr>td,.shiny-table.spacing-s>tbody>tr>th,.shiny-table.spacing-s>tbody>tr>td,.shiny-table.spacing-s>tfoot>tr>th,.shiny-table.spacing-s>tfoot>tr>td{padding-top:5px;padding-bottom:5px}.shiny-table.spacing-m>thead>tr>th,.shiny-table.spacing-m>thead>tr>td,.shiny-table.spacing-m>tbody>tr>th,.shiny-table.spacing-m>tbody>tr>td,.shiny-table.spacing-m>tfoot>tr>th,.shiny-table.spacing-m>tfoot>tr>td{padding-top:8px;padding-bottom:8px}.shiny-table.spacing-l>thead>tr>th,.shiny-table.spacing-l>thead>tr>td,.shiny-table.spacing-l>tbody>tr>th,.shiny-table.spacing-l>tbody>tr>td,.shiny-table.spacing-l>tfoot>tr>th,.shiny-table.spacing-l>tfoot>tr>td{padding-top:10px;padding-bottom:10px}.shiny-table .NA{color:#909090}.shiny-output-error{color:red;white-space:pre-wrap}.shiny-output-error:before{content:"Error: ";font-weight:700}.shiny-output-error-validation{color:#888}.shiny-output-error-validation:before{content:"";font-weight:inherit}@supports (-ms-ime-align:auto){.shiny-bound-output{transition:0}}.recalculating{--_shiny-fade-opacity: var(--shiny-fade-opacity, .3);opacity:var(--_shiny-fade-opacity);transition:opacity .25s ease .5s}.slider-animate-container{text-align:right;margin-top:-9px}.slider-animate-button{position:relative;z-index:1;opacity:.5}.slider-animate-button .pause{display:none}.slider-animate-button.playing .pause,.slider-animate-button .play{display:inline}.slider-animate-button.playing .play{display:none}.progress.shiny-file-input-progress{visibility:hidden}.progress.shiny-file-input-progress .progress-bar.bar-danger{transition:none}.btn-file{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.shiny-input-container input[type=file]{overflow:hidden;max-width:100%}.shiny-progress-container{position:fixed;top:0;width:100%;z-index:2000}.shiny-progress .progress{position:absolute;width:100%;top:0;height:3px;margin:0}.shiny-progress .bar{opacity:.6;transition-duration:.25s}.shiny-progress .progress-text{position:absolute;right:10px;width:240px;background-color:#eef8ff;margin:0;padding:2px 3px;opacity:.85}.shiny-progress .progress-text .progress-message{padding:0 3px;font-weight:700;font-size:90%}.shiny-progress .progress-text .progress-detail{padding:0 3px;font-size:80%}.shiny-progress-notification .progress{margin-bottom:5px;height:10px}.shiny-progress-notification .progress-text .progress-message{font-weight:700;font-size:90%}.shiny-progress-notification .progress-text .progress-detail{font-size:80%}.shiny-label-null{display:none}.crosshair{cursor:crosshair}.grabbable{cursor:grab;cursor:-moz-grab;cursor:-webkit-grab}.grabbing{cursor:grabbing;cursor:-moz-grabbing;cursor:-webkit-grabbing}.ns-resize{cursor:ns-resize}.ew-resize{cursor:ew-resize}.nesw-resize{cursor:nesw-resize}.nwse-resize{cursor:nwse-resize}.qt pre,.qt code{font-family:monospace!important}.qt5 .radio input[type=radio],.qt5 .checkbox input[type=checkbox]{margin-top:0}.qtmac input[type=radio],.qtmac input[type=checkbox]{zoom:1.0000001}.shiny-frame{border:none}.shiny-flow-layout>div{display:inline-block;vertical-align:top;padding-right:12px;width:220px}.shiny-split-layout{width:100%;white-space:nowrap}.shiny-split-layout>div{display:inline-block;vertical-align:top;box-sizing:border-box;overflow:auto}.shiny-input-panel{padding:6px 8px;margin-top:6px;margin-bottom:6px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:2px}.shiny-input-checkboxgroup label~.shiny-options-group,.shiny-input-radiogroup label~.shiny-options-group{margin-top:-10px}.shiny-input-checkboxgroup.shiny-input-container-inline label~.shiny-options-group,.shiny-input-radiogroup.shiny-input-container-inline label~.shiny-options-group{margin-top:-1px}.shiny-input-container:not(.shiny-input-container-inline){width:300px;max-width:100%}.well .shiny-input-container{width:auto}.shiny-input-container>div>select:not(.selectized){width:100%}#shiny-notification-panel{position:fixed;bottom:0;right:0;background-color:#0000;padding:2px;width:300px;max-width:100%;z-index:99999}.shiny-notification{position:relative;background-color:#e8e8e8;color:#333;border:1px solid #ccc;border-radius:3px;opacity:.85;padding:10px 2rem 10px 10px;margin:5px}.shiny-notification-message{color:#31708f;background-color:#d9edf7;border:1px solid #bce8f1}.shiny-notification-warning{color:#8a6d3b;background-color:#fcf8e3;border:1px solid #faebcc}.shiny-notification-error{color:#a94442;background-color:#f2dede;border:1px solid #ebccd1}.shiny-notification-close{position:absolute;width:2rem;height:2rem;top:0;right:0;display:flex;align-items:center;justify-content:center;font-weight:400;font-size:1.125em;padding:.25rem;color:#444;cursor:pointer}.shiny-notification-close:hover{color:#000;font-weight:700}.shiny-notification-content-action a{color:#337ab7;text-decoration:underline;font-weight:700}.shiny-file-input-active{box-shadow:inset 0 1px 1px #00000013,0 0 8px #66afe999}.shiny-file-input-over{box-shadow:inset 0 1px 1px #00000013,0 0 8px #4cae4c99}.datepicker table tbody tr td.disabled,.datepicker table tbody tr td.disabled:hover,.datepicker table tbody tr td span.disabled,.datepicker table tbody tr td span.disabled:hover{color:#aaa;cursor:not-allowed}.nav-hidden{display:none!important} diff --git a/inst/www/shared/shiny_scss/shiny.bootstrap5.scss b/inst/www/shared/shiny_scss/shiny.bootstrap5.scss index 69a36b5cf..f6ba34a70 100644 --- a/inst/www/shared/shiny_scss/shiny.bootstrap5.scss +++ b/inst/www/shared/shiny_scss/shiny.bootstrap5.scss @@ -44,9 +44,9 @@ div:where(.shiny-html-output) { /* uiOutput()/ conditionalPanel() are "pass-through" containers when they have children. */ &:has(> *) { display: contents; - /* Pass along styles that no longer impact the pass-through container */ + /* Pass along styles that no longer impact the pass-through container */ &.recalculating > * { - opacity: 0.3; + opacity: var(--_shiny-fade-opacity); } } } diff --git a/inst/www/shared/shiny_scss/shiny.scss b/inst/www/shared/shiny_scss/shiny.scss index 916b1b0b5..ad80dc439 100644 --- a/inst/www/shared/shiny_scss/shiny.scss +++ b/inst/www/shared/shiny_scss/shiny.scss @@ -156,7 +156,8 @@ html.autoreload-enabled #shiny-disconnected-overlay.reloading { } .recalculating { - opacity: 0.3; + --_shiny-fade-opacity: var(--shiny-fade-opacity, 0.3); + opacity: var(--_shiny-fade-opacity); transition: opacity 250ms ease 500ms; } diff --git a/man/busyIndicatorOptions.Rd b/man/busyIndicatorOptions.Rd index 7cc17048d..6b7096aef 100644 --- a/man/busyIndicatorOptions.Rd +++ b/man/busyIndicatorOptions.Rd @@ -11,6 +11,8 @@ busyIndicatorOptions( spinner_size = NULL, spinner_delay = NULL, spinner_selector = NULL, + fade_opacity = NULL, + fade_selector = NULL, pulse_background = NULL, pulse_height = NULL, pulse_speed = NULL @@ -45,6 +47,13 @@ if the computation finishes quickly.} scoping the spinner customization. The default (\code{NULL}) will apply the spinner customization to the parent element of the spinner.} +\item{fade_opacity}{The opacity (a number between 0 and 1) for recalculating +output. Set to 1 to "disable" the fade.} + +\item{fade_selector}{A character string containing a CSS selector for +scoping the spinner customization. The default (\code{NULL}) will apply the +spinner customization to the parent element of the spinner.} + \item{pulse_background}{A CSS background definition for the pulse. The default uses a \href{https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient}{linear-gradient} @@ -57,11 +66,21 @@ CSS size.} time.} } \description{ -When busy indicators are enabled (see \code{\link[=useBusyIndicators]{useBusyIndicators()}}), a spinner is -shown on each calculating/recalculating output, and a pulsing banner is shown -at the top of the page when the app is otherwise busy. This function allows -you to customize the appearance of those busy indicators. To apply the -customization, include the result of this function inside the app's UI. +Shiny automatically includes busy indicators, which more specifically means: +\enumerate{ +\item Calculating/recalculating outputs have a spinner overlay. +\item Outputs fade out/in when recalculating. +\item When no outputs are calculating/recalculating, but Shiny is busy +doing something else (e.g., a download, side-effect, etc), a page-level +pulsing banner is shown. +} + +This function allows you to customize the appearance of these busy indicators +by including the result of this function inside the app's UI. Note that, +unless \code{spinner_selector} (or \code{fade_selector}) is specified, the spinner/fade +customization applies to the parent element. If the customization should +instead apply to the entire page, set \code{spinner_selector = 'html'} and +\code{fade_selector = 'html'}. } \examples{ \dontshow{if (rlang::is_interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} @@ -113,5 +132,5 @@ shinyApp(ui, server) \dontshow{\}) # examplesIf} } \seealso{ -\code{\link[=useBusyIndicators]{useBusyIndicators()}} for enabling/disabling busy indicators. +\code{\link[=useBusyIndicators]{useBusyIndicators()}} to disable/enable busy indicators. } diff --git a/man/useBusyIndicators.Rd b/man/useBusyIndicators.Rd index d1b4f45cb..05ce53b62 100644 --- a/man/useBusyIndicators.Rd +++ b/man/useBusyIndicators.Rd @@ -4,7 +4,7 @@ \alias{useBusyIndicators} \title{Enable/disable busy indication} \usage{ -useBusyIndicators(..., spinners = TRUE, pulse = TRUE) +useBusyIndicators(..., spinners = TRUE, pulse = TRUE, fade = TRUE) } \arguments{ \item{...}{Currently ignored.} @@ -14,6 +14,9 @@ output.} \item{pulse}{Whether to show a pulsing banner at the top of the page when the app is busy.} + +\item{fade}{Whether to fade recalculating outputs. A value of \code{FALSE} is +equivalent to \code{busyIndicatorOptions(fade_opacity=1)}.} } \description{ Busy indicators provide a visual cue to users when the server is busy diff --git a/srcts/extras/busy-indicators/busy-indicators.scss b/srcts/extras/busy-indicators/busy-indicators.scss index 30c0101c2..587b6deaa 100644 --- a/srcts/extras/busy-indicators/busy-indicators.scss +++ b/srcts/extras/busy-indicators/busy-indicators.scss @@ -37,9 +37,11 @@ the spinner. Undo that, but still apply (smaller) opacity to immediate children that aren't recalculating. */ - opacity: 1; + &:has(> *), &:empty { + opacity: 1; + } > *:not(.recalculating) { - opacity: 0.2; + opacity: var(--_shiny-fade-opacity); transition: opacity 250ms ease var(--shiny-spinner-delay, 1s); } @@ -141,3 +143,12 @@ width: 75%; } } + + +// Effectively disable the spinner when it's wrapped in shinycssloader::withSpinner() +// since that's a sign our spinner isn't needed. +// The reason this sets size to 0px instead of display:none is so, if someone +// really wants to show the spinner, they can override this with a custom size. +.shiny-spinner-output-container { + --shiny-spinner-size: 0px; +} \ No newline at end of file