From 7642fc84b762819421598dfd3df61c775eb319b3 Mon Sep 17 00:00:00 2001 From: olivroy <52606734+olivroy@users.noreply.github.com> Date: Tue, 21 Jan 2025 11:00:51 -0500 Subject: [PATCH 1/3] Replace crayon by cli + address some TODOs to add some color (#4170) * Replace crayon by cli + address some TODOs to add some color * docs: add news --------- Co-authored-by: Garrick Aden-Buie --- DESCRIPTION | 3 +-- NEWS.md | 4 ++++ R/conditions.R | 6 +++--- R/test.R | 8 +++----- tests/testthat/test-test-runTests.R | 2 +- tests/testthat/test-test-server-app.R | 4 ++-- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 3763bd0e22..68f3e79461 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -85,7 +85,7 @@ Imports: later (>= 1.0.0), promises (>= 1.3.2), tools, - crayon, + cli, rlang (>= 0.4.10), fastmap (>= 1.1.1), withr, @@ -210,7 +210,6 @@ Collate: RoxygenNote: 7.3.2 Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RdMacros: lifecycle Config/testthat/edition: 3 Config/Needs/check: shinytest2 diff --git a/NEWS.md b/NEWS.md index 687af2abb6..1db08151a1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # shiny (development version) +## New features and improvements + +* Shiny now uses `{cli}` instead of `{crayon}` for rich log messages. (@olivroy #4170) + ## Bug fixes * Fixed a bug with modals where calling `removeModal()` too quickly after `showModal()` would fail to remove the modal if the remove modal message was received while the modal was in the process of being revealed. (#4173) diff --git a/R/conditions.R b/R/conditions.R index 164792a8de..241f6403a5 100644 --- a/R/conditions.R +++ b/R/conditions.R @@ -417,11 +417,11 @@ printOneStackTrace <- function(stackTrace, stripResult, full, offset) { ": ", mapply(paste0(st$call, st$loc), st$category, FUN = function(name, category) { if (category == "pkg") - crayon::silver(name) + cli::col_silver(name) else if (category == "user") - crayon::blue$bold(name) + cli::style_bold(cli::col_blue(name)) else - crayon::white(name) + cli::col_white(name) }), "\n" ) diff --git a/R/test.R b/R/test.R index 98b5ce7326..bad1bf1082 100644 --- a/R/test.R +++ b/R/test.R @@ -158,8 +158,7 @@ print.shiny_runtests <- function(x, ..., reporter = "summary") { if (any(x$pass)) { - # TODO in future... use clisymbols::symbol$tick and crayon green - cat("* Success\n") + cli::cat_bullet("Success", bullet = "tick", bullet_col = "green") mapply( x$file, x$pass, @@ -171,9 +170,8 @@ print.shiny_runtests <- function(x, ..., reporter = "summary") { } ) } - if (any(!x$pass)) { - # TODO in future... use clisymbols::symbol$cross and crayon red - cat("* Failure\n") + if (!all(x$pass)) { + cli::cat_bullet("Failure", bullet = "cross", bullet_col = "red") mapply( x$file, x$pass, diff --git a/tests/testthat/test-test-runTests.R b/tests/testthat/test-test-runTests.R index 159060cd67..c42fd0a6cc 100644 --- a/tests/testthat/test-test-runTests.R +++ b/tests/testthat/test-test-runTests.R @@ -122,7 +122,7 @@ test_that("runTests runs as expected without rewiring", { appDir <- test_path(file.path("..", "test-helpers", "app1-standard")) df <- testthat::expect_output( print(runTests(appDir = appDir, assert = FALSE)), - "Shiny App Test Results\\n\\* Success\\n - app1-standard/tests/runner1\\.R\\n - app1-standard/tests/runner2\\.R" + "Shiny App Test Results\\n\\v Success\\n - app1-standard/tests/runner1\\.R\\n - app1-standard/tests/runner2\\.R" ) expect_equal(df, data.frame( diff --git a/tests/testthat/test-test-server-app.R b/tests/testthat/test-test-server-app.R index 4c6a364bde..cab7dff7cf 100644 --- a/tests/testthat/test-test-server-app.R +++ b/tests/testthat/test-test-server-app.R @@ -41,7 +41,7 @@ test_that("runTests works with a dir app that calls modules and uses testServer" app <- test_path("..", "test-modules", "12_counter") run <- testthat::expect_output( print(runTests(app)), - "Shiny App Test Results\\n\\* Success\\n - 12_counter/tests/testthat\\.R" + "Shiny App Test Results\\n\\v Success\\n - 12_counter/tests/testthat\\.R" ) expect_true(all(run$pass)) }) @@ -50,7 +50,7 @@ test_that("runTests works with a dir app that calls modules that return reactive app <- test_path("..", "test-modules", "107_scatterplot") run <- testthat::expect_output( print(runTests(app)), - "Shiny App Test Results\\n\\* Success\\n - 107_scatterplot/tests/testthat\\.R" + "Shiny App Test Results\\n\\v Success\\n - 107_scatterplot/tests/testthat\\.R" ) expect_true(all(run$pass)) }) From 8ad779f9490614519fc01dfda74753ebff139ca0 Mon Sep 17 00:00:00 2001 From: olivroy <52606734+olivroy@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:08:00 -0500 Subject: [PATCH 2/3] Various test lints (#4171) Co-authored-by: Garrick Aden-Buie --- DESCRIPTION | 2 +- tests/testthat/test-bootstrap.r | 14 ++++++----- tests/testthat/test-busy-indication.R | 2 +- tests/testthat/test-input-select.R | 34 +++++++++++++------------- tests/testthat/test-plot-png.R | 2 +- tests/testthat/test-reactivity.r | 10 ++++---- tests/testthat/test-render-functions.R | 4 +-- tests/testthat/test-stacks.R | 6 ++--- tests/testthat/test-tabPanel.R | 4 +-- tests/testthat/test-update-input.R | 20 +++++++-------- tests/testthat/test-utils.R | 2 +- 11 files changed, 49 insertions(+), 51 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 68f3e79461..122fa67702 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -99,7 +99,7 @@ Suggests: datasets, DT, Cairo (>= 1.5-5), - testthat (>= 3.0.0), + testthat (>= 3.2.1), knitr (>= 1.6), markdown, rmarkdown, diff --git a/tests/testthat/test-bootstrap.r b/tests/testthat/test-bootstrap.r index f39bb726cd..c2e9b51dba 100644 --- a/tests/testthat/test-bootstrap.r +++ b/tests/testthat/test-bootstrap.r @@ -21,10 +21,11 @@ test_that("Repeated names for selectInput and radioButtons choices", { # Select input x <- selectInput('id','label', choices = c(a='x1', a='x2', b='x3'), selectize = FALSE) - expect_true(grepl(fixed = TRUE, + expect_match( + format(x), '', - format(x) - )) + fixed = TRUE + ) # Radio buttons using choices x <- radioButtons('id','label', choices = c(a='x1', a='x2', b='x3')) @@ -248,10 +249,11 @@ test_that("selectInput selects items by default", { )) # Nothing selected when choices=NULL - expect_true(grepl(fixed = TRUE, + expect_match( + format(selectInput('x', NULL, NULL, selectize = FALSE)), '', - format(selectInput('x', NULL, NULL, selectize = FALSE)) - )) + fixed = TRUE + ) # None specified as selected. With multiple=TRUE, none selected by default. expect_true(grepl(fixed = TRUE, diff --git a/tests/testthat/test-busy-indication.R b/tests/testthat/test-busy-indication.R index 4fdd90aee1..1f36d92bf8 100644 --- a/tests/testthat/test-busy-indication.R +++ b/tests/testthat/test-busy-indication.R @@ -48,7 +48,7 @@ test_that("busyIndicatorOptions()", { test_that("Can provide svg file for busyIndicatorOptions(spinner_type)", { - skip_if(.Platform$OS.type == "windows") + skip_on_os("windows") tmpsvg <- tempfile(fileext = ".svg") writeLines("", tmpsvg) diff --git a/tests/testthat/test-input-select.R b/tests/testthat/test-input-select.R index b41a2a2072..d30c686f6a 100644 --- a/tests/testthat/test-input-select.R +++ b/tests/testthat/test-input-select.R @@ -1,10 +1,10 @@ test_that("performance warning works", { pattern <- "consider using server-side selectize" - expect_warning(selectInput("x", "x", as.character(1:999)), NA) - expect_warning(selectInput("x", "x", as.character(1:999), selectize = TRUE), NA) - expect_warning(selectInput("x", "x", as.character(1:999), selectize = FALSE), NA) - expect_warning(selectizeInput("x", "x", as.character(1:999)), NA) + expect_no_warning(selectInput("x", "x", as.character(1:999))) + expect_no_warning(selectInput("x", "x", as.character(1:999), selectize = TRUE)) + expect_no_warning(selectInput("x", "x", as.character(1:999), selectize = FALSE)) + expect_no_warning(selectizeInput("x", "x", as.character(1:999))) expect_warning(selectInput("x", "x", as.character(1:1000)), pattern) expect_warning(selectInput("x", "x", as.character(1:1000), selectize = TRUE), pattern) @@ -17,9 +17,9 @@ test_that("performance warning works", { session <- MockShinySession$new() - expect_warning(updateSelectInput(session, "x", choices = as.character(1:999)), NA) - expect_warning(updateSelectizeInput(session, "x", choices = as.character(1:999)), NA) - expect_warning(updateSelectizeInput(session, "x", choices = as.character(1:999), server = FALSE), NA) + expect_no_warning(updateSelectInput(session, "x", choices = as.character(1:999))) + expect_no_warning(updateSelectizeInput(session, "x", choices = as.character(1:999))) + expect_no_warning(updateSelectizeInput(session, "x", choices = as.character(1:999), server = FALSE)) expect_warning(updateSelectInput(session, "x", choices = as.character(1:1000)), pattern) expect_warning(updateSelectizeInput(session, "x", choices = as.character(1:1000)), pattern) @@ -28,9 +28,9 @@ test_that("performance warning works", { expect_warning(updateSelectizeInput(session, "x", choices = as.character(1:2000)), pattern) expect_warning(updateSelectizeInput(session, "x", choices = as.character(1:2000), server = FALSE), pattern) - expect_warning(updateSelectizeInput(session, "x", choices = as.character(1:999), server = TRUE), NA) - expect_warning(updateSelectizeInput(session, "x", choices = as.character(1:1000), server = TRUE), NA) - expect_warning(updateSelectizeInput(session, "x", choices = as.character(1:2000), server = TRUE), NA) + expect_no_warning(updateSelectizeInput(session, "x", choices = as.character(1:999), server = TRUE)) + expect_no_warning(updateSelectizeInput(session, "x", choices = as.character(1:1000), server = TRUE)) + expect_no_warning(updateSelectizeInput(session, "x", choices = as.character(1:2000), server = TRUE)) }) @@ -55,9 +55,9 @@ test_that("selectInput options are properly escaped", { )) si_str <- as.character(si) - expect_true(any(grepl("", si_str, fixed = TRUE))) + expect_match(si_str, "", fixed = TRUE, all = FALSE) }) @@ -75,10 +75,10 @@ test_that("selectInputUI has a select at an expected location", { ) # if this getter is changed, varSelectInput getter needs to be changed selectHtml <- selectInputVal$children[[2]]$children[[1]] - expect_true(inherits(selectHtml, "shiny.tag")) + expect_s3_class(selectHtml, "shiny.tag") expect_equal(selectHtml$name, "select") if (!is.null(selectHtml$attribs$class)) { - expect_false(grepl(selectHtml$attribs$class, "symbol")) + expect_no_match(selectHtml$attribs$class, "symbol") } varSelectInputVal <- varSelectInput( @@ -91,9 +91,9 @@ test_that("selectInputUI has a select at an expected location", { ) # if this getter is changed, varSelectInput getter needs to be changed varSelectHtml <- varSelectInputVal$children[[2]]$children[[1]] - expect_true(inherits(varSelectHtml, "shiny.tag")) + expect_s3_class(varSelectHtml, "shiny.tag") expect_equal(varSelectHtml$name, "select") - expect_true(grepl("symbol", varSelectHtml$attribs$class, fixed = TRUE)) + expect_match(varSelectHtml$attribs$class, "symbol", fixed = TRUE) } } } diff --git a/tests/testthat/test-plot-png.R b/tests/testthat/test-plot-png.R index 500746b184..54cae7feeb 100644 --- a/tests/testthat/test-plot-png.R +++ b/tests/testthat/test-plot-png.R @@ -2,5 +2,5 @@ test_that("plotPNG()/startPNG() ignores NULL dimensions", { f <- plotPNG(function() plot(1), width = NULL, height = NULL) on.exit(unlink(f)) bits <- readBin(f, "raw", file.info(f)$size) - expect_true(length(bits) > 0) + expect_gt(length(bits), 0) }) diff --git a/tests/testthat/test-reactivity.r b/tests/testthat/test-reactivity.r index 04ccfe48d1..d7a8372bb5 100644 --- a/tests/testthat/test-reactivity.r +++ b/tests/testthat/test-reactivity.r @@ -9,7 +9,7 @@ test_that("ReactiveVal", { val <- reactiveVal() isolate({ - expect_true(is.null(val())) + expect_null(val()) # Set to a simple value val(1) @@ -99,12 +99,12 @@ test_that("ReactiveValues", { values <- reactiveValues(a=NULL, b=2) # a should exist and be NULL expect_setequal(isolate(names(values)), c("a", "b")) - expect_true(is.null(isolate(values$a))) + expect_null(isolate(values$a)) # Assigning NULL should keep object (not delete it), and set value to NULL values$b <- NULL expect_setequal(isolate(names(values)), c("a", "b")) - expect_true(is.null(isolate(values$b))) + expect_null(isolate(values$b)) # Errors ----------------------------------------------------------------- @@ -960,8 +960,8 @@ test_that("classes of reactive object", { }) test_that("{} and NULL also work in reactive()", { - expect_error(reactive({}), NA) - expect_error(reactive(NULL), NA) + expect_no_error(reactive({})) + expect_no_error(reactive(NULL)) }) test_that("shiny.suppressMissingContextError option works", { diff --git a/tests/testthat/test-render-functions.R b/tests/testthat/test-render-functions.R index fe7bbefbfa..598fed3f06 100644 --- a/tests/testthat/test-render-functions.R +++ b/tests/testthat/test-render-functions.R @@ -29,8 +29,8 @@ test_that("Render functions correctly handle quosures", { r1 <- inject(renderTable({ pressure[!!a, ] }, digits = 1)) r2 <- renderTable({ eval_tidy(quo(pressure[!!a, ])) }, digits = 1) a <- 2 - expect_true(grepl("0\\.0", r1())) - expect_true(grepl("20\\.0", r2())) + expect_match(r1(), "0\\.0") + expect_match(r2(), "20\\.0") }) test_that("functionLabel returns static value when the label can not be assigned to", { diff --git a/tests/testthat/test-stacks.R b/tests/testthat/test-stacks.R index c3adc4779d..b340482170 100644 --- a/tests/testthat/test-stacks.R +++ b/tests/testthat/test-stacks.R @@ -227,7 +227,7 @@ test_that("observeEvent is not overly stripped (#4162)", { }) ) st_str <- capture.output(printStackTrace(caught), type = "message") - expect_true(any(grepl("observeEvent\\(1\\)", st_str))) + expect_match(st_str, "observeEvent\\(1\\)", all = FALSE) # Now same thing, but deep stack trace version @@ -257,6 +257,6 @@ test_that("observeEvent is not overly stripped (#4162)", { ) st_str <- capture.output(printStackTrace(caught), type = "message") # cat(st_str, sep = "\n") - expect_true(any(grepl("A__", st_str))) - expect_true(any(grepl("B__", st_str))) + expect_match(st_str, "A__", all = FALSE) + expect_match(st_str, "B__", all = FALSE) }) diff --git a/tests/testthat/test-tabPanel.R b/tests/testthat/test-tabPanel.R index 0c03f6bc44..7a6c45c7fe 100644 --- a/tests/testthat/test-tabPanel.R +++ b/tests/testthat/test-tabPanel.R @@ -115,7 +115,5 @@ test_that("tabItem titles can contain tag objects", { # " Hello world" # As opposed to: # "<i>Hello</i> world - expect_true( - grepl("]+>\\s*Hello\\s+world", x$html) - ) + expect_match(x$html, "]+>\\s*Hello\\s+world") }) diff --git a/tests/testthat/test-update-input.R b/tests/testthat/test-update-input.R index 369db4efc1..0a4a87d27f 100644 --- a/tests/testthat/test-update-input.R +++ b/tests/testthat/test-update-input.R @@ -15,22 +15,20 @@ test_that("Radio buttons and checkboxes work with modules", { updateRadioButtons(sessA, "test1", label = "Label", choices = letters[1:5]) resultA <- sessA$lastInputMessage - expect_equal("test1", resultA$id) - expect_equal("Label", resultA$message$label) - expect_equal("a", resultA$message$value) - expect_true(grepl('"modA-test1"', resultA$message$options)) - expect_false(grepl('"test1"', resultA$message$options)) - + expect_equal(resultA$id, "test1") + expect_equal(resultA$message$label, "Label") + expect_equal(resultA$message$value, "a") + expect_match(resultA$message$options, '"modA-test1"') + expect_no_match(resultA$message$options, '"test1"') sessB <- createModuleSession("modB") updateCheckboxGroupInput(sessB, "test2", label = "Label", choices = LETTERS[1:5]) resultB <- sessB$lastInputMessage - expect_equal("test2", resultB$id) - expect_equal("Label", resultB$message$label) + expect_equal(resultB$id, "test2") + expect_equal(resultB$message$label, "Label") expect_null(resultB$message$value) - expect_true(grepl('"modB-test2"', resultB$message$options)) - expect_false(grepl('"test2"', resultB$message$options)) - + expect_match(resultB$message$options, '"modB-test2"') + expect_no_match(resultB$message$options, '"test2"') }) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index df462bf1d3..2bcb9d7c51 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -4,7 +4,7 @@ test_that("Private randomness works at startup", { rm(".Random.seed", envir = .GlobalEnv) .globals$ownSeed <- NULL # Just make sure this doesn't blow up - expect_error(createUniqueId(4), NA) + expect_no_error(createUniqueId(4)) }) test_that("Setting process-wide seed doesn't affect private randomness", { From d764ea9b4e4f773d7dc5c3bfc2e2741441b10b44 Mon Sep 17 00:00:00 2001 From: Carson Sievert Date: Wed, 22 Jan 2025 14:14:20 -0600 Subject: [PATCH 3/3] Busy indicator improvements (#4172) * Make sure spinner is visible when htmlwidget errors are visible * Give recalculating outputs a min-height large enough to show the spinner * tableOutput() now gets the spinner treatment * yarn run bundle_extras * Forward visibility hidden for all recalculating widgets, not just those with a error message (otherwise spinner won't be visible after a req()) * Update news --- NEWS.md | 5 ++++ R/bootstrap.R | 2 +- .../busy-indicators/busy-indicators.css | 2 +- .../busy-indicators/busy-indicators.scss | 25 ++++++++++++++++++- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index 1db08151a1..de29901ff1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,11 @@ ## New features and improvements +* When busy indicators are enabled (i.e., `useBusyIndicators()`), Shiny now: + * Shows a spinner on recalculating htmlwidgets that have previously rendered an error (including `req()` and `validate()`). (#4172) + * Shows a spinner on `tableOutput()`. (#4172) + * Places a minimum height on recalculating outputs so that the spinner is always visible. (#4172) + * Shiny now uses `{cli}` instead of `{crayon}` for rich log messages. (@olivroy #4170) ## Bug fixes diff --git a/R/bootstrap.R b/R/bootstrap.R index 970b434009..23aeb05bed 100644 --- a/R/bootstrap.R +++ b/R/bootstrap.R @@ -1113,7 +1113,7 @@ plotOutput <- function(outputId, width = "100%", height="400px", #' @rdname renderTable #' @export tableOutput <- function(outputId) { - div(id = outputId, class="shiny-html-output") + div(id = outputId, class="shiny-html-output shiny-table-output") } dataTableDependency <- list( diff --git a/inst/www/shared/busy-indicators/busy-indicators.css b/inst/www/shared/busy-indicators/busy-indicators.css index 4de6c82e04..5ab9fcdd57 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.10.0.9000 | (c) 2012-2025 Posit Software, PBC. | License: GPL-3 | file LICENSE */ -: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, transparent, var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), transparent ) );--_shiny-pulse-height: var(--shiny-pulse-height, 3px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.2s);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-direction:alternate;animation-iteration-count:infinite;animation-timing-function:ease-in-out;content:""}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(.recalculating:not(.shiny-html-output)):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, transparent, var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), transparent ) );--_shiny-pulse-height: var(--shiny-pulse-height, 3px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.2s);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-direction:alternate;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:-14%;right:97%}45%{left:0%;right:14%}55%{left:14%;right:0%}to{left:97%;right:-14%}}.shiny-spinner-output-container{--shiny-spinner-size: 0px} +:where([data-shiny-busy-spinners] .recalculating){position:relative}[data-shiny-busy-spinners] .recalculating{min-height:var(--shiny-spinner-size, 32px)}[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.html-widget-output{visibility:inherit!important}[data-shiny-busy-spinners] .recalculating.html-widget-output>*{visibility:hidden}[data-shiny-busy-spinners] .recalculating.html-widget-output :after{visibility:visible}[data-shiny-busy-spinners] .recalculating.shiny-html-output:not(.shiny-table-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, transparent, var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), transparent ) );--_shiny-pulse-height: var(--shiny-pulse-height, 3px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.2s);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-direction:alternate;animation-iteration-count:infinite;animation-timing-function:ease-in-out;content:""}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(.recalculating:not(.shiny-html-output)):after{display:none}[data-shiny-busy-spinners][data-shiny-busy-pulse].shiny-busy:has(.recalculating.shiny-table-output):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, transparent, var(--bs-indigo, #4b00c1), var(--bs-purple, #74149c), var(--bs-pink, #bf007f), transparent ) );--_shiny-pulse-height: var(--shiny-pulse-height, 3px);--_shiny-pulse-speed: var(--shiny-pulse-speed, 1.2s);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-direction:alternate;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:-14%;right:97%}45%{left:0%;right:14%}55%{left:14%;right:0%}to{left:97%;right:-14%}}.shiny-spinner-output-container{--shiny-spinner-size: 0px} diff --git a/srcts/extras/busy-indicators/busy-indicators.scss b/srcts/extras/busy-indicators/busy-indicators.scss index 98f556c1f5..78a5040e46 100644 --- a/srcts/extras/busy-indicators/busy-indicators.scss +++ b/srcts/extras/busy-indicators/busy-indicators.scss @@ -7,6 +7,8 @@ .recalculating { + min-height: var(--shiny-spinner-size, 32px); + &::after { position: absolute; content: ""; @@ -45,13 +47,31 @@ transition: opacity 250ms ease var(--shiny-spinner-delay, 1s); } + /* + When htmlwidget errors are rendered, an inline `visibility:hidden` is put + on the html-widget-output, and the error message (if any) is put in a + sibling element that overlays the output container (this way, the height + of the output container doesn't change). Work around this by making the + output container itself visible and making the children (except the + spinner) invisible. + */ + &.html-widget-output { + visibility: inherit !important; + > * { + visibility: hidden; + } + ::after { + visibility: visible; + } + } + /* Disable spinner on uiOutput() mainly because (for other reasons) it has `display:contents`, which breaks the ::after positioning. Note that, even if we could position it, we'd probably want to disable it if it has recalculating children. */ - &.shiny-html-output::after { + &.shiny-html-output:not(.shiny-table-output)::after { display: none; } } @@ -105,6 +125,9 @@ &.shiny-busy:has(.recalculating:not(.shiny-html-output))::after { display: none; } + &.shiny-busy:has(.recalculating.shiny-table-output)::after { + display: none; + } &.shiny-busy:has(#shiny-disconnected-overlay)::after { display: none; }