diff --git a/DESCRIPTION b/DESCRIPTION
index 3763bd0e2..122fa6770 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,
@@ -99,7 +99,7 @@ Suggests:
datasets,
DT,
Cairo (>= 1.5-5),
- testthat (>= 3.0.0),
+ testthat (>= 3.2.1),
knitr (>= 1.6),
markdown,
rmarkdown,
@@ -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 687af2abb..de29901ff 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,5 +1,14 @@
# shiny (development version)
+## 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
* 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/bootstrap.R b/R/bootstrap.R
index 970b43400..23aeb05be 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/R/conditions.R b/R/conditions.R
index 164792a8d..241f6403a 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 98b5ce732..bad1bf108 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/inst/www/shared/busy-indicators/busy-indicators.css b/inst/www/shared/busy-indicators/busy-indicators.css
index 4de6c82e0..5ab9fcdd5 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 98f556c1f..78a5040e4 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;
}
diff --git a/tests/testthat/test-bootstrap.r b/tests/testthat/test-bootstrap.r
index f39bb726c..c2e9b51db 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 4fdd90aee..1f36d92bf 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 b41a2a207..d30c686f6 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("