Compare commits

...

37 Commits

Author SHA1 Message Date
Joe Cheng
bf390a83dd Experiment with removing debounce from slider 2024-03-13 16:51:37 -07:00
Garrick Aden-Buie
e2b7f91138 feat: Add shiny.error.unhandled error handler (#3989)
* feat(shiny.error.unhandled): Allow users to provide an unhandled error handler

* Extract `shinyUserErrorUnhandled()` to use in MockSession too

* tests(shiny.error.unhandled): Test that unhandled errors are handled safely

* docs: Clarify that session still ends with an unhandled error

* docs: Add news item
2024-03-08 13:36:36 -06:00
Garrick Aden-Buie
c73978cdd5 docs: update roxygen2 (#3988)
* fix: `@docType "package"` is deprecated

* fix: S3 methods need `@export` or `@exportS3method` tag.

* chore: devtools::document()
2024-03-08 09:15:37 -06:00
Andreas Deininger
6760c31818 Documentation: fixing typos (#3932)
* Documentation: fixing typos

* Commit changes after generation of package docs
2024-02-02 16:53:48 -06:00
olivroy
781ceaaa5c Remove ellipsis dependency (#3959)
* Remove ellipsis dependency

* Use the above `@importfrom rlang`

* move usethis namespace
2024-02-02 16:52:39 -06:00
Fenno Vermeij
fff283648b fix(downloadButton): Return tag directly (#2672)
* Fix `downloadButton()` not rendering in rmarkdown documents

---------

Co-authored-by: Garrick Aden-Buie <garrick@adenbuie.com>
2024-01-30 17:31:40 -05:00
Winston Chang
f71f1256b8 Fix duplicate ID check logic (#3978)
* Fix duplicate ID logic

* Build shiny.js
2024-01-24 14:42:20 -06:00
Nick Strayer
f26b1335d8 Dev mode aware client and duplicate input/output ID handling updates (#3956)
* Add field with devmode status to the initial config object sent over on connection

* Add indicator of "devmode" status to the client via an injected script tag on load. This is modeled after what is done for showcase mode.

* Add logic to flag all duplicated IDs when in devmode

* Only show error console in devmode.

* Remove left-over devmode status in code

* `yarn build` (GitHub Actions)

* Build shiny.js

* `devtools::document()` (GitHub Actions)

* `yarn build` (GitHub Actions)

---------

Co-authored-by: nstrayer <nstrayer@users.noreply.github.com>
Co-authored-by: Winston Chang <winston@posit.co>
Co-authored-by: wch <wch@users.noreply.github.com>
2024-01-24 12:37:27 -06:00
Nick Strayer
370ba1f288 Make error console sizing constant across base and bslib apps (#3947)
* Add more encapsulated sizes using css variables instead of `rem` units so console is consistantly sized across apps that set the body font size.

* `yarn build` (GitHub Actions)

* `yarn build` (GitHub Actions)

* `devtools::document()` (GitHub Actions)

* `yarn build` (GitHub Actions)

---------

Co-authored-by: nstrayer <nstrayer@users.noreply.github.com>
Co-authored-by: jcheng5 <jcheng5@users.noreply.github.com>
Co-authored-by: Winston Chang <winston@posit.co>
Co-authored-by: wch <wch@users.noreply.github.com>
2024-01-22 14:17:10 -06:00
Joe Cheng
54988c17c8 Merge pull request #3958 from rstudio/extended-task
Add ExtendedTask R6 class
2024-01-11 09:11:58 -08:00
Joe Cheng
65fe23fa02 Don't take reactive dependency on rv_status() during invoke 2024-01-11 08:59:46 -08:00
Joe Cheng
b22b06e3d2 Add NEWS 2024-01-10 19:00:11 -08:00
Joe Cheng
3677f4e1c6 Add unit tests for maskReactiveContext 2024-01-10 19:00:11 -08:00
Joe Cheng
d6eb0493b3 Code review feedback 2024-01-10 19:00:11 -08:00
Joe Cheng
4e13cdb365 Realized we no longer care about the bslib version
Now that bind_task_to_button is gone to bslib, there's no specific
code in Shiny that cares about task buttons
2024-01-10 19:00:11 -08:00
Joe Cheng
4e3710cdaa Use correct remote 2024-01-10 19:00:11 -08:00
Joe Cheng
5feedaf4c8 Add Remotes for bslib 2024-01-10 19:00:11 -08:00
Joe Cheng
ce29695e44 Remove bind_button_to_task (moved to bslib::bind_task_button) 2024-01-10 19:00:11 -08:00
Joe Cheng
f0f06a2c34 Update bind_button_to_task to use newest task button API 2024-01-10 19:00:11 -08:00
Joe Cheng
860a3fef86 Rebuild 2024-01-10 19:00:11 -08:00
Joe Cheng
6afadade5d Add bind_button_to_task feature 2024-01-10 19:00:11 -08:00
jcheng5
c1bda7fb7b yarn build (GitHub Actions) 2024-01-10 19:00:11 -08:00
jcheng5
509c165ee8 devtools::document() (GitHub Actions) 2024-01-10 19:00:11 -08:00
Joe Cheng
54e0ef7598 Add ExtendedTask to pkgdown.yml 2024-01-10 19:00:11 -08:00
Joe Cheng
03f2d5f014 Add ExtendedTask R6 class 2024-01-10 19:00:11 -08:00
Garrick Aden-Buie
122c1e74cd refactor: pass-through containers in BS5 only (#3960) 2023-12-20 19:15:09 -05:00
Garrick Aden-Buie
d29f4cdf21 fix(ui-containers): Use display: contents (#3957) 2023-12-19 22:27:50 -05:00
Joe Cheng
300fb217d1 Merge pull request #3954 from rstudio/persistent-progress
Allow outputs to stay in progress mode after flush
2023-12-12 12:23:25 -08:00
Joe Cheng
33dc41c4bd Add disabled argument to actionButton and updateActionButton 2023-12-11 17:04:09 -08:00
Joe Cheng
4b6e257dfc Don't add progressKeys more than once
Doesn't matter much but this is closer to the old behavior
2023-12-07 09:22:01 -08:00
Joe Cheng
1f23f37f89 Allow outputs to stay in progress mode after flush
Adds a req(FALSE, cancelOutput="progress") which behaves similarly to
cancelOutput=TRUE, but also keeps the output in .recalculating state
even across flush cycles. This is called "persistent progress" and an
output can leave this state when it is invalidated again and doesn't
call req(FALSE, cancelOutput="progress") during that flush cycle.

This will be useful for implementing long-running tasks that don't
hold up the flush cycle, leaving sessions responsive to do other
tasks.
2023-12-06 09:30:58 -08:00
Garrick Aden-Buie
59b1c46485 fix: Allow bindInputs() to no-op when attempting to bind currently bound inputs (#3946)
* fix: Do not re-bind previously bound inputs

* refactor: Add binding to the registry after binding happens

* fix: Spelling of `bindingsRegistry`

* chore: yarn build

* `yarn build` (GitHub Actions)

* fix: spelling

* feat: isRegistered can check if bound to input or output

* fix: Do not throw for shared input/output IDs

`input$caption` and `output$caption` may not be the best idea for several reasons, but it was previously allowed

Fixes #3943

* fix: check element directly to know whether it a bound input

* chore: yarn build

* fix: test `.shiny-bound-input` instead of data prop

* refactor: Remove `bindingsRegistry.isRegistered()` method

* refactor: Use a map for duplicateIds again

* refactor: Add `BindingTypes` type and use `bindingType` everywhere

* refactor: More concise duplicateIds typing

Co-authored-by: Nick Strayer <nick.strayer@rstudio.com>

* refactor: count by forEach + incrementing

Co-authored-by: Nick Strayer <nick.strayer@rstudio.com>

* `yarn build` (GitHub Actions)

* thanks, vscode

* docs: rewrite checkValidity() jsdoc to capture current state of things

* chore: yarn build

* docs: slight rewording

---------

Co-authored-by: gadenbuie <gadenbuie@users.noreply.github.com>
Co-authored-by: Nick Strayer <nick.strayer@rstudio.com>
2023-11-30 15:46:28 -06:00
Garrick Aden-Buie
01705c1299 fix(shiny.scss): Constrain notification panel to max-width: 100% (#3949) 2023-11-30 16:11:22 -05:00
Carson Sievert
18955a2abf Update tabPanel() snapshot tests in anticipation of bslib release (#3936)
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2023-11-29 15:38:48 -06:00
Nick Strayer
dbbe7f9679 Client error console and duplicate input/output binding errors (#3931)
Co-authored-by: nstrayer <nstrayer@users.noreply.github.com>
Co-authored-by: Winston Chang <winston@stdout.org>
2023-11-27 12:34:13 -06:00
Carson Sievert
61a51a869f Run yarn build (#3942)
Co-authored-by: cpsievert <cpsievert@users.noreply.github.com>
2023-11-20 13:32:32 -06:00
Carson
298822fc44 Start new version 2023-11-20 12:57:08 -06:00
57 changed files with 10067 additions and 4067 deletions

View File

@@ -1,7 +1,7 @@
Package: shiny
Type: Package
Title: Web Application Framework for R
Version: 1.8.0
Version: 1.8.0.9000
Authors@R: c(
person("Winston", "Chang", role = c("aut", "cre"), email = "winston@posit.co", comment = c(ORCID = "0000-0002-1576-2126")),
person("Joe", "Cheng", role = "aut", email = "joe@posit.co"),
@@ -93,7 +93,6 @@ Imports:
glue (>= 1.3.2),
bslib (>= 0.3.0),
cachem,
ellipsis,
lifecycle (>= 0.2.0)
Suggests:
datasets,
@@ -132,6 +131,7 @@ Collate:
'deprecated.R'
'devmode.R'
'diagnose.R'
'extended-task.R'
'fileupload.R'
'graph.R'
'reactives.R'
@@ -203,7 +203,7 @@ Collate:
'version_selectize.R'
'version_strftime.R'
'viewer.R'
RoxygenNote: 7.2.3
RoxygenNote: 7.3.1
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RdMacros: lifecycle

View File

@@ -19,6 +19,7 @@ S3method("[[",shinyoutput)
S3method("[[<-",reactivevalues)
S3method("[[<-",shinyoutput)
S3method("names<-",reactivevalues)
S3method(as.list,Map)
S3method(as.list,reactivevalues)
S3method(as.shiny.appobj,character)
S3method(as.shiny.appobj,list)
@@ -43,6 +44,7 @@ S3method(bindEvent,reactiveExpr)
S3method(bindEvent,shiny.render.function)
S3method(format,reactiveExpr)
S3method(format,reactiveVal)
S3method(length,Map)
S3method(names,reactivevalues)
S3method(print,reactive)
S3method(print,reactivevalues)
@@ -53,6 +55,7 @@ S3method(str,reactivevalues)
export("conditionStackTrace<-")
export(..stacktraceoff..)
export(..stacktraceon..)
export(ExtendedTask)
export(HTML)
export(MockShinySession)
export(NS)
@@ -332,8 +335,6 @@ import(httpuv)
import(methods)
import(mime)
import(xtable)
importFrom(ellipsis,check_dots_empty)
importFrom(ellipsis,check_dots_unnamed)
importFrom(fastmap,fastmap)
importFrom(fastmap,is.key_missing)
importFrom(fastmap,key_missing)
@@ -391,6 +392,8 @@ importFrom(rlang,"fn_body<-")
importFrom(rlang,"fn_fmls<-")
importFrom(rlang,as_function)
importFrom(rlang,as_quosure)
importFrom(rlang,check_dots_empty)
importFrom(rlang,check_dots_unnamed)
importFrom(rlang,enexpr)
importFrom(rlang,enquo)
importFrom(rlang,enquo0)

22
NEWS.md
View File

@@ -1,3 +1,23 @@
# shiny (development version)
## Breaking changes
* Both `conditionalPanel()` and `uiOutput()` are now styled with `display: contents` by default in Shiny apps that use Bootstrap 5. This means that the elements they contain are positioned as if they were direct children of the parent container holding the `conditionalPanel()` or `uiOutput()`. This is probably what most users intend when they use these functions, but it may break apps that applied styles directly to the container elements created by these two functions. In that case, you may include CSS rules to set `display: block` for the `.shiny-panel-conditional` or `.shiny-html-output` classes. (#3957, #3960)
## New features and improvements
* Added an console that shows some errors in the browser. Also provide better error messages for duplicate input and output bindings. (#3931)
* Added a new `ExtendedTask` abstraction, for long-running asynchronous tasks that you don't want to block the rest of the app, or even the rest of the session. Designed to be used with new `bslib::input_task_button()` and `bslib::bind_task_button()` functions that help give user feedback and prevent extra button clicks. (#3958)
* Added a `shiny.error.unhandled` option that can be set to a function that will be called when an unhandled error occurs in a Shiny app. Note that this handler doesn't stop the error or prevent the session from closing, but it can be used to log the error or to clean up session-specific resources. (thanks @JohnCoene, #3989)
## Bug fixes
* Notifications are now constrained to the width of the viewport for window widths smaller the default notification panel size. (#3949)
* Fixed #2392: `downloadButton()` now visibly returns its HTML tag so that it renders correctly in R Markdown and Quarto output. (Thanks to @fennovj, #2672)
# shiny 1.8.0
## Breaking changes
@@ -13,7 +33,7 @@
* Default styles for `showNotification()` were tweaked slightly to improve accessibility, sizing, and padding. (#3913)
* Shiny inputs and `{htmlwidgets}` are no longer treated as draggable inside of `absolutePanel()`/`fixedPanel()` with `draggable = TRUE`. As a result, interactions like zooming and panning now work as expected with widgets like `{plotly}` and `{leaflet}` when they appear in a draggable panel. (#3752, #3933)
* Shiny inputs and `{htmlwidgets}` are no longer treated as draggable inside of `absolutePanel()`/`fixedPanel()` with `draggable = TRUE`. As a result, interactions like zooming and panning now work as expected with widgets like `{plotly}` and `{leaflet}` when they appear in a draggable panel. (#3752, #3933)
* For `InputBinding`s, the `.receiveMessage()` method can now be asynchronous or synchronous (previously it could only be synchronous). (#3930)

View File

@@ -453,7 +453,7 @@ utils::globalVariables(".GenericCallEnv", add = TRUE)
#' bindEvent(input$go)
#' # The cached, eventified reactive takes a reactive dependency on
#' # input$go, but doesn't use it for the cache key. It uses input$x and
#' # input$y for the cache key, but doesn't take a reactive depdency on
#' # input$y for the cache key, but doesn't take a reactive dependency on
#' # them, because the reactive dependency is superseded by addEvent().
#'
#' output$txt <- renderText(r())

View File

@@ -532,7 +532,12 @@ wellPanel <- function(...) {
#' }
#' @export
conditionalPanel <- function(condition, ..., ns = NS(NULL)) {
div(`data-display-if`=condition, `data-ns-prefix`=ns(""), ...)
div(
class = "shiny-panel-conditional",
`data-display-if` = condition,
`data-ns-prefix` = ns(""),
...
)
}
#' Create a help text element
@@ -1233,13 +1238,13 @@ downloadButton <- function(outputId,
class=NULL,
...,
icon = shiny::icon("download")) {
aTag <- tags$a(id=outputId,
class=paste('btn btn-default shiny-download-link', class),
href='',
target='_blank',
download=NA,
validateIcon(icon),
label, ...)
tags$a(id=outputId,
class=paste('btn btn-default shiny-download-link', class),
href='',
target='_blank',
download=NA,
validateIcon(icon),
label, ...)
}
#' @rdname downloadButton

206
R/extended-task.R Normal file
View File

@@ -0,0 +1,206 @@
#' Task or computation that proceeds in the background
#'
#' @description In normal Shiny reactive code, whenever an observer, calc, or
#' output is busy computing, it blocks the current session from receiving any
#' inputs or attempting to proceed with any other computation related to that
#' session.
#'
#' The `ExtendedTask` class allows you to have an expensive operation that is
#' started by a reactive effect, and whose (eventual) results can be accessed
#' by a regular observer, calc, or output; but during the course of the
#' operation, the current session is completely unblocked, allowing the user
#' to continue using the rest of the app while the operation proceeds in the
#' background.
#'
#' Note that each `ExtendedTask` object does not represent a _single
#' invocation_ of its long-running function. Rather, it's an object that is
#' used to invoke the function with different arguments, keeps track of
#' whether an invocation is in progress, and provides ways to get at the
#' current status or results of the operation. A single `ExtendedTask` object
#' does not permit overlapping invocations: if the `invoke()` method is called
#' before the previous `invoke()` is completed, the new invocation will not
#' begin until the previous invocation has completed.
#'
#' @section `ExtendedTask` versus asynchronous reactives:
#'
#' Shiny has long supported [using
#' \{promises\}](https://rstudio.github.io/promises/articles/promises_06_shiny.html)
#' to write asynchronous observers, calcs, or outputs. You may be wondering
#' what the differences are between those techniques and this class.
#'
#' Asynchronous observers, calcs, and outputs are not--and have never
#' been--designed to let a user start a long-running operation, while keeping
#' that very same (browser) session responsive to other interactions. Instead,
#' they unblock other sessions, so you can take a long-running operation that
#' would normally bring the entire R process to a halt and limit the blocking
#' to just the session that started the operation. (For more details, see the
#' section on ["The Flush
#' Cycle"](https://rstudio.github.io/promises/articles/promises_06_shiny.html#the-flush-cycle).)
#'
#' `ExtendedTask`, on the other hand, invokes an asynchronous function (that
#' is, a function that quickly returns a promise) and allows even that very
#' session to immediately unblock and carry on with other user interactions.
#'
#' @export
ExtendedTask <- R6Class("ExtendedTask", portable = TRUE,
public = list(
#' @description
#' Creates a new `ExtendedTask` object. `ExtendedTask` should generally be
#' created either at the top of a server function, or at the top of a module
#' server function.
#'
#' @param func The long-running operation to execute. This should be an
#' asynchronous function, meaning, it should use the
#' [\{promises\}](https://rstudio.github.io/promises/) package, most
#' likely in conjuction with the
#' [\{future\}](https://rstudio.github.io/promises/articles/promises_04_futures.html)
#' package. (In short, the return value of `func` should be a
#' [`Future`][future::future()] object, or a `promise`, or something else
#' that [promises::as.promise()] understands.)
#'
#' It's also important that this logic does not read from any
#' reactive inputs/sources, as inputs may change after the function is
#' invoked; instead, if the function needs to access reactive inputs, it
#' should take parameters and the caller of the `invoke()` method should
#' read reactive inputs and pass them as arguments.
initialize = function(func) {
private$func <- func
private$rv_status <- reactiveVal("initial")
private$rv_value <- reactiveVal(NULL)
private$rv_error <- reactiveVal(NULL)
private$invocation_queue <- fastmap::fastqueue()
},
#' @description
#' Starts executing the long-running operation. If this `ExtendedTask` is
#' already running (meaning, a previous call to `invoke()` is not yet
#' complete) then enqueues this invocation until after the current
#' invocation, and any already-enqueued invocation, completes.
#'
#' @param ... Parameters to use for this invocation of the underlying
#' function. If reactive inputs are needed by the underlying function,
#' they should be read by the caller of `invoke` and passed in as
#' arguments.
invoke = function(...) {
args <- rlang::dots_list(..., .ignore_empty = "none")
if (
isolate(private$rv_status()) == "running" ||
private$invocation_queue$size() > 0
) {
private$invocation_queue$add(args)
} else {
private$do_invoke(args)
}
invisible(NULL)
},
#' @description
#' This is a reactive read that invalidates the caller when the task's
#' status changes.
#'
#' Returns one of the following values:
#'
#' * `"initial"`: This `ExtendedTask` has not yet been invoked
#' * `"running"`: An invocation is currently running
#' * `"success"`: An invocation completed successfully, and a value can be
#' retrieved via the `result()` method
#' * `"error"`: An invocation completed with an error, which will be
#' re-thrown if you call the `result()` method
status = function() {
private$rv_status()
},
#' @description
#' Attempts to read the results of the most recent invocation. This is a
#' reactive read that invalidates as the task's status changes.
#'
#' The actual behavior differs greatly depending on the current status of
#' the task:
#'
#' * `"initial"`: Throws a silent error (like [`req(FALSE)`][req()]). If
#' this happens during output rendering, the output will be blanked out.
#' * `"running"`: Throws a special silent error that, if it happens during
#' output rendering, makes the output appear "in progress" until further
#' notice.
#' * `"success"`: Returns the return value of the most recent invocation.
#' * `"error"`: Throws whatever error was thrown by the most recent
#' invocation.
#'
#' This method is intended to be called fairly naively by any output or
#' reactive expression that cares about the output--you just have to be
#' aware that if the result isn't ready for whatever reason, processing will
#' stop in much the same way as `req(FALSE)` does, but when the result is
#' ready you'll get invalidated, and when you run again the result should be
#' there.
#'
#' Note that the `result()` method is generally not meant to be used with
#' [observeEvent()], [eventReactive()], [bindEvent()], or [isolate()] as the
#' invalidation will be ignored.
result = function() {
switch (private$rv_status(),
running = req(FALSE, cancelOutput="progress"),
success = if (private$rv_value()$visible) {
private$rv_value()$value
} else {
invisible(private$rv_value()$value)
},
error = stop(private$rv_error()),
# default case (initial, cancelled)
req(FALSE)
)
}
),
private = list(
func = NULL,
# reactive value with "initial"|"running"|"success"|"error"
rv_status = NULL,
rv_value = NULL,
rv_error = NULL,
invocation_queue = NULL,
do_invoke = function(args) {
private$rv_status("running")
private$rv_value(NULL)
private$rv_error(NULL)
p <- NULL
tryCatch({
maskReactiveContext({
# TODO: Bounce the do.call off of a promise_resolve(), so that the
# call to invoke() always returns immediately?
result <- do.call(private$func, args)
p <- promises::as.promise(result)
})
}, error = function(e) {
private$on_error(e)
})
promises::finally(
promises::then(p,
onFulfilled = function(value, .visible) {
private$on_success(list(value=value, visible=.visible))
},
onRejected = function(error) {
private$on_error(error)
}
),
onFinally = function() {
if (private$invocation_queue$size() > 0) {
private$do_invoke(private$invocation_queue$remove())
}
}
)
invisible(NULL)
},
on_error = function(err) {
private$rv_status("error")
private$rv_error(err)
},
on_success = function(value) {
private$rv_status("success")
private$rv_value(value)
}
)
)

View File

@@ -14,7 +14,7 @@ NULL
#' depending on the values in the query string / hash (e.g. instead of basing
#' the conditional on an input or a calculated reactive, you can base it on the
#' query string). However, note that, if you're changing the query string / hash
#' programatically from within the server code, you must use
#' programmatically from within the server code, you must use
#' `updateQueryString(_yourNewQueryString_, mode = "push")`. The default
#' `mode` for `updateQueryString` is `"replace"`, which doesn't
#' raise any events, so any observers or reactives that depend on it will

View File

@@ -7,6 +7,8 @@
#' @param label The contents of the button or link--usually a text label, but
#' you could also use any other HTML, like an image.
#' @param icon An optional [icon()] to appear on the button.
#' @param disabled If `TRUE`, the button will not be clickable. Use
#' [updateActionButton()] to dynamically enable/disable the button.
#' @param ... Named attributes to be applied to the button or link.
#'
#' @family input elements
@@ -49,7 +51,8 @@
#' * Event handlers (e.g., [observeEvent()], [eventReactive()]) won't execute on initial load.
#' * Input validation (e.g., [req()], [need()]) will fail on initial load.
#' @export
actionButton <- function(inputId, label, icon = NULL, width = NULL, ...) {
actionButton <- function(inputId, label, icon = NULL, width = NULL,
disabled = FALSE, ...) {
value <- restoreInput(id = inputId, default = NULL)
@@ -58,6 +61,7 @@ actionButton <- function(inputId, label, icon = NULL, width = NULL, ...) {
type="button",
class="btn btn-default action-button",
`data-val` = value,
disabled = if (isTRUE(disabled)) NA else NULL,
list(validateIcon(icon), label),
...
)

View File

@@ -48,9 +48,12 @@ Map <- R6Class(
)
)
#' @export
as.list.Map <- function(x, ...) {
x$values()
}
#' @export
length.Map <- function(x) {
x$size()
}

View File

@@ -563,6 +563,7 @@ MockShinySession <- R6Class(
#' @description Called by observers when a reactive expression errors.
#' @param e An error object.
unhandledError = function(e) {
shinyUserErrorUnhandled(e)
self$close()
},
#' @description Freeze a value until the flush cycle completes.

View File

@@ -219,10 +219,10 @@ getDummyContext <- function() {
wrapForContext <- function(func, ctx) {
force(func)
force(ctx)
force(ctx) # may be NULL (in the case of maskReactiveContext())
function(...) {
ctx$run(function() {
.getReactiveEnvironment()$runWith(ctx, function() {
captureStackTraces(
func(...)
)
@@ -234,12 +234,18 @@ reactivePromiseDomain <- function() {
promises::new_promise_domain(
wrapOnFulfilled = function(onFulfilled) {
force(onFulfilled)
ctx <- getCurrentContext()
# ctx will be NULL if we're in a maskReactiveContext()
ctx <- if (hasCurrentContext()) getCurrentContext() else NULL
wrapForContext(onFulfilled, ctx)
},
wrapOnRejected = function(onRejected) {
force(onRejected)
ctx <- getCurrentContext()
# ctx will be NULL if we're in a maskReactiveContext()
ctx <- if (hasCurrentContext()) getCurrentContext() else NULL
wrapForContext(onRejected, ctx)
}
)

View File

@@ -81,6 +81,12 @@ getShinyOption <- function(name, default = NULL) {
#' \item{shiny.error (defaults to `NULL`)}{This can be a function which is called when an error
#' occurs. For example, `options(shiny.error=recover)` will result a
#' the debugger prompt when an error occurs.}
#' \item{shiny.error.unhandled (defaults to `NULL`)}{A function that will be
#' called when an unhandled error that will stop the app session occurs. This
#' function should take the error condition object as its first argument.
#' Note that this function will not stop the error or prevent the session
#' from ending, but it will provide you with an opportunity to log the error
#' or clean up resources before the session is closed.}
#' \item{shiny.fullstacktrace (defaults to `FALSE`)}{Controls whether "pretty" (`FALSE`) or full
#' stack traces (`TRUE`) are dumped to the console when errors occur during Shiny app execution.
#' Pretty stack traces attempt to only show user-supplied code, but this pruning can't always
@@ -113,7 +119,7 @@ getShinyOption <- function(name, default = NULL) {
#' production.}
#' \item{shiny.sanitize.errors (defaults to `FALSE`)}{If `TRUE`, then normal errors (i.e.
#' errors not wrapped in `safeError`) won't show up in the app; a simple
#' generic error message is printed instead (the error and strack trace printed
#' generic error message is printed instead (the error and stack trace printed
#' to the console remain unchanged). If you want to sanitize errors in general, but you DO want a
#' particular error `e` to get displayed to the user, then set this option
#' to `TRUE` and use `stop(safeError(e))` for errors you want the

View File

@@ -1,7 +1,6 @@
# See also R/reexports.R
## usethis namespace: start
## usethis namespace: end
#' @importFrom lifecycle deprecated is_present
#' @importFrom grDevices dev.set dev.cur
#' @importFrom fastmap fastmap
@@ -18,13 +17,13 @@
#' is_false list2
#' missing_arg is_missing maybe_missing
#' quo_is_missing fn_fmls<- fn_body fn_body<-
#' @importFrom ellipsis
#' check_dots_empty check_dots_unnamed
#' @import htmltools
#' @import httpuv
#' @import xtable
#' @import R6
#' @import mime
## usethis namespace: end
NULL
# It's necessary to Depend on methods so Rscript doesn't fail. It's necessary

View File

@@ -16,8 +16,7 @@ NULL
#'
#' @name shiny-package
#' @aliases shiny
#' @docType package
NULL
"_PACKAGE"
createUniqueId <- function(bytes, prefix = "", suffix = "") {
withPrivateSeed({
@@ -215,7 +214,7 @@ workerId <- local({
#' Sends a custom message to the web page. `type` must be a
#' single-element character vector giving the type of message, while
#' `message` can be any jsonlite-encodable value. Custom messages
#' have no meaning to Shiny itself; they are used soley to convey information
#' have no meaning to Shiny itself; they are used solely to convey information
#' to custom JavaScript logic in the browser. You can do this by adding
#' JavaScript code to the browser that calls
#' \code{Shiny.addCustomMessageHandler(type, function(message){...})}
@@ -1045,6 +1044,8 @@ ShinySession <- R6Class(
return(private$inputReceivedCallbacks$register(callback))
},
unhandledError = function(e) {
"Call the user's unhandled error handler and then close the session."
shinyUserErrorUnhandled(e)
self$close()
},
close = function() {
@@ -1149,6 +1150,8 @@ ShinySession <- R6Class(
structure(list(), class = "try-error", condition = cond)
} else if (inherits(cond, "shiny.output.cancel")) {
structure(list(), class = "cancel-output")
} else if (inherits(cond, "shiny.output.progress")) {
structure(list(), class = "progress-output")
} else if (cnd_inherits(cond, "shiny.silent.error")) {
# The error condition might have been chained by
# foreign code, e.g. dplyr. Find the original error.
@@ -1177,6 +1180,33 @@ ShinySession <- R6Class(
# client knows that progress is over.
self$requestFlush()
if (inherits(value, "progress-output")) {
# This is the case where an output needs to compute for longer
# than this reactive flush. We put the output into progress mode
# (i.e. adding .recalculating) with a special flag that means
# the progress indication should not be cleared until this
# specific output receives a new value or error.
self$showProgress(name, persistent=TRUE)
# It's conceivable that this output already ran successfully
# within this reactive flush, in which case we could either show
# the new output while simultaneously making it .recalculating;
# or we squelch the new output and make whatever output is in
# the client .recalculating. I (jcheng) decided on the latter as
# it seems more in keeping with what we do with these kinds of
# intermediate output values/errors in general, i.e. ignore them
# and wait until we have a final answer. (Also kind of feels
# like a bug in the app code if you routinely have outputs that
# are executing successfully, only to be invalidated again
# within the same reactive flush--use priority to fix that.)
private$invalidatedOutputErrors$remove(name)
private$invalidatedOutputValues$remove(name)
# It's important that we return so that the existing output in
# the client remains untouched.
return()
}
private$sendMessage(recalculating = list(
name = name, status = 'recalculated'
))
@@ -1309,23 +1339,29 @@ ShinySession <- R6Class(
private$startCycle()
}
},
showProgress = function(id) {
showProgress = function(id, persistent=FALSE) {
'Send a message to the client that recalculation of the output identified
by \\code{id} is in progress. There is currently no mechanism for
explicitly turning off progress for an output component; instead, all
progress is implicitly turned off when flushOutput is next called.'
progress is implicitly turned off when flushOutput is next called.
You can use persistent=TRUE if the progress for this output component
should stay on beyond the flushOutput (or any subsequent flushOutputs); in
that case, progress is only turned off (and the persistent flag cleared)
when the output component receives a value or error, or, if
showProgress(id, persistent=FALSE) is called and a subsequent flushOutput
occurs.'
# If app is already closed, be sure not to show progress, otherwise we
# will get an error because of the closed websocket
if (self$closed)
return()
if (id %in% private$progressKeys)
return()
if (!id %in% private$progressKeys) {
private$progressKeys <- c(private$progressKeys, id)
}
private$progressKeys <- c(private$progressKeys, id)
self$sendProgress('binding', list(id = id))
self$sendProgress('binding', list(id = id, persistent = persistent))
},
sendProgress = function(type, message) {
private$sendMessage(

View File

@@ -69,6 +69,21 @@ renderPage <- function(ui, showcase=0, testMode=FALSE) {
)
}
if (in_devmode()) {
# If we're in dev mode, add a simple script to the head that injects a
# global variable for the client to use to detect dev mode.
shiny_deps[[length(shiny_deps) + 1]] <-
htmlDependency(
"shiny-devmode",
get_package_version("shiny"),
src = "www/shared",
package = "shiny",
head="<script>window.__SHINY_DEV_MODE__ = true;</script>",
all_files = FALSE
)
}
html <- renderDocument(ui, shiny_deps, processDep = createWebDependency)
enc2utf8(paste(collapse = "\n", html))
}
@@ -159,7 +174,7 @@ shinyDependencyCSS <- function(theme) {
#' This function is kept for backwards compatibility with older applications. It
#' returns the value that is passed to it.
#'
#' @param ui A user interace definition
#' @param ui A user interface definition
#' @return The user interface definition, without modifications or side effects.
#' @keywords internal
#' @export

View File

@@ -119,6 +119,8 @@ updateCheckboxInput <- function(session = getDefaultReactiveDomain(), inputId, l
#' Change the label or icon of an action button on the client
#'
#' @template update-input
#' @param disabled If `TRUE`, the button will not be clickable; if `FALSE`, it
#' will be.
#' @inheritParams actionButton
#'
#' @seealso [actionButton()]
@@ -148,13 +150,13 @@ updateCheckboxInput <- function(session = getDefaultReactiveDomain(), inputId, l
#' label = "New label",
#' icon = icon("calendar"))
#'
#' # Leaves goButton2's label unchaged and
#' # Leaves goButton2's label unchanged and
#' # removes its icon
#' updateActionButton(session, "goButton2",
#' icon = character(0))
#'
#' # Leaves goButton3's icon, if it exists,
#' # unchaged and changes its label
#' # unchanged and changes its label
#' updateActionButton(session, "goButton3",
#' label = "New label 3")
#'
@@ -169,16 +171,18 @@ updateCheckboxInput <- function(session = getDefaultReactiveDomain(), inputId, l
#' }
#' @rdname updateActionButton
#' @export
updateActionButton <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, icon = NULL) {
updateActionButton <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, icon = NULL, disabled = NULL) {
validate_session_object(session)
if (!is.null(icon)) icon <- as.character(validateIcon(icon))
message <- dropNulls(list(label=label, icon=icon))
message <- dropNulls(list(label=label, icon=icon, disabled=disabled))
session$sendInputMessage(inputId, message)
}
#' @rdname updateActionButton
#' @export
updateActionLink <- updateActionButton
updateActionLink <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, icon = NULL) {
updateActionButton(session, inputId=inputId, label=label, icon=icon)
}
#' Change the value of a date input on the client

View File

@@ -493,6 +493,30 @@ shinyCallingHandlers <- function(expr) {
)
}
shinyUserErrorUnhandled <- function(error, handler = NULL) {
if (is.null(handler)) {
handler <- getShinyOption(
"shiny.error.unhandled",
getOption("shiny.error.unhandled", NULL)
)
}
if (is.null(handler)) return()
if (!is.function(handler) || length(formals(handler)) == 0) {
warning(
"`shiny.error.unhandled` must be a function ",
"that takes an error object as its first argument",
immediate. = TRUE
)
return()
}
tryCatch(
shinyCallingHandlers(handler(error)),
error = printError
)
}
#' Register a function with the debugger (if one is active).
#'
@@ -1093,7 +1117,7 @@ need <- function(expr, message = paste(label, "must be provided"), label) {
#'
#' You can use `req(FALSE)` (i.e. no condition) if you've already performed
#' all the checks you needed to by that point and just want to stop the reactive
#' chain now. There is no advantange to this, except perhaps ease of readibility
#' chain now. There is no advantage to this, except perhaps ease of readability
#' if you have a complicated condition to check for (or perhaps if you'd like to
#' divide your condition into nested `if` statements).
#'
@@ -1115,7 +1139,10 @@ need <- function(expr, message = paste(label, "must be provided"), label) {
#' @param ... Values to check for truthiness.
#' @param cancelOutput If `TRUE` and an output is being evaluated, stop
#' processing as usual but instead of clearing the output, leave it in
#' whatever state it happens to be in.
#' whatever state it happens to be in. If `"progress"`, do the same as `TRUE`,
#' but also keep the output in recalculating state; this is intended for cases
#' when an in-progress calculation will not be completed in this reactive
#' flush cycle, but is still expected to provide a result in the future.
#' @return The first value that was passed in.
#' @export
#' @examples
@@ -1147,6 +1174,8 @@ req <- function(..., cancelOutput = FALSE) {
if (!isTruthy(item)) {
if (isTRUE(cancelOutput)) {
cancelOutput()
} else if (identical(cancelOutput, "progress")) {
reactiveStop(class = "shiny.output.progress")
} else {
reactiveStop(class = "validation")
}

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +1,2 @@
/*! shiny 1.8.0 | (c) 2012-2023 RStudio, PBC. | License: GPL-3 | file LICENSE */
/*! shiny 1.8.0.9000 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
#showcase-well{border-radius:0}.shiny-code{background-color:#fff;margin-bottom:0}.shiny-code code{font-family:Menlo,Consolas,Courier New,monospace}.shiny-code-container{margin-top:20px;clear:both}.shiny-code-container h3{display:inline;margin-right:15px}.showcase-header{font-size:16px;font-weight:400}.showcase-code-link{text-align:right;padding:15px}#showcase-app-container{vertical-align:top}#showcase-code-tabs{margin-right:15px}#showcase-code-tabs pre{border:none;line-height:1em}#showcase-code-tabs .nav,#showcase-code-tabs ul{margin-bottom:0}#showcase-code-tabs .tab-content{border-style:solid;border-color:#e5e5e5;border-width:0px 1px 1px 1px;overflow:auto;border-bottom-right-radius:4px;border-bottom-left-radius:4px}#showcase-app-code{width:100%}#showcase-code-position-toggle{float:right}#showcase-sxs-code{padding-top:20px;vertical-align:top}.showcase-code-license{display:block;text-align:right}#showcase-code-content pre{background-color:#fff}

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,3 @@
/*! shiny 1.8.0 | (c) 2012-2023 RStudio, PBC. | License: GPL-3 | file LICENSE */
/*! shiny 1.8.0.9000 | (c) 2012-2024 RStudio, PBC. | License: GPL-3 | file LICENSE */
"use strict";(function(){var a=eval;window.addEventListener("message",function(i){var e=i.data;e.code&&a(e.code)});})();
//# sourceMappingURL=shiny-testmode.js.map

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -37,3 +37,9 @@ $notification-content-action-color: RGB(var(--#{$prefix}primary-rgb, #{to-rgb($p
$datepicker-disabled-color: $dropdown-link-disabled-color !default;
$shiny-file-active-shadow: $input-focus-box-shadow !default;
/* Treat conditional panels and uiOutput as "pass-through" containers */
.shiny-panel-conditional,
div:where(.shiny-html-output) {
display: contents;
}

View File

@@ -391,6 +391,7 @@ html.autoreload-enabled #shiny-disconnected-overlay.reloading {
background-color: rgba(0,0,0,0);
padding: 2px;
width: 300px;
max-width: 100%;
z-index: 99999;
}

186
man/ExtendedTask.Rd Normal file
View File

@@ -0,0 +1,186 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/extended-task.R
\name{ExtendedTask}
\alias{ExtendedTask}
\title{Task or computation that proceeds in the background}
\description{
In normal Shiny reactive code, whenever an observer, calc, or
output is busy computing, it blocks the current session from receiving any
inputs or attempting to proceed with any other computation related to that
session.
The \code{ExtendedTask} class allows you to have an expensive operation that is
started by a reactive effect, and whose (eventual) results can be accessed
by a regular observer, calc, or output; but during the course of the
operation, the current session is completely unblocked, allowing the user
to continue using the rest of the app while the operation proceeds in the
background.
Note that each \code{ExtendedTask} object does not represent a \emph{single
invocation} of its long-running function. Rather, it's an object that is
used to invoke the function with different arguments, keeps track of
whether an invocation is in progress, and provides ways to get at the
current status or results of the operation. A single \code{ExtendedTask} object
does not permit overlapping invocations: if the \code{invoke()} method is called
before the previous \code{invoke()} is completed, the new invocation will not
begin until the previous invocation has completed.
}
\section{\code{ExtendedTask} versus asynchronous reactives}{
Shiny has long supported \href{https://rstudio.github.io/promises/articles/promises_06_shiny.html}{using \{promises\}}
to write asynchronous observers, calcs, or outputs. You may be wondering
what the differences are between those techniques and this class.
Asynchronous observers, calcs, and outputs are not--and have never
been--designed to let a user start a long-running operation, while keeping
that very same (browser) session responsive to other interactions. Instead,
they unblock other sessions, so you can take a long-running operation that
would normally bring the entire R process to a halt and limit the blocking
to just the session that started the operation. (For more details, see the
section on \href{https://rstudio.github.io/promises/articles/promises_06_shiny.html#the-flush-cycle}{"The Flush Cycle"}.)
\code{ExtendedTask}, on the other hand, invokes an asynchronous function (that
is, a function that quickly returns a promise) and allows even that very
session to immediately unblock and carry on with other user interactions.
}
\section{Methods}{
\subsection{Public methods}{
\itemize{
\item \href{#method-ExtendedTask-new}{\code{ExtendedTask$new()}}
\item \href{#method-ExtendedTask-invoke}{\code{ExtendedTask$invoke()}}
\item \href{#method-ExtendedTask-status}{\code{ExtendedTask$status()}}
\item \href{#method-ExtendedTask-result}{\code{ExtendedTask$result()}}
\item \href{#method-ExtendedTask-clone}{\code{ExtendedTask$clone()}}
}
}
\if{html}{\out{<hr>}}
\if{html}{\out{<a id="method-ExtendedTask-new"></a>}}
\if{latex}{\out{\hypertarget{method-ExtendedTask-new}{}}}
\subsection{Method \code{new()}}{
Creates a new \code{ExtendedTask} object. \code{ExtendedTask} should generally be
created either at the top of a server function, or at the top of a module
server function.
\subsection{Usage}{
\if{html}{\out{<div class="r">}}\preformatted{ExtendedTask$new(func)}\if{html}{\out{</div>}}
}
\subsection{Arguments}{
\if{html}{\out{<div class="arguments">}}
\describe{
\item{\code{func}}{The long-running operation to execute. This should be an
asynchronous function, meaning, it should use the
\href{https://rstudio.github.io/promises/}{\{promises\}} package, most
likely in conjuction with the
\href{https://rstudio.github.io/promises/articles/promises_04_futures.html}{\{future\}}
package. (In short, the return value of \code{func} should be a
\code{\link[future:future]{Future}} object, or a \code{promise}, or something else
that \code{\link[promises:is.promise]{promises::as.promise()}} understands.)
It's also important that this logic does not read from any
reactive inputs/sources, as inputs may change after the function is
invoked; instead, if the function needs to access reactive inputs, it
should take parameters and the caller of the \code{invoke()} method should
read reactive inputs and pass them as arguments.}
}
\if{html}{\out{</div>}}
}
}
\if{html}{\out{<hr>}}
\if{html}{\out{<a id="method-ExtendedTask-invoke"></a>}}
\if{latex}{\out{\hypertarget{method-ExtendedTask-invoke}{}}}
\subsection{Method \code{invoke()}}{
Starts executing the long-running operation. If this \code{ExtendedTask} is
already running (meaning, a previous call to \code{invoke()} is not yet
complete) then enqueues this invocation until after the current
invocation, and any already-enqueued invocation, completes.
\subsection{Usage}{
\if{html}{\out{<div class="r">}}\preformatted{ExtendedTask$invoke(...)}\if{html}{\out{</div>}}
}
\subsection{Arguments}{
\if{html}{\out{<div class="arguments">}}
\describe{
\item{\code{...}}{Parameters to use for this invocation of the underlying
function. If reactive inputs are needed by the underlying function,
they should be read by the caller of \code{invoke} and passed in as
arguments.}
}
\if{html}{\out{</div>}}
}
}
\if{html}{\out{<hr>}}
\if{html}{\out{<a id="method-ExtendedTask-status"></a>}}
\if{latex}{\out{\hypertarget{method-ExtendedTask-status}{}}}
\subsection{Method \code{status()}}{
This is a reactive read that invalidates the caller when the task's
status changes.
Returns one of the following values:
\itemize{
\item \code{"initial"}: This \code{ExtendedTask} has not yet been invoked
\item \code{"running"}: An invocation is currently running
\item \code{"success"}: An invocation completed successfully, and a value can be
retrieved via the \code{result()} method
\item \code{"error"}: An invocation completed with an error, which will be
re-thrown if you call the \code{result()} method
}
\subsection{Usage}{
\if{html}{\out{<div class="r">}}\preformatted{ExtendedTask$status()}\if{html}{\out{</div>}}
}
}
\if{html}{\out{<hr>}}
\if{html}{\out{<a id="method-ExtendedTask-result"></a>}}
\if{latex}{\out{\hypertarget{method-ExtendedTask-result}{}}}
\subsection{Method \code{result()}}{
Attempts to read the results of the most recent invocation. This is a
reactive read that invalidates as the task's status changes.
The actual behavior differs greatly depending on the current status of
the task:
\itemize{
\item \code{"initial"}: Throws a silent error (like \code{\link[=req]{req(FALSE)}}). If
this happens during output rendering, the output will be blanked out.
\item \code{"running"}: Throws a special silent error that, if it happens during
output rendering, makes the output appear "in progress" until further
notice.
\item \code{"success"}: Returns the return value of the most recent invocation.
\item \code{"error"}: Throws whatever error was thrown by the most recent
invocation.
}
This method is intended to be called fairly naively by any output or
reactive expression that cares about the output--you just have to be
aware that if the result isn't ready for whatever reason, processing will
stop in much the same way as \code{req(FALSE)} does, but when the result is
ready you'll get invalidated, and when you run again the result should be
there.
Note that the \code{result()} method is generally not meant to be used with
\code{\link[=observeEvent]{observeEvent()}}, \code{\link[=eventReactive]{eventReactive()}}, \code{\link[=bindEvent]{bindEvent()}}, or \code{\link[=isolate]{isolate()}} as the
invalidation will be ignored.
\subsection{Usage}{
\if{html}{\out{<div class="r">}}\preformatted{ExtendedTask$result()}\if{html}{\out{</div>}}
}
}
\if{html}{\out{<hr>}}
\if{html}{\out{<a id="method-ExtendedTask-clone"></a>}}
\if{latex}{\out{\hypertarget{method-ExtendedTask-clone}{}}}
\subsection{Method \code{clone()}}{
The objects of this class are cloneable with this method.
\subsection{Usage}{
\if{html}{\out{<div class="r">}}\preformatted{ExtendedTask$clone(deep = FALSE)}\if{html}{\out{</div>}}
}
\subsection{Arguments}{
\if{html}{\out{<div class="arguments">}}
\describe{
\item{\code{deep}}{Whether to make a deep clone.}
}
\if{html}{\out{</div>}}
}
}
}

View File

@@ -5,7 +5,7 @@
\alias{actionLink}
\title{Action button/link}
\usage{
actionButton(inputId, label, icon = NULL, width = NULL, ...)
actionButton(inputId, label, icon = NULL, width = NULL, disabled = FALSE, ...)
actionLink(inputId, label, icon = NULL, ...)
}
@@ -20,6 +20,9 @@ you could also use any other HTML, like an image.}
\item{width}{The width of the input, e.g. \code{'400px'}, or \code{'100\%'};
see \code{\link[=validateCssUnit]{validateCssUnit()}}.}
\item{disabled}{If \code{TRUE}, the button will not be clickable. Use
\code{\link[=updateActionButton]{updateActionButton()}} to dynamically enable/disable the button.}
\item{...}{Named attributes to be applied to the button or link.}
}
\description{

View File

@@ -448,7 +448,7 @@ shinyApp(
bindEvent(input$go)
# The cached, eventified reactive takes a reactive dependency on
# input$go, but doesn't use it for the cache key. It uses input$x and
# input$y for the cache key, but doesn't take a reactive depdency on
# input$y for the cache key, but doesn't take a reactive dependency on
# them, because the reactive dependency is superseded by addEvent().
output$txt <- renderText(r())

View File

@@ -26,7 +26,7 @@ These can be particularly useful if you want to display different content
depending on the values in the query string / hash (e.g. instead of basing
the conditional on an input or a calculated reactive, you can base it on the
query string). However, note that, if you're changing the query string / hash
programatically from within the server code, you must use
programmatically from within the server code, you must use
\verb{updateQueryString(_yourNewQueryString_, mode = "push")}. The default
\code{mode} for \code{updateQueryString} is \code{"replace"}, which doesn't
raise any events, so any observers or reactives that depend on it will

View File

@@ -11,7 +11,10 @@ req(..., cancelOutput = FALSE)
\item{cancelOutput}{If \code{TRUE} and an output is being evaluated, stop
processing as usual but instead of clearing the output, leave it in
whatever state it happens to be in.}
whatever state it happens to be in. If \code{"progress"}, do the same as \code{TRUE},
but also keep the output in recalculating state; this is intended for cases
when an in-progress calculation will not be completed in this reactive
flush cycle, but is still expected to provide a result in the future.}
}
\value{
The first value that was passed in.
@@ -59,7 +62,7 @@ way to check for a value "inline" with its first use.
You can use \code{req(FALSE)} (i.e. no condition) if you've already performed
all the checks you needed to by that point and just want to stop the reactive
chain now. There is no advantange to this, except perhaps ease of readibility
chain now. There is no advantage to this, except perhaps ease of readability
if you have a complicated condition to check for (or perhaps if you'd like to
divide your condition into nested \code{if} statements).
}

View File

@@ -121,7 +121,7 @@ any \code{imageOutput} or \code{plotOutput} in the app.
Sends a custom message to the web page. \code{type} must be a
single-element character vector giving the type of message, while
\code{message} can be any jsonlite-encodable value. Custom messages
have no meaning to Shiny itself; they are used soley to convey information
have no meaning to Shiny itself; they are used solely to convey information
to custom JavaScript logic in the browser. You can do this by adding
JavaScript code to the browser that calls
\code{Shiny.addCustomMessageHandler(type, function(message){...})}

View File

@@ -19,3 +19,49 @@ includes extensive annotated examples.
\seealso{
\link{shiny-options} for documentation about global options.
}
\author{
\strong{Maintainer}: Winston Chang \email{winston@posit.co} (\href{https://orcid.org/0000-0002-1576-2126}{ORCID})
Authors:
\itemize{
\item Joe Cheng \email{joe@posit.co}
\item JJ Allaire \email{jj@posit.co}
\item Carson Sievert \email{carson@posit.co} (\href{https://orcid.org/0000-0002-4958-2844}{ORCID})
\item Barret Schloerke \email{barret@posit.co} (\href{https://orcid.org/0000-0001-9986-114X}{ORCID})
\item Yihui Xie \email{yihui@posit.co}
\item Jeff Allen
\item Jonathan McPherson \email{jonathan@posit.co}
\item Alan Dipert
\item Barbara Borges
}
Other contributors:
\itemize{
\item Posit Software, PBC [copyright holder, funder]
\item jQuery Foundation (jQuery library and jQuery UI library) [copyright holder]
\item jQuery contributors (jQuery library; authors listed in inst/www/shared/jquery-AUTHORS.txt) [contributor, copyright holder]
\item jQuery UI contributors (jQuery UI library; authors listed in inst/www/shared/jqueryui/AUTHORS.txt) [contributor, copyright holder]
\item Mark Otto (Bootstrap library) [contributor]
\item Jacob Thornton (Bootstrap library) [contributor]
\item Bootstrap contributors (Bootstrap library) [contributor]
\item Twitter, Inc (Bootstrap library) [copyright holder]
\item Prem Nawaz Khan (Bootstrap accessibility plugin) [contributor]
\item Victor Tsaran (Bootstrap accessibility plugin) [contributor]
\item Dennis Lembree (Bootstrap accessibility plugin) [contributor]
\item Srinivasu Chakravarthula (Bootstrap accessibility plugin) [contributor]
\item Cathy O'Connor (Bootstrap accessibility plugin) [contributor]
\item PayPal, Inc (Bootstrap accessibility plugin) [copyright holder]
\item Stefan Petre (Bootstrap-datepicker library) [contributor, copyright holder]
\item Andrew Rowls (Bootstrap-datepicker library) [contributor, copyright holder]
\item Brian Reavis (selectize.js library) [contributor, copyright holder]
\item Salmen Bejaoui (selectize-plugin-a11y library) [contributor, copyright holder]
\item Denis Ineshin (ion.rangeSlider library) [contributor, copyright holder]
\item Sami Samhuri (Javascript strftime library) [contributor, copyright holder]
\item SpryMedia Limited (DataTables library) [contributor, copyright holder]
\item John Fraser (showdown.js library) [contributor, copyright holder]
\item John Gruber (showdown.js library) [contributor, copyright holder]
\item Ivan Sagalaev (highlight.js library) [contributor, copyright holder]
\item R Core Team (tar implementation from R) [contributor, copyright holder]
}
}

View File

@@ -60,6 +60,12 @@ deprecated functions in Shiny will be printed. See
\item{shiny.error (defaults to \code{NULL})}{This can be a function which is called when an error
occurs. For example, \code{options(shiny.error=recover)} will result a
the debugger prompt when an error occurs.}
\item{shiny.error.unhandled (defaults to \code{NULL})}{A function that will be
called when an unhandled error that will stop the app session occurs. This
function should take the error condition object as its first argument.
Note that this function will not stop the error or prevent the session
from ending, but it will provide you with an opportunity to log the error
or clean up resources before the session is closed.}
\item{shiny.fullstacktrace (defaults to \code{FALSE})}{Controls whether "pretty" (\code{FALSE}) or full
stack traces (\code{TRUE}) are dumped to the console when errors occur during Shiny app execution.
Pretty stack traces attempt to only show user-supplied code, but this pruning can't always
@@ -92,7 +98,7 @@ This incurs a substantial performance penalty and should not be used in
production.}
\item{shiny.sanitize.errors (defaults to \code{FALSE})}{If \code{TRUE}, then normal errors (i.e.
errors not wrapped in \code{safeError}) won't show up in the app; a simple
generic error message is printed instead (the error and strack trace printed
generic error message is printed instead (the error and stack trace printed
to the console remain unchanged). If you want to sanitize errors in general, but you DO want a
particular error \code{e} to get displayed to the user, then set this option
to \code{TRUE} and use \code{stop(safeError(e))} for errors you want the

View File

@@ -7,7 +7,7 @@
shinyUI(ui)
}
\arguments{
\item{ui}{A user interace definition}
\item{ui}{A user interface definition}
}
\value{
The user interface definition, without modifications or side effects.

View File

@@ -9,7 +9,8 @@ updateActionButton(
session = getDefaultReactiveDomain(),
inputId,
label = NULL,
icon = NULL
icon = NULL,
disabled = NULL
)
updateActionLink(
@@ -28,6 +29,9 @@ updateActionLink(
\item{label}{The label to set for the input object.}
\item{icon}{An optional \code{\link[=icon]{icon()}} to appear on the button.}
\item{disabled}{If \code{TRUE}, the button will not be clickable; if \code{FALSE}, it
will be.}
}
\description{
Change the label or icon of an action button on the client
@@ -74,13 +78,13 @@ server <- function(input, output, session) {
label = "New label",
icon = icon("calendar"))
# Leaves goButton2's label unchaged and
# Leaves goButton2's label unchanged and
# removes its icon
updateActionButton(session, "goButton2",
icon = character(0))
# Leaves goButton3's icon, if it exists,
# unchaged and changes its label
# unchanged and changes its label
updateActionButton(session, "goButton3",
label = "New label 3")

View File

@@ -3,7 +3,7 @@
"homepage": "https://shiny.rstudio.com",
"repository": "github:rstudio/shiny",
"name": "@types/rstudio-shiny",
"version": "1.8.0",
"version": "1.8.0-alpha.9000",
"license": "GPL-3.0-only",
"main": "",
"browser": "",
@@ -24,7 +24,8 @@
"@types/datatables.net": "^1.10.19",
"@types/ion-rangeslider": "2.3.0",
"@types/jquery": "3.5.14",
"@types/selectize": "0.12.34"
"@types/selectize": "0.12.34",
"lit": "^3.0.0"
},
"devDependencies": {
"@babel/core": "^7.14.3",

View File

@@ -2,7 +2,11 @@ import $ from "jquery";
import { hasDefinedProperty } from "../../utils";
import { InputBinding } from "./inputBinding";
type ActionButtonReceiveMessageData = { label?: string; icon?: string | [] };
type ActionButtonReceiveMessageData = {
label?: string;
icon?: string | [];
disabled?: boolean;
};
class ActionButtonInputBinding extends InputBinding {
find(scope: HTMLElement): JQuery<HTMLElement> {
@@ -38,34 +42,44 @@ class ActionButtonInputBinding extends InputBinding {
receiveMessage(el: HTMLElement, data: ActionButtonReceiveMessageData): void {
const $el = $(el);
// retrieve current label and icon
let label: string = $el.text();
let icon = "";
if (hasDefinedProperty(data, "label") || hasDefinedProperty(data, "icon")) {
// retrieve current label and icon
let label: string = $el.text();
let icon = "";
// to check (and store) the previous icon, we look for a $el child
// object that has an i tag, and some (any) class (this prevents
// italicized text - which has an i tag but, usually, no class -
// from being mistakenly selected)
if ($el.find("i[class]").length > 0) {
const iconHtml = $el.find("i[class]")[0];
// to check (and store) the previous icon, we look for a $el child
// object that has an i tag, and some (any) class (this prevents
// italicized text - which has an i tag but, usually, no class -
// from being mistakenly selected)
if ($el.find("i[class]").length > 0) {
const iconHtml = $el.find("i[class]")[0];
if (iconHtml === $el.children()[0]) {
// another check for robustness
icon = $(iconHtml).prop("outerHTML");
if (iconHtml === $el.children()[0]) {
// another check for robustness
icon = $(iconHtml).prop("outerHTML");
}
}
// update the requested properties
if (hasDefinedProperty(data, "label")) {
label = data.label;
}
if (hasDefinedProperty(data, "icon")) {
// `data.icon` can be an [] if user gave `character(0)`.
icon = Array.isArray(data.icon) ? "" : data.icon ?? "";
}
// produce new html
$el.html(icon + " " + label);
}
if (hasDefinedProperty(data, "disabled")) {
if (data.disabled) {
$el.attr("disabled", "");
} else {
$el.attr("disabled", null);
}
}
// update the requested properties
if (hasDefinedProperty(data, "label")) {
label = data.label;
}
if (hasDefinedProperty(data, "icon")) {
// `data.icon` can be an [] if user gave `character(0)`.
icon = Array.isArray(data.icon) ? "" : data.icon ?? "";
}
// produce new html
$el.html(icon + " " + label);
}
unsubscribe(el: HTMLElement): void {

View File

@@ -259,11 +259,7 @@ class SliderInputBinding extends TextInputBindingBase {
}
}
getRatePolicy(el: HTMLElement): { policy: "debounce"; delay: 250 } {
return {
policy: "debounce",
delay: 250,
};
el;
return null as any;
}
// TODO-barret Why not implemented?
getState(el: HTMLInputElement): void {

View File

@@ -0,0 +1,531 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { LitElement, html, css } from "lit";
import { ShinyClientError } from "../shiny/error";
const buttonStyles = css`
button {
background-color: transparent;
outline: none;
border-style: none;
padding: var(--space-3);
border-radius: var(--space-1);
font-size: var(--font-lg);
background-color: inherit;
display: block;
}
button > svg {
display: block;
}
`;
class ShinyErrorConsole extends LitElement {
static styles = [
css`
:host {
/* We declare hard pixel values here to avoid body font size changes
messing up the size of the console. This was an issue with bslib setting
the body font-size at 16px relative to base shiny's 14px. */
--font-md: 14px;
--font-lg: 16px;
--font-xl: 18px;
/* These are all taken from open-props */
--space-1: 6px;
--space-2: calc(var(--space-1) * 2);
--space-3: calc(var(--space-1) * 3);
--space-4: calc(var(--space-1) * 4);
--space-8: calc(var(--space-1) * 8);
--red-2: #ffc9c9;
--red-6: #fa5252;
--red-7: #f03e3e;
--red-8: #e03131;
--red-10: #b02525;
--red-11: #962020;
--red-12: #7d1a1a;
--gray-1: #f8f9fa;
--gray-2: #e9ecef;
--gray-3: #dee2e6;
--gray-4: #ced4da;
--gray-6: #868e96;
--gray-8: #6c757d;
--green-8: #51cf66;
--shadow-color: 220 3% 15%;
--shadow-strength: 1%;
--shadow-3: 0 -1px 3px 0 hsl(var(--shadow-color) /
calc(var(--shadow-strength) + 2%)),
0 1px 2px -5px hsl(var(--shadow-color) /
calc(var(--shadow-strength) + 2%)),
0 2px 5px -5px hsl(var(--shadow-color) /
calc(var(--shadow-strength) + 4%)),
0 4px 12px -5px hsl(var(--shadow-color) /
calc(var(--shadow-strength) + 5%)),
0 12px 15px -5px hsl(var(--shadow-color) /
calc(var(--shadow-strength) + 7%));
--ring-shadow: 0 0 0 1px var(--gray-2);
/* How fast should the message pop in and out of the screen? */
--animation-speed: 500ms;
/* Taken from open-props */
--ease-3: cubic-bezier(0.25, 0, 0.3, 1);
--animation-slide-in-left: slide-in-left var(--animation-speed)
var(--ease-3);
--animation-slide-out-left: slide-out-left var(--animation-speed)
var(--ease-3);
--modal-bg-color: white;
position: fixed;
top: var(--space-1);
right: var(--space-1);
z-index: 1000;
display: flex;
flex-direction: column;
background-color: var(--modal-bg-color);
border-radius: var(--space-1);
animation: var(--animation-slide-in-left);
box-shadow: var(--shadow-3), var(--ring-shadow);
/* Dont let the error console burst out of the viewport */
max-height: calc(100vh - 2 * var(--space-1));
}
@keyframes slide-in-left {
from {
transform: translateX(100%);
}
}
@keyframes slide-out-left {
to {
transform: translateX(100%);
}
}
:host(.leaving) {
animation: var(--animation-slide-out-left);
}
.header {
display: flex;
justify-content: flex-end;
align-items: flex-start;
gap: var(--space-2);
}
.title {
font-size: var(--font-xl);
margin-right: auto;
padding: var(--space-3);
line-height: 1;
font-weight: 600;
color: var(--red-12);
}
${buttonStyles}
button:hover {
background-color: var(--gray-2);
}
.toggle-button {
width: fit-content;
border: none;
aspect-ratio: 1;
border-color: var(--gray-4);
}
.close-button {
display: flex;
align-items: center;
color: var(--red-11);
}
.close-button > svg {
margin-right: 3px;
}
.toggle-button:focus {
outline: 1px solid black;
}
.toggle-icon {
transition: transform var(--animation-speed) ease-in-out;
}
:host(.collapsed) .toggle-icon {
transform: scaleX(-1) scaleY(-1);
}
:host(.collapsed) .close-button {
display: none;
}
.content {
display: block;
padding-inline: var(--space-4);
padding-block-start: 0;
padding-block-end: var(--space-4);
max-height: 100%;
overflow: auto;
}
:host(.collapsed) .content {
display: none;
}
`,
];
toggleCollapsed(): void {
this.classList.toggle("collapsed");
// Remove focus from the toggle button
(this.querySelector(".toggle-button") as HTMLButtonElement)?.blur();
}
handleDismissAll(): void {
// Animate out by adding the class "leaving" and then
// wait for the animation to finish before removing the element
this.classList.add("leaving");
this.addEventListener("animationend", () => {
this.remove();
});
}
render() {
return html` <div class="header">
<span class="title"> Shiny Client Errors </span>
<button
class="close-button"
@click=${this.handleDismissAll}
title="Dismiss all console messages and close console"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
height="1em"
width="1em"
stroke="currentColor"
class="close-icon"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
Dismiss all
</button>
<button class="toggle-button" @click=${this.toggleCollapsed}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
height="1em"
width="1em"
stroke="currentColor"
class="toggle-icon"
>
<path
class="collapse"
stroke-linecap="round"
stroke-linejoin="round"
d="M4.5 19.5l15-15m0 0H8.25m11.25 0v11.25"
/>
</svg>
</button>
</div>
<slot class="content"></slot>`;
}
}
customElements.define("shiny-error-console", ShinyErrorConsole);
export class ShinyErrorMessage extends LitElement {
static properties = {
headline: {},
message: {},
};
headline = "";
message = "";
static styles = [
css`
:host {
color: var(--red-11);
display: block;
font-size: var(--font-md);
position: relative;
--icon-size: var(--font-lg)
/* Reset box sizing */
box-sizing: border-box;
}
.container {
display: flex;
gap: var(--space-2);
}
.contents {
width: 40ch;
display: flex;
flex-direction: column;
gap: var(--space-1);
padding-block-start: 0;
padding-block-end: var(--space-3);
overflow: auto;
}
:host(:last-of-type) .contents {
padding-block-end: var(--space-1);
}
.contents > h3 {
font-size: 1em;
font-weight: 500;
color: var(--red-12);
}
.contents > * {
margin-block: 0;
}
.error-message {
font-family: "Courier New", Courier, monospace;
}
.decoration-container {
flex-shrink: 0;
position: relative;
--line-w: 2px;
--dot-size: 11px;
}
:host(:hover) .decoration-container {
--scale: 1.25;
}
.vertical-line {
margin-inline: auto;
width: var(--line-w);
height: 100%;
background-color: var(--red-10);
}
:host(:first-of-type) .vertical-line {
height: calc(100% - var(--dot-size));
margin-top: var(--dot-size);
}
.dot {
position: absolute;
width: var(--dot-size);
height: var(--dot-size);
top: calc(-1px + var(--dot-size) / 2);
left: calc(50% - var(--dot-size) / 2);
border-radius: 100%;
transform: scale(var(--scale, 1));
color: var(--red-6);
background-color: var(--red-10);
}
.actions {
transform: scaleX(0);
transition: transform calc(var(--animation-speed) / 2) ease-in-out;
display: flex;
justify-content: center;
flex-direction: column;
}
/* Delay transition on mouseout so the buttons don't jump away if the user
overshoots them with their mouse */
:host(:not(:hover)) .actions {
transition-delay: 0.15s;
}
:host(:hover) .actions {
transform: scaleX(1);
}
${buttonStyles}
.copy-button {
padding: 0;
width: var(--space-8);
height: var(--space-8);
position: relative;
--pad: var(--space-2);
}
.copy-button-inner {
position: relative;
width: 100%;
height: 100%;
border-radius: inherit;
transition: transform 0.5s;
transform-style: preserve-3d;
}
/* Animate flipping to the other side when the .copy-success class is
added to the host */
:host(.copy-success) .copy-button-inner {
transform: rotateY(180deg);
}
/* Position the front and back side */
.copy-button .front,
.copy-button .back {
--side: calc(100% - 2 * var(--pad));
position: absolute;
inset: var(--pad);
height: var(--side);
width: var(--side);
-webkit-backface-visibility: hidden; /* Safari */
backface-visibility: hidden;
}
.copy-button:hover .copy-button-inner {
background-color: var(--gray-2);
}
/* Style the back side */
.copy-button .back {
--pad: var(--space-1);
color: var(--green-8);
transform: rotateY(180deg);
}
`,
];
async copyErrorToClipboard(): Promise<void> {
await navigator.clipboard.writeText(this.message);
this.classList.add("copy-success");
// After a second, remove the copy success class
setTimeout(() => {
this.classList.remove("copy-success");
}, 1000);
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
render() {
return html`
<div class="container">
<div class="decoration-container">
<div class="vertical-line"></div>
<div class="dot"></div>
</div>
<div class="contents">
<h3>${this.headline}</h3>
<pre class="error-message">${this.message}</pre>
</div>
<div class="actions">
<button
class="copy-button"
@click=${this.copyErrorToClipboard}
title="Copy error to clipboard"
>
<div class="copy-button-inner">
<svg
class="front"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
height="1em"
width="1em"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
/>
</svg>
<svg
class="back"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
height="1em"
width="1em"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
</button>
</div>
</div>
`;
}
}
customElements.define("shiny-error-message", ShinyErrorMessage);
/**
* Function to show an error message to user in shiny-error-message web
* component. Only shows the error if we're in development mode.
* @param e - Error object to show to user. This is whatever is caught in
* a try-catch statement so it may be a string or it may be a proper Error
* object.
*/
export function showErrorInClientConsole(e: unknown): void {
if (!Shiny.inDevMode()) {
// If we're in production, don't show the error to the user
return;
}
let errorMsg: string | null = null;
let headline = "Error on client while running Shiny app";
if (typeof e === "string") {
errorMsg = e;
} else if (e instanceof ShinyClientError) {
errorMsg = e.message;
headline = e.headline;
} else if (e instanceof Error) {
errorMsg = e.message;
} else {
errorMsg = "Unknown error";
}
// Check to see if an Error Console Container element already exists. If it
// doesn't we need to add it before putting an error on the screen
let errorConsoleContainer = document.querySelector("shiny-error-console");
if (!errorConsoleContainer) {
errorConsoleContainer = document.createElement("shiny-error-console");
document.body.appendChild(errorConsoleContainer);
}
const errorConsole = document.createElement("shiny-error-message");
errorConsole.setAttribute("headline", headline || "");
errorConsole.setAttribute("message", errorMsg);
errorConsoleContainer.appendChild(errorConsole);
}

View File

@@ -8,10 +8,7 @@ import type {
} from "../inputPolicies";
import { shinyAppBindOutput, shinyAppUnbindOutput } from "./initedMethods";
import { sendImageSizeFns } from "./sendImageSize";
const boundInputs: {
[key: string]: { binding: InputBinding; node: HTMLElement };
} = {};
import { ShinyClientError } from "./error";
type BindScope = HTMLElement | JQuery<HTMLElement>;
@@ -44,6 +41,150 @@ function valueChangeCallback(
}
}
/**
* Registry for input and output binding IDs. Used to check for duplicate IDs
* and to keep track of which IDs have already been added to the app. Use an
* immediately invoked function to keep the sets private and not clutter the
* scope.
*/
const bindingsRegistry = (() => {
type BindingTypes = "input" | "output";
/**
* Keyed by binding IDs to the array of each type of binding that ID is associated for in current app state.
*
* Ideally the
* value would be a length 1 array but in some (invalid) cases there could be
* multiple types for a single ID.
*/
type IdToBindingTypes = Map<string, BindingTypes[]>;
// Main store of bindings.
const bindings: IdToBindingTypes = new Map();
/**
* Checks if the bindings registry is valid. Currently this just checks if IDs
* are duplicated within a binding typ but in the future could be expanded to
* check more conditions.
*
* @description IDs are allowed to be duplicated across binding types, but
* when duplicated within a binding type we report all uses of the ID.
* Currently the IDs are typically stored in the bound element's `id`
* attribute, in which case they really *should* be globally unique for
* accessibility and other reasons. However, in practice our bindings still
* work as long as inputs the IDs within a binding type don't overlap.
*
* @returns ShinyClientError if current ID bindings are invalid, otherwise
* returns an ok status.
*/
function checkValidity():
| { status: "error"; error: ShinyClientError }
| { status: "ok" } {
type BindingCounts = { [T in BindingTypes]: number };
const duplicateIds = new Map<string, BindingCounts>();
// count duplicate IDs of each binding type
bindings.forEach((idTypes, id) => {
const counts: { [T in BindingTypes]: number } = { input: 0, output: 0 };
idTypes.forEach((type) => (counts[type] += 1));
// If there's a single duplication of ids across both binding types, then
// when we're not in devmode, we allow this to pass because a good amount of
// existing applications use this pattern even though its invalid. Eventually
// this behavior should be removed.
if (counts.input === 1 && counts.output === 1 && !Shiny.inDevMode()) {
return;
}
// If we have duplicated IDs, then add them to the set of duplicated IDs
// to be reported to the user.
if (counts.input + counts.output > 1) {
duplicateIds.set(id, counts);
}
});
if (duplicateIds.size === 0) return { status: "ok" };
const duplicateIdMsg = Array.from(duplicateIds.entries())
.map(([id, counts]) => {
const messages = [
pluralize(counts.input, "input"),
pluralize(counts.output, "output"),
]
.filter((msg) => msg !== "")
.join(" and ");
return `- "${id}": ${messages}`;
})
.join("\n");
return {
status: "error",
error: new ShinyClientError({
headline: "Duplicate input/output IDs found",
message: `The following ${
duplicateIds.size === 1 ? "ID was" : "IDs were"
} repeated:\n${duplicateIdMsg}`,
}),
};
}
/**
* Add a binding id to the binding ids registry
* @param id Id to add
* @param bindingType Binding type, either "input" or "output"
*/
function addBinding(id: string, bindingType: BindingTypes): void {
if (id === "") {
throw new ShinyClientError({
headline: `Empty ${bindingType} ID found`,
message: "Binding IDs must not be empty.",
});
}
const existingBinding = bindings.get(id);
if (existingBinding) {
existingBinding.push(bindingType);
} else {
bindings.set(id, [bindingType]);
}
}
/**
* Remove a binding id from the binding ids registry
* @param id Id to remove
* @param bindingType Binding type, either "input" or "output"
*/
function removeBinding(id: string, bindingType: BindingTypes): void {
const existingBinding = bindings.get(id);
if (existingBinding) {
const index = existingBinding.indexOf(bindingType);
if (index > -1) {
existingBinding.splice(index, 1);
}
}
if (existingBinding?.length === 0) {
bindings.delete(id);
}
}
return {
addBinding,
removeBinding,
checkValidity,
};
})();
function pluralize(num: number, word: string): string {
if (num === 0) return "";
if (num === 1) return `${num} ${word}`;
return `${num} ${word}s`;
}
type BindInputsCtx = {
inputs: InputValidateDecorator;
inputsRate: InputRateDecorator;
@@ -85,8 +226,8 @@ function bindInputs(
if (el.hasAttribute("data-shiny-no-bind-input")) continue;
const id = binding.getId(el);
// Check if ID is falsy, or if already bound
if (!id || boundInputs[id]) continue;
// Don't bind if ID is falsy or is currently bound
if (!id || $(el).hasClass("shiny-bound-input")) continue;
const type = binding.getType(el);
const effectiveId = type ? id + ":" + type : id;
@@ -123,11 +264,7 @@ function bindInputs(
);
}
boundInputs[id] = {
binding: binding,
node: el,
};
bindingsRegistry.addBinding(id, "input");
$(el).trigger({
type: "shiny:bound",
// @ts-expect-error; Can not remove info on a established, malformed Event object
@@ -156,6 +293,7 @@ async function bindOutputs(
const binding = bindings[i].binding;
const matches = binding.find($scope) || [];
// First loop over the matches and assemble map of id->element
for (let j = 0; j < matches.length; j++) {
const el = matches[j];
const id = binding.getId(el);
@@ -188,6 +326,8 @@ async function bindOutputs(
$el.data("shiny-output-binding", bindingAdapter);
$el.addClass("shiny-bound-output");
if (!$el.attr("aria-live")) $el.attr("aria-live", "polite");
bindingsRegistry.addBinding(id, "output");
$el.trigger({
type: "shiny:bound",
// @ts-expect-error; Can not remove info on a established, malformed Event object
@@ -222,7 +362,8 @@ function unbindInputs(
const id = binding.getId(el);
$(el).removeClass("shiny-bound-input");
delete boundInputs[id];
bindingsRegistry.removeBinding(id, "input");
binding.unsubscribe(el);
$(el).trigger({
type: "shiny:unbound",
@@ -253,6 +394,8 @@ function unbindOutputs(
const id = bindingAdapter.binding.getId(outputs[i]);
shinyAppUnbindOutput(id, bindingAdapter);
bindingsRegistry.removeBinding(id, "output");
$el.removeClass("shiny-bound-output");
$el.removeData("shiny-output-binding");
$el.trigger({
@@ -275,7 +418,19 @@ async function _bindAll(
scope: BindScope
): Promise<ReturnType<typeof bindInputs>> {
await bindOutputs(shinyCtx, scope);
return bindInputs(shinyCtx, scope);
const currentInputs = bindInputs(shinyCtx, scope);
// Check to make sure the bindings setup is valid. By checking the validity
// _after_ we've attempted all the bindings we can give the user a more
// complete error message that contains everything they will need to fix. If
// we threw as we saw collisions then the user would fix the first collision,
// re-run, and then see the next collision, etc.
const bindingValidity = bindingsRegistry.checkValidity();
if (bindingValidity.status === "error") {
throw bindingValidity.error;
}
return currentInputs;
}
function unbindAll(
shinyCtx: BindInputsCtx,

16
srcts/src/shiny/error.ts Normal file
View File

@@ -0,0 +1,16 @@
/**
* Custom error to throw when a we detect a known error type on the client
* @param headline - Error headline to show to user. Will be shown in normal
* font and should be used to give plain language description of problem
* @param message - Error message to show to user. Will be shown in monospaced
* font
*/
export class ShinyClientError extends Error {
headline: string;
constructor({ headline, message }: { headline: string; message: string }) {
super(message);
this.name = "ShinyClientError";
this.headline = headline;
}
}

View File

@@ -27,6 +27,7 @@ import type { Handler, ShinyApp } from "./shinyapp";
import { addCustomMessageHandler } from "./shinyapp";
import { initInputBindings } from "../bindings/input";
import { initOutputBindings } from "../bindings/output";
import { showErrorInClientConsole } from "../components/errorConsole";
interface Shiny {
version: string;
@@ -67,6 +68,14 @@ interface Shiny {
// Eventually deprecate
// For old-style custom messages - should deprecate and migrate to new
oncustommessage?: Handler;
/**
* Method to check if Shiny is running in development mode. By packaging as a
* method, we can we can avoid needing to look for the `__SHINY_DEV_MODE__`
* variable in the global scope.
* @returns `true` if Shiny is running in development mode, `false` otherwise.
*/
inDevMode: () => boolean;
}
let windowShiny: Shiny;
@@ -107,12 +116,23 @@ function setShiny(windowShiny_: Shiny): void {
windowShiny.renderHtmlAsync = renderHtmlAsync;
windowShiny.renderHtml = renderHtml;
windowShiny.inDevMode = () => {
if ("__SHINY_DEV_MODE__" in window)
return Boolean(window.__SHINY_DEV_MODE__);
return false;
};
$(function () {
// Init Shiny a little later than document ready, so user code can
// run first (i.e. to register bindings)
setTimeout(function () {
/* eslint-disable @typescript-eslint/no-floating-promises */
initShiny(windowShiny);
setTimeout(async function () {
try {
await initShiny(windowShiny);
} catch (e) {
showErrorInClientConsole(e);
throw e;
}
}, 1);
});
}

View File

@@ -26,6 +26,7 @@ import { indirectEval } from "../utils/eval";
import type { WherePosition } from "./singletons";
import type { UploadInitValue, UploadEndValue } from "../file/fileProcessor";
import { AsyncQueue } from "../utils/asyncQueue";
import { showErrorInClientConsole } from "../components/errorConsole";
type ResponseValue = UploadEndValue | UploadInitValue;
type Handler = (message: any) => Promise<void> | void;
@@ -129,6 +130,7 @@ class ShinyApp {
// Output bindings
$bindings: { [key: string]: OutputBindingAdapter } = {};
$persistentProgress: Set<string> = new Set();
// Cached values/errors
$values: { [key: string]: any } = {};
@@ -270,11 +272,12 @@ class ShinyApp {
async startActionQueueLoop(): Promise<void> {
// eslint-disable-next-line no-constant-condition
while (true) {
const action = await this.taskQueue.dequeue();
try {
const action = await this.taskQueue.dequeue();
await action();
} catch (e) {
showErrorInClientConsole(e);
console.error(e);
}
}
@@ -514,8 +517,7 @@ class ShinyApp {
id: string,
binding: OutputBindingAdapter
): Promise<OutputBindingAdapter> {
if (!id) throw "Can't bind an element with no ID";
if (this.$bindings[id]) throw "Duplicate binding for ID " + id;
if (!id) throw new Error("Can't bind an element with no ID");
this.$bindings[id] = binding;
if (this.$values[id] !== undefined)
@@ -687,19 +689,28 @@ class ShinyApp {
}
}
private _clearProgress() {
for (const name in this.$bindings) {
if (
hasOwnProperty(this.$bindings, name) &&
!this.$persistentProgress.has(name)
) {
this.$bindings[name].showProgress(false);
}
}
}
private _init() {
// Dev note:
// * Use arrow functions to allow the Types to propagate.
// * However, `_sendMessagesToHandlers()` will adjust the `this` context to the same _`this`_.
addMessageHandler("values", async (message: { [key: string]: any }) => {
for (const name in this.$bindings) {
if (hasOwnProperty(this.$bindings, name))
this.$bindings[name].showProgress(false);
}
this._clearProgress();
for (const key in message) {
if (hasOwnProperty(message, key)) {
this.$persistentProgress.delete(key);
await this.receiveOutput(key, message[key]);
}
}
@@ -709,8 +720,10 @@ class ShinyApp {
"errors",
(message: { [key: string]: ErrorsMessageValue }) => {
for (const key in message) {
if (hasOwnProperty(message, key))
if (hasOwnProperty(message, key)) {
this.$persistentProgress.delete(key);
this.receiveError(key, message[key]);
}
}
}
);
@@ -1400,7 +1413,10 @@ class ShinyApp {
progressHandlers = {
// Progress for a particular object
binding: function (this: ShinyApp, message: { id: string }): void {
binding: function (
this: ShinyApp,
message: { id: string; persistent: boolean }
): void {
const key = message.id;
const binding = this.$bindings[key];
@@ -1412,6 +1428,11 @@ class ShinyApp {
name: key,
});
if (binding.showProgress) binding.showProgress(true);
if (message.persistent) {
this.$persistentProgress.add(key);
} else {
this.$persistentProgress.delete(key);
}
}
},

View File

@@ -2,6 +2,7 @@ import { InputBinding } from "./inputBinding";
type ActionButtonReceiveMessageData = {
label?: string;
icon?: string | [];
disabled?: boolean;
};
declare class ActionButtonInputBinding extends InputBinding {
find(scope: HTMLElement): JQuery<HTMLElement>;

View File

@@ -0,0 +1,20 @@
import { LitElement } from "lit";
export declare class ShinyErrorMessage extends LitElement {
static properties: {
headline: {};
message: {};
};
headline: string;
message: string;
static styles: import("lit").CSSResult[];
copyErrorToClipboard(): Promise<void>;
render(): import("lit-html").TemplateResult<1>;
}
/**
* Function to show an error message to user in shiny-error-message web
* component. Only shows the error if we're in development mode.
* @param e - Error object to show to user. This is whatever is caught in
* a try-catch statement so it may be a string or it may be a proper Error
* object.
*/
export declare function showErrorInClientConsole(e: unknown): void;

14
srcts/types/src/shiny/error.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
/**
* Custom error to throw when a we detect a known error type on the client
* @param headline - Error headline to show to user. Will be shown in normal
* font and should be used to give plain language description of problem
* @param message - Error message to show to user. Will be shown in monospaced
* font
*/
export declare class ShinyClientError extends Error {
headline: string;
constructor({ headline, message }: {
headline: string;
message: string;
});
}

View File

@@ -47,6 +47,13 @@ interface Shiny {
unbindAll?: typeof shinyUnbindAll;
initializeInputs?: typeof shinyInitializeInputs;
oncustommessage?: Handler;
/**
* Method to check if Shiny is running in development mode. By packaging as a
* method, we can we can avoid needing to look for the `__SHINY_DEV_MODE__`
* variable in the global scope.
* @returns `true` if Shiny is running in development mode, `false` otherwise.
*/
inDevMode: () => boolean;
}
declare let windowShiny: Shiny;
declare function setShiny(windowShiny_: Shiny): void;

View File

@@ -30,6 +30,7 @@ declare class ShinyApp {
$bindings: {
[key: string]: OutputBindingAdapter;
};
$persistentProgress: Set<string>;
$values: {
[key: string]: any;
};
@@ -74,10 +75,12 @@ declare class ShinyApp {
$updateConditionals(): void;
dispatchMessage(data: ArrayBufferLike | string): Promise<void>;
private _sendMessagesToHandlers;
private _clearProgress;
private _init;
progressHandlers: {
binding: (this: ShinyApp, message: {
id: string;
persistent: boolean;
}) => void;
open: (message: {
style: "notification" | "old";

View File

@@ -182,84 +182,88 @@
Code
nav_page
Output
<nav class="navbar navbar-default navbar-static-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<span class="navbar-brand">Title</span>
<body class="bslib-page-navbar">
<nav class="navbar navbar-default navbar-static-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<span class="navbar-brand">Title</span>
</div>
<ul class="nav navbar-nav nav-underline" data-tabsetid="4785">
<li class="active">
<a href="#tab-4785-1" data-toggle="tab" data-bs-toggle="tab" data-value="A">A</a>
</li>
<li>
<a href="#tab-4785-2" data-toggle="tab" data-bs-toggle="tab" data-value="B">
<i aria-label="github icon" class="fab fa-github fa-fw" role="presentation"></i>
B
</a>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" data-bs-toggle="dropdown" data-value="Menu">
Menu
<b class="caret"></b>
</a>
<ul class="dropdown-menu" data-tabsetid="1502">
<li>
<a href="#tab-1502-1" data-toggle="tab" data-bs-toggle="tab" data-value="C">C</a>
</li>
</ul>
</li>
</ul>
</div>
</nav>
<div class="container-fluid">
<div class="tab-content" data-tabsetid="4785">
<div class="tab-pane active" data-value="A" id="tab-4785-1">a</div>
<div class="tab-pane" data-value="B" data-icon-class="fab fa-github fa-fw" id="tab-4785-2">b</div>
<div class="tab-pane" data-value="C" id="tab-1502-1">c</div>
</div>
<ul class="nav navbar-nav" data-tabsetid="4785">
<li class="active">
<a href="#tab-4785-1" data-toggle="tab" data-bs-toggle="tab" data-value="A">A</a>
</li>
<li>
<a href="#tab-4785-2" data-toggle="tab" data-bs-toggle="tab" data-value="B">
<i aria-label="github icon" class="fab fa-github fa-fw" role="presentation"></i>
B
</a>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" data-bs-toggle="dropdown" data-value="Menu">
Menu
<b class="caret"></b>
</a>
<ul class="dropdown-menu" data-tabsetid="1502">
<li>
<a href="#tab-1502-1" data-toggle="tab" data-bs-toggle="tab" data-value="C">C</a>
</li>
</ul>
</li>
</ul>
</div>
</nav>
<div class="container-fluid">
<div class="tab-content" data-tabsetid="4785">
<div class="tab-pane active" data-value="A" id="tab-4785-1">a</div>
<div class="tab-pane" data-value="B" data-icon-class="fab fa-github fa-fw" id="tab-4785-2">b</div>
<div class="tab-pane" data-value="C" id="tab-1502-1">c</div>
</div>
</div>
</body>
---
Code
bslib_tags(x)
Output
<nav class="navbar navbar-default navbar-static-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<span class="navbar-brand">Title</span>
<body class="bslib-page-navbar">
<nav class="navbar navbar-default navbar-static-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<span class="navbar-brand">Title</span>
</div>
<ul class="nav navbar-nav nav-underline" data-tabsetid="4785">
<li class="nav-item">
<a href="#tab-4785-1" data-toggle="tab" data-bs-toggle="tab" data-value="A" class="nav-link active">A</a>
</li>
<li class="nav-item">
<a href="#tab-4785-2" data-toggle="tab" data-bs-toggle="tab" data-value="B" class="nav-link">
<i aria-label="github icon" class="fab fa-github fa-fw" role="presentation"></i>
B
</a>
</li>
<li class="dropdown nav-item">
<a href="#" class="dropdown-toggle nav-link" data-toggle="dropdown" data-bs-toggle="dropdown" data-value="Menu">
Menu
<b class="caret"></b>
</a>
<ul class="dropdown-menu" data-tabsetid="1502">
<li>
<a href="#tab-1502-1" data-toggle="tab" data-bs-toggle="tab" data-value="C" class="dropdown-item">C</a>
</li>
</ul>
</li>
</ul>
</div>
</nav>
<div class="container-fluid">
<div class="tab-content" data-tabsetid="4785">
<div class="tab-pane active" data-value="A" id="tab-4785-1">a</div>
<div class="tab-pane" data-value="B" data-icon-class="fab fa-github fa-fw" id="tab-4785-2">b</div>
<div class="tab-pane" data-value="C" id="tab-1502-1">c</div>
</div>
<ul class="nav navbar-nav" data-tabsetid="4785">
<li class="nav-item">
<a href="#tab-4785-1" data-toggle="tab" data-bs-toggle="tab" data-value="A" class="nav-link active">A</a>
</li>
<li class="nav-item">
<a href="#tab-4785-2" data-toggle="tab" data-bs-toggle="tab" data-value="B" class="nav-link">
<i aria-label="github icon" class="fab fa-github fa-fw" role="presentation"></i>
B
</a>
</li>
<li class="dropdown nav-item">
<a href="#" class="dropdown-toggle nav-link" data-toggle="dropdown" data-bs-toggle="dropdown" data-value="Menu">
Menu
<b class="caret"></b>
</a>
<ul class="dropdown-menu" data-tabsetid="1502">
<li>
<a href="#tab-1502-1" data-toggle="tab" data-bs-toggle="tab" data-value="C" class="dropdown-item">C</a>
</li>
</ul>
</li>
</ul>
</div>
</nav>
<div class="container-fluid">
<div class="tab-content" data-tabsetid="4785">
<div class="tab-pane active" data-value="A" id="tab-4785-1">a</div>
<div class="tab-pane" data-value="B" data-icon-class="fab fa-github fa-fw" id="tab-4785-2">b</div>
<div class="tab-pane" data-value="C" id="tab-1502-1">c</div>
</div>
</div>
</body>
# String input is handled properly

View File

@@ -1696,3 +1696,41 @@ test_that("Reactive expression labels", {
"hello"
)
})
test_that("Contexts can be masked off", {
expect_error(
{
r <- reactiveVal()
isolate({
maskReactiveContext({
r()
})
})
},
regexp = "Operation not allowed without an active reactive context"
)
})
test_that("Contexts can be masked off via promise domains", {
r <- reactiveVal()
done <- FALSE
isolate({
maskReactiveContext({
promises::promise_resolve(NULL)$then(function(value) {
done <<- TRUE
expect_error(
{
r()
},
regexp = "Operation not allowed without an active reactive context"
)
})
})
})
while (!done) {
later::run_now(all=FALSE)
}
})

View File

@@ -1,7 +1,6 @@
# tabsetPanel() et al. use p_randomInt() to generate ids (which uses withPrivateSeed()),
# so we need to fix Shiny's private seed in order to make their HTML output deterministic
navlist_panel <- function(...) {
withPrivateSeed(set.seed(100))
navlistPanel(...)
}
@@ -31,6 +30,7 @@ expect_snapshot_bslib <- function(x, ...) {
# Simulates the UI tags that would be produced by
# shinyApp(bootstrapPage(theme), function(...) {})
bslib_tags <- function(ui, theme = bslib::bs_theme()) {
skip_if_not_installed("bslib", "0.5.1.9000")
old_theme <- getCurrentTheme()
on.exit(setCurrentTheme(old_theme), add = TRUE)
setCurrentTheme(theme)

View File

@@ -507,6 +507,53 @@ test_that("session ended handlers work", {
})
})
test_that("shiny.error.unhandled handles unhandled errors", {
caught <- NULL
op <- options(shiny.error.unhandled = function(error) {
caught <<- error
stop("bad user error handler")
})
on.exit(options(op))
server <- function(input, output, session) {
observe({
req(input$boom > 1) # This signals an error that shiny handles
stop("unhandled error") # This error is *not* and brings down the app
})
}
testServer(server, {
session$setInputs(boom = 1)
# validation errors *are* handled and don't trigger the unhandled error
expect_null(caught)
expect_false(session$isEnded())
expect_false(session$isClosed())
# All errors are caught, even the error from the unhandled error handler
# And these errors are converted to warnings
expect_no_error(
expect_warning(
expect_warning(
# Setting input$boom = 2 throws two errors that become warnings:
# 1. The unhandled error in the observe
# 2. The error thrown by the user error handler
capture.output(
session$setInputs(boom = 2),
type = "message"
),
"unhandled error"
),
"bad user error handler"
)
)
expect_s3_class(caught, "error")
expect_equal(conditionMessage(caught), "unhandled error")
expect_true(session$isEnded())
expect_true(session$isClosed())
})
})
test_that("session flush handlers work", {
server <- function(input, output, session) {
rv <- reactiveValues(x = 0, flushCounter = 0, flushedCounter = 0,

View File

@@ -121,6 +121,7 @@ reference:
- reactiveValues
- bindCache
- bindEvent
- ExtendedTask
- reactiveValuesToList
- is.reactivevalues
- isolate

View File

@@ -1761,6 +1761,22 @@ __metadata:
languageName: node
linkType: hard
"@lit-labs/ssr-dom-shim@npm:^1.1.2-pre.0":
version: 1.1.2
resolution: "@lit-labs/ssr-dom-shim@npm:1.1.2"
checksum: 73fd787893851d4ec4aaa5c775405ed2aae4ca0891b2dd3c973b32c2f4bf70ada5481dd0224e52b786d037aa8a00052186ad1623c44551affd66f6409cca8da6
languageName: node
linkType: hard
"@lit/reactive-element@npm:^2.0.0":
version: 2.0.0
resolution: "@lit/reactive-element@npm:2.0.0"
dependencies:
"@lit-labs/ssr-dom-shim": ^1.1.2-pre.0
checksum: afa12f1cf72e8735cb7eaa51d428610785ee796882ca52108310e75ac54bbf5690da718c8bf85d042060f98c139ff0d5efd54f677a9d3fc4d794ad2e0f7a12c5
languageName: node
linkType: hard
"@nodelib/fs.scandir@npm:2.1.5":
version: 2.1.5
resolution: "@nodelib/fs.scandir@npm:2.1.5"
@@ -2232,6 +2248,7 @@ __metadata:
fs-readdir-recursive: ^1.1.0
jest: ^26.6.3
jquery: ^3.6.0
lit: ^3.0.0
lodash: ^4.17.21
madge: ^4.0.2
node-gyp: ^8.1.0
@@ -2292,6 +2309,13 @@ __metadata:
languageName: node
linkType: hard
"@types/trusted-types@npm:^2.0.2":
version: 2.0.5
resolution: "@types/trusted-types@npm:2.0.5"
checksum: e138a70a702e31b49ac73cc33852d892367224be6e096c445194d76327cb46f54f971ae311e34371f649a2d5ac9204afee345bb22f32cfc515eb21c3f12f66b7
languageName: node
linkType: hard
"@types/yargs-parser@npm:*":
version: 21.0.0
resolution: "@types/yargs-parser@npm:21.0.0"
@@ -7033,6 +7057,37 @@ __metadata:
languageName: node
linkType: hard
"lit-element@npm:^4.0.0":
version: 4.0.0
resolution: "lit-element@npm:4.0.0"
dependencies:
"@lit-labs/ssr-dom-shim": ^1.1.2-pre.0
"@lit/reactive-element": ^2.0.0
lit-html: ^3.0.0
checksum: 788ea8ed0883d656583843dc4bbdfcb698f3a4c442a529bf861131e5741239e1076e8809e29b7733dccf6baea72a827aa3cccd444aca2ebe1dd04409608753f9
languageName: node
linkType: hard
"lit-html@npm:^3.0.0":
version: 3.0.0
resolution: "lit-html@npm:3.0.0"
dependencies:
"@types/trusted-types": ^2.0.2
checksum: 1df6bc84a30b9f848962483f456fe820ddac0f836476af8e2e01e4388def21afc129631acde2ad29c761df724b932ddd4d5be87146d8e98c25bc049076f3e4b8
languageName: node
linkType: hard
"lit@npm:^3.0.0":
version: 3.0.0
resolution: "lit@npm:3.0.0"
dependencies:
"@lit/reactive-element": ^2.0.0
lit-element: ^4.0.0
lit-html: ^3.0.0
checksum: 562e53d2902112f55949ff92244ab7a198abdd35455657e364f866954958b22f536f79c02f11a12f00f5b69799e28d4e27917fb120d7216491cb2a526b5ee718
languageName: node
linkType: hard
"locate-path@npm:^5.0.0":
version: 5.0.0
resolution: "locate-path@npm:5.0.0"