Compare commits

...

38 Commits

Author SHA1 Message Date
E Nelson
32847b98cc fix: remove redundant auxclick handler
pointer-events:none CSS on disabled download links already blocks all
mouse interactions including middle-click, making the auxclick handler
unnecessary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 18:34:38 -04:00
E Nelson
27f3cdfb74 style: convert enabled to logical in one line, drop if/else block
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 18:34:38 -04:00
E Nelson
2594e3e41b style: use braces for auto_enable if/else block
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 18:34:38 -04:00
E Nelson
41dd9023f9 docs: convert shinyjs doc links to plain text, remove from Suggests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 18:34:38 -04:00
E Nelson
f737b4b7bb refactor: replace sendCustomMessage with renderUI in test app, use input_switch
Replace the custom setEnabled JS message handler with renderUI()-based
toggling, which exercises the actual Shiny rendering/re-binding code
path including renderValue re-firing on element replacement. Replace
actionButton toggles with bslib::input_switch for clearer intent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 18:34:24 -04:00
E Nelson
14e1a65b09 fix: apply data-shiny-disable-auto-enable to enabled=TRUE as well
When `enabled = TRUE`, the attribute was previously omitted. Now it is
included for both `enabled = TRUE` and `enabled = FALSE`, since the
attribute's purpose is to signal any non-auto state — not just the
disabled state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 18:34:24 -04:00
E Nelson
0a1c424a79 fix: address code review feedback
- Rename data-ignore-update to data-shiny-disable-auto-enable
- Separate auxclick handler from shiny:filedownload to preserve event semantics
- Move test-only deps (callr, devtools, httr, shinytest2) to Config/Needs/check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 18:34:24 -04:00
elnelson575
85da5b5a97 devtools::document() (GitHub Actions) 2026-04-28 16:11:39 +00:00
E Nelson
719886421d Merge main into fix/autoenable-behavior 2026-04-28 12:04:44 -04:00
E Nelson
1d24bd47bc fix: revert formatting in downloadButton signature, add auxclick comment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 11:59:34 -04:00
E Nelson
c9bc2418ae fix comment 2026-04-24 12:14:06 -04:00
E Nelson
c16218b79d fix AppDriver setup 2026-04-24 12:11:45 -04:00
E Nelson
5948c66ec8 refactor: address code review feedback
- Use auto_enable/enabled logical vars in downloadButton/downloadLink
  instead of string comparisons; drop redundant else NULL
- Share single AppDriver across all shinytest2 tests (faster, ~12s vs ~33s)
- Add skip_on_os() to restrict shinytest2 tests to mac
- Remove redundant Config/Needs/check entry now that shinytest2 is in Suggests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 11:47:38 -04:00
E Nelson
cee115e112 Update DESCRIPTION
Co-authored-by: Barret Schloerke <barret@posit.co>
2026-04-24 11:38:01 -04:00
E Nelson
11c6a866b1 chore: update generated type declaration for showProgress revert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 11:35:03 -04:00
E Nelson
2483746d5f fix: revert showProgress and dead code cleanups per code review
Restore showProgress signature and return/e dead code to keep
the diff minimal per cpsievert's feedback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 11:34:28 -04:00
E Nelson
9366f5d72b fix: use e.currentTarget instead of this in download link click handler
Cast to HTMLAnchorElement for precise typing of .id and .href.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 11:17:19 -04:00
elnelson575
48382c43b4 npm run build (GitHub Actions) 2026-04-23 23:30:52 +00:00
E Nelson
5e04142fef Fix formatting of DownloadLinkOutputBinding class 2026-04-23 19:24:11 -04:00
E Nelson
9b5a36d1aa Update showProgress method signature in DownloadLinkOutputBinding 2026-04-23 19:23:49 -04:00
E Nelson
35f71c4a76 chore: remove shinyjs from seealso (already linked inline in param docs)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 19:21:17 -04:00
E Nelson
c76e5663de chore: fix review issues — add shinyjs to Suggests, correct docs, skip_on_cran
- Add shinyjs to DESCRIPTION Suggests (referenced in @param and @seealso)
- Add shinyjs::enable()/disable() to @seealso
- Fix enabled="auto" doc: "non-NULL filename" was inaccurate; say "initialized the downloadHandler"
- Remove "(replacing autoEnable)" from NEWS — autoEnable was never public
- Add skip_on_cran() to shinytest2 browser test
- Fix trailing space in test app comment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 19:17:43 -04:00
E Nelson
3e57bb3c66 Add to suggests 2026-04-23 18:48:51 -04:00
elnelson575
008efd7363 devtools::document() (GitHub Actions) 2026-04-23 21:41:34 +00:00
E Nelson
bc34f3443f tests: add unit and shinytest2 tests for downloadButton/Link enabled param
- Add missing `downloadLink starts disabled with correct attributes` unit test
- Add shinytest2 runtime tests covering all three `enabled` values for both
  downloadButton and downloadLink: initial state (auto-enable, stays disabled,
  starts enabled, shinyjs-disabled) and toggle on/off via a setEnabled custom
  message handler that mirrors shinyjs::enable()/disable()
- Clarify `enabled=FALSE` docs: the opt-out applies for the page lifetime via
  data-ignore-update, so renderValue never auto-enables regardless of runtime state
- Remove unreachable dead code in DownloadLinkOutputBinding.showProgress()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 17:38:31 -04:00
E Nelson
958027cdfa Update autoEnabled -> enabled 2026-04-23 17:38:20 -04:00
elnelson575
a3f317c75d npm run build (GitHub Actions) 2026-04-10 03:17:27 +00:00
E Nelson
ebd085f3f8 chore: update comment in downloadlink.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 23:14:39 -04:00
E Nelson
4d12245112 fix: block clicks on disabled download links via CSS and correct jQuery this binding
- Add `a.shiny-download-link.disabled { pointer-events: none }` to shiny.scss
  so disabled download links (not just btn-styled ones) block pointer events
- Fix click handler to use `this` instead of `e.currentTarget` — with jQuery
  delegated events, `e.currentTarget` is the document, not the matched element

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 22:57:28 -04:00
E Nelson
6e834a757c chore: add test snapshots and rebuild source maps
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 21:41:08 -04:00
elnelson575
c3a890fcef npm run build (GitHub Actions) 2026-04-09 16:18:39 +00:00
elnelson575
a28a325e7b devtools::document() (GitHub Actions) 2026-04-09 16:18:00 +00:00
E Nelson
12981caf25 feat: rename autoUpdate to autoEnable, invert to data-ignore-update attr, block clicks on disabled download buttons
- Rename `autoUpdate` param to `autoEnable` in `downloadButton()`/`downloadLink()`
- Invert HTML attribute: `autoEnable=FALSE` now sets `data-ignore-update` (absent by default)
- Block click/auxclick events via preventDefault() when button has `disabled` class
- Update NEWS.md and add testthat tests for new attribute behavior

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 12:15:28 -04:00
elnelson575
ff829e929b npm run build (GitHub Actions) 2026-04-09 14:40:33 +00:00
elnelson575
9bf06b75b0 devtools::document() (GitHub Actions) 2026-04-09 14:39:49 +00:00
E Nelson
88888cf47a chore: rebuild shiny.js bundle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 10:35:48 -04:00
E Nelson
d25a8f17fb Revised approach 2026-04-08 23:23:07 -04:00
E Nelson
e93dea6c32 feat: add data-shiny-disable-auto-enable attribute to opt out of download button auto-enable (#4119)
Frameworks like shinyjs, py-shiny, and custom JS can add
`data-shiny-disable-auto-enable` to a download button/link to prevent
Shiny from automatically removing the `disabled` class and
`aria-disabled` attribute on render. This provides a generic,
framework-agnostic opt-out for the auto-enable behavior introduced
in PR #4041.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 09:39:01 -04:00
15 changed files with 556 additions and 38 deletions

View File

@@ -120,7 +120,7 @@ Suggests:
testthat (>= 3.2.1),
watcher,
yaml
Config/Needs/check: shinytest2
Config/Needs/check: callr, devtools, httr, shinytest2
Config/testthat/edition: 3
Encoding: UTF-8
Roxygen: list(markdown = TRUE)

View File

@@ -10,6 +10,10 @@
third-party tab components, and non-Bootstrap frameworks — without requiring
custom event hooks. (#3682)
## Bug fixes and minor improvements
* `downloadButton()` and `downloadLink()` gain a new `enabled` parameter. Use `enabled = "auto"` (default) to automatically enable the button when the download is ready, `enabled = TRUE` to start the button already enabled, or `enabled = FALSE` to opt into manual state management. (#4119)
# shiny 1.13.0
## New features

View File

@@ -1237,6 +1237,15 @@ uiOutput <- htmlOutput
#' @param label The label that should appear on the button.
#' @param class Additional CSS classes to apply to the tag, if any.
#' @param icon An [icon()] to appear on the button. Default is `icon("download")`.
#' @param enabled Controls the enabled/disabled behavior of the button.
#' - `"auto"` (the default): the button starts disabled and is automatically
#' enabled once the server has initialized the `downloadHandler`.
#' - `TRUE`: the button starts enabled immediately, without waiting for the
#' `downloadHandler`.
#' - `FALSE`: the button starts disabled and Shiny will **never**
#' automatically enable it, even after the `downloadHandler` is ready.
#' You are responsible for managing the enabled/disabled state yourself
#' (e.g., with `shinyjs::enable()` and `shinyjs::disable()`).
#' @param ... Other arguments to pass to the container tag function.
#'
#' @examples
@@ -1275,30 +1284,42 @@ downloadButton <- function(outputId,
label="Download",
class=NULL,
...,
enabled = c("auto", TRUE, FALSE),
icon = shiny::icon("download")) {
enabled <- match.arg(as.character(enabled), c("auto", "TRUE", "FALSE"))
auto_enable <- identical(enabled, "auto")
enabled <- identical(enabled, "TRUE")
tags$a(id=outputId,
class='btn btn-default shiny-download-link disabled',
class="btn btn-default shiny-download-link",
class=if (!enabled) "disabled",
class=class,
href='',
target='_blank',
download=NA,
"aria-disabled"="true",
tabindex="-1",
"aria-disabled"=if (!enabled) "true",
"data-shiny-disable-auto-enable"=if (!auto_enable) NA,
tabindex=if (!enabled) "-1",
validateIcon(icon),
label, ...)
}
#' @rdname downloadButton
#' @export
downloadLink <- function(outputId, label="Download", class=NULL, ...) {
tags$a(id=outputId,
class='shiny-download-link disabled',
class=class,
href='',
target='_blank',
download=NA,
"aria-disabled"="true",
tabindex="-1",
downloadLink <- function(outputId, label = "Download", class = NULL, ...,
enabled = c("auto", TRUE, FALSE)) {
enabled <- match.arg(as.character(enabled), c("auto", "TRUE", "FALSE"))
auto_enable <- identical(enabled, "auto")
enabled <- identical(enabled, "TRUE")
tags$a(id = outputId,
class = "shiny-download-link",
class = if (!enabled) "disabled",
class = class,
href = '',
target = '_blank',
download = NA,
"aria-disabled" = if (!enabled) "true",
"data-shiny-disable-auto-enable" = if (!auto_enable) NA,
tabindex = if (!enabled) "-1",
label, ...)
}

View File

@@ -3074,9 +3074,11 @@
}
renderValue(el, data) {
el.setAttribute("href", data);
el.classList.remove("disabled");
el.removeAttribute("aria-disabled");
el.removeAttribute("tabindex");
if (!el.hasAttribute("data-shiny-disable-auto-enable") && !el.classList.contains("shinyjs-disabled")) {
el.classList.remove("disabled");
el.removeAttribute("aria-disabled");
el.removeAttribute("tabindex");
}
}
// Progress shouldn't be shown on the download button
// (progress will be shown as a page level pulse instead)
@@ -3090,9 +3092,14 @@
"click.shinyDownloadLink",
"a.shiny-download-link",
function(e4) {
const el = e4.currentTarget;
if (el.classList.contains("disabled")) {
e4.preventDefault();
return;
}
const evt = import_jquery25.default.Event("shiny:filedownload");
evt.name = this.id;
evt.href = this.href;
evt.name = el.id;
evt.href = el.href;
(0, import_jquery25.default)(document).trigger(evt);
return;
e4;

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

@@ -493,3 +493,10 @@ textarea.textarea-autoresize.form-control {
/* override anything bootstrap sets for `.nav` */
display: none !important;
}
/* Bootstrap only applies pointer-events: none to a.btn.disabled, not plain
anchor-based download links. Apply it universally so disabled download
links cannot be clicked regardless of how they were disabled. */
a.shiny-download-link.disabled {
pointer-events: none;
}

View File

@@ -10,10 +10,17 @@ downloadButton(
label = "Download",
class = NULL,
...,
enabled = c("auto", TRUE, FALSE),
icon = shiny::icon("download")
)
downloadLink(outputId, label = "Download", class = NULL, ...)
downloadLink(
outputId,
label = "Download",
class = NULL,
...,
enabled = c("auto", TRUE, FALSE)
)
}
\arguments{
\item{outputId}{The name of the output slot that the \code{downloadHandler}
@@ -25,6 +32,19 @@ is assigned to.}
\item{...}{Other arguments to pass to the container tag function.}
\item{enabled}{Controls the enabled/disabled behavior of the button.
\itemize{
\item \code{"auto"} (the default): the button starts disabled and is automatically
enabled once the server has initialized the \code{downloadHandler}.
\item \code{TRUE}: the button starts enabled immediately, without waiting for the
\code{downloadHandler}.
\item \code{FALSE}: the button starts disabled and Shiny will \strong{never}
automatically enable it, even after the \code{downloadHandler} is ready.
You are responsible for managing the enabled/disabled state yourself
(e.g., with \code{\link[shinyjs:enable]{shinyjs::enable()}} and \code{\link[shinyjs:disable]{shinyjs::disable()}}).
(e.g., with \code{shinyjs::enable()} and \code{shinyjs::disable()}).
}}
\item{icon}{An \code{\link[=icon]{icon()}} to appear on the button. Default is \code{icon("download")}.}
}
\description{

View File

@@ -8,9 +8,17 @@ class DownloadLinkOutputBinding extends OutputBinding {
}
renderValue(el: HTMLElement, data: string): void {
el.setAttribute("href", data);
el.classList.remove("disabled");
el.removeAttribute("aria-disabled");
el.removeAttribute("tabindex");
// If we or shinyjs have marked this element as disabled (via shinyjs::disabled()),
// skip the auto-enable behavior so that the intentional disabled state is
// preserved. See https://github.com/rstudio/shiny/issues/4119.
if (
!el.hasAttribute("data-shiny-disable-auto-enable") &&
!el.classList.contains("shinyjs-disabled")
) {
el.classList.remove("disabled");
el.removeAttribute("aria-disabled");
el.removeAttribute("tabindex");
}
}
// Progress shouldn't be shown on the download button
// (progress will be shown as a page level pulse instead)
@@ -32,10 +40,18 @@ $(document).on(
"click.shinyDownloadLink",
"a.shiny-download-link",
function (e: Event) {
const el = e.currentTarget as HTMLAnchorElement;
// Prevent clicks when the button is disabled.
if (el.classList.contains("disabled")) {
e.preventDefault();
return;
}
const evt: FileDownloadEvent = $.Event("shiny:filedownload");
evt.name = this.id;
evt.href = this.href;
evt.name = el.id;
evt.href = el.href;
$(document).trigger(evt);
return;

View File

@@ -0,0 +1,51 @@
# downloadButton snapshot (enabled = 'auto')
Code
downloadButton("dl", "Download")
Output
<a aria-disabled="true" class="btn btn-default shiny-download-link disabled" download href="" id="dl" tabindex="-1" target="_blank">
<i class="fas fa-download" role="presentation" aria-label="download icon"></i>
Download
</a>
# downloadButton snapshot (enabled = FALSE)
Code
downloadButton("dl", "Download", enabled = FALSE)
Output
<a aria-disabled="true" class="btn btn-default shiny-download-link disabled" data-shiny-disable-auto-enable download href="" id="dl" tabindex="-1" target="_blank">
<i class="fas fa-download" role="presentation" aria-label="download icon"></i>
Download
</a>
# downloadButton snapshot (enabled = TRUE)
Code
downloadButton("dl", "Download", enabled = TRUE)
Output
<a id="dl" class="btn btn-default shiny-download-link" href="" target="_blank" download data-shiny-disable-auto-enable>
<i class="fas fa-download" role="presentation" aria-label="download icon"></i>
Download
</a>
# downloadLink snapshot (enabled = 'auto')
Code
downloadLink("dl", "Download")
Output
<a aria-disabled="true" class="shiny-download-link disabled" download href="" id="dl" tabindex="-1" target="_blank">Download</a>
# downloadLink snapshot (enabled = FALSE)
Code
downloadLink("dl", "Download", enabled = FALSE)
Output
<a aria-disabled="true" class="shiny-download-link disabled" data-shiny-disable-auto-enable download href="" id="dl" tabindex="-1" target="_blank">Download</a>
# downloadLink snapshot (enabled = TRUE)
Code
downloadLink("dl", "Download", enabled = TRUE)
Output
<a id="dl" class="shiny-download-link" href="" target="_blank" download data-shiny-disable-auto-enable>Download</a>

View File

@@ -0,0 +1,123 @@
library(shiny)
# This app covers the three `enabled` values for both downloadButton and downloadLink,
# plus a simulated shinyjs-disabled case, with toggle switches for each scenario.
# Create a downloadHandler that just serves a simple text file for testing.
handler <- function() {
downloadHandler(
filename = "test.txt",
content = function(file) writeLines("test", file)
)
}
ui <- fluidPage(
h3("downloadButton"),
uiOutput("btn_auto_ui"),
bslib::input_switch("toggle_btn_auto", "Enabled", value = TRUE),
uiOutput("btn_off_ui"),
bslib::input_switch("toggle_btn_off", "Enabled", value = FALSE),
uiOutput("btn_on_ui"),
bslib::input_switch("toggle_btn_on", "Enabled", value = TRUE),
# This mimics what happens when a download button is wrapped in a
# shinyjs::disabled() call within the UI (and therefore at render time).
uiOutput("btn_shinyjs_ui"),
bslib::input_switch("toggle_btn_shinyjs", "Enabled", value = FALSE),
h3("downloadLink"),
uiOutput("lnk_auto_ui"),
bslib::input_switch("toggle_lnk_auto", "Enabled", value = TRUE),
uiOutput("lnk_off_ui"),
bslib::input_switch("toggle_lnk_off", "Enabled", value = FALSE),
uiOutput("lnk_on_ui"),
bslib::input_switch("toggle_lnk_on", "Enabled", value = TRUE),
uiOutput("lnk_shinyjs_ui"),
bslib::input_switch("toggle_lnk_shinyjs", "Enabled", value = FALSE)
)
server <- function(input, output, session) {
output$btn_auto <- handler()
output$btn_off <- handler()
output$btn_on <- handler()
output$btn_shinyjs <- handler()
output$lnk_auto <- handler()
output$lnk_off <- handler()
output$lnk_on <- handler()
output$lnk_shinyjs <- handler()
output$btn_auto_ui <- renderUI({
if (isTRUE(input$toggle_btn_auto)) {
downloadButton("btn_auto", "Auto (default)")
} else {
downloadButton("btn_auto", "Auto (default)", enabled = FALSE)
}
})
output$btn_off_ui <- renderUI({
if (isTRUE(input$toggle_btn_off)) {
downloadButton("btn_off", "Disabled", enabled = TRUE)
} else {
downloadButton("btn_off", "Disabled", enabled = FALSE)
}
})
output$btn_on_ui <- renderUI({
if (isTRUE(input$toggle_btn_on)) {
downloadButton("btn_on", "Pre-enabled", enabled = TRUE)
} else {
downloadButton("btn_on", "Pre-enabled", enabled = FALSE)
}
})
output$btn_shinyjs_ui <- renderUI({
btn <- downloadButton("btn_shinyjs", "shinyjs-disabled")
if (!isTRUE(input$toggle_btn_shinyjs)) {
htmltools::tagAppendAttributes(btn, class = "shinyjs-disabled")
} else {
btn
}
})
output$lnk_auto_ui <- renderUI({
if (isTRUE(input$toggle_lnk_auto)) {
downloadLink("lnk_auto", "Auto (default)")
} else {
downloadLink("lnk_auto", "Auto (default)", enabled = FALSE)
}
})
output$lnk_off_ui <- renderUI({
if (isTRUE(input$toggle_lnk_off)) {
downloadLink("lnk_off", "Disabled", enabled = TRUE)
} else {
downloadLink("lnk_off", "Disabled", enabled = FALSE)
}
})
output$lnk_on_ui <- renderUI({
if (isTRUE(input$toggle_lnk_on)) {
downloadLink("lnk_on", "Pre-enabled", enabled = TRUE)
} else {
downloadLink("lnk_on", "Pre-enabled", enabled = FALSE)
}
})
output$lnk_shinyjs_ui <- renderUI({
lnk <- downloadLink("lnk_shinyjs", "shinyjs-disabled")
if (!isTRUE(input$toggle_lnk_shinyjs)) {
htmltools::tagAppendAttributes(lnk, class = "shinyjs-disabled")
} else {
lnk
}
})
}
shinyApp(ui, server)

View File

@@ -0,0 +1,178 @@
skip_on_cran()
# Skip on everything but mac
skip_on_os(c("windows", "linux", "solaris", "emscripten"))
skip_if_not_installed("shinytest2")
skip_if_not_installed("callr")
library(shinytest2)
# Start the test app in a subprocess, loading the local dev shiny.
# AppDriver$new(url) is used instead of AppDriver$new(app_dir) because the
# latter's internal subprocess runner has a timing issue with devtools-loaded
# packages that prevents the shinytest2 tracer from detecting jQuery.
app_process <- callr::r_bg(
function(app_file, port) {
devtools::load_all(quiet = TRUE)
shiny::runApp(
shiny::shinyAppFile(app_file),
port = port,
host = "127.0.0.1",
launch.browser = FALSE,
quiet = TRUE,
test.mode = TRUE
)
},
args = list(
app_file = testthat::test_path("apps/download-button/app.R"),
port = 7314L
)
)
withr::defer(app_process$kill())
# Wait for the app to be ready
for (i in seq_len(20)) {
Sys.sleep(0.5)
ready <- tryCatch(
{
httr::GET("http://127.0.0.1:7314", httr::timeout(1))
TRUE
},
error = function(e) FALSE
)
if (ready) break
}
if (!ready) skip("Download button test app failed to start")
app_url <- "http://127.0.0.1:7314"
# Start up app once and share across all tests
app <- AppDriver$new(app_url)
withr::defer({ app$stop() })
app$wait_for_idle()
is_disabled <- function(id) {
app$get_js(sprintf(
"var el = document.querySelector('#%s');
el.classList.contains('disabled') &&
el.getAttribute('aria-disabled') === 'true' &&
el.getAttribute('tabindex') === '-1';", id
))
}
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
click_toggle <- function(id) {
input_name <- paste0("toggle_", id)
current <- isTRUE(app$get_value(input = input_name))
do.call(app$set_inputs, setNames(list(!current), input_name))
app$wait_for_idle()
}
# ---------------------------------------------------------------------------
# downloadButton
# ---------------------------------------------------------------------------
test_that("downloadButton (enabled='auto') auto-enables after server init", {
expect_false(is_disabled("btn_auto"))
expect_null(app$get_js("document.querySelector('#btn_auto').getAttribute('aria-disabled')"))
})
test_that("downloadButton (enabled=FALSE) stays disabled after server init", {
expect_true(is_disabled("btn_off"))
})
test_that("downloadButton (enabled=TRUE) starts and stays enabled", {
expect_false(is_disabled("btn_on"))
expect_null(app$get_js("document.querySelector('#btn_on').getAttribute('aria-disabled')"))
})
test_that("downloadButton with shinyjs-disabled class stays disabled after server init", {
expect_true(is_disabled("btn_shinyjs"))
})
test_that("downloadButton (enabled='auto') can be toggled off and back on", {
click_toggle("btn_auto")
expect_true(is_disabled("btn_auto"))
click_toggle("btn_auto")
expect_false(is_disabled("btn_auto"))
})
test_that("downloadButton (enabled=FALSE) can be toggled on and back off", {
click_toggle("btn_off")
expect_false(is_disabled("btn_off"))
click_toggle("btn_off")
expect_true(is_disabled("btn_off"))
})
test_that("downloadButton (enabled=TRUE) can be toggled off and back on", {
click_toggle("btn_on")
expect_true(is_disabled("btn_on"))
click_toggle("btn_on")
expect_false(is_disabled("btn_on"))
})
test_that("downloadButton (shinyjs-disabled) can be toggled on and back off", {
click_toggle("btn_shinyjs")
expect_false(is_disabled("btn_shinyjs"))
click_toggle("btn_shinyjs")
expect_true(is_disabled("btn_shinyjs"))
})
# ---------------------------------------------------------------------------
# downloadLink
# ---------------------------------------------------------------------------
test_that("downloadLink (enabled='auto') auto-enables after server init", {
expect_false(is_disabled("lnk_auto"))
expect_null(app$get_js("document.querySelector('#lnk_auto').getAttribute('aria-disabled')"))
})
test_that("downloadLink (enabled=FALSE) stays disabled after server init", {
expect_true(is_disabled("lnk_off"))
})
test_that("downloadLink (enabled=TRUE) starts and stays enabled", {
expect_false(is_disabled("lnk_on"))
expect_null(app$get_js("document.querySelector('#lnk_on').getAttribute('aria-disabled')"))
})
test_that("downloadLink with shinyjs-disabled class stays disabled after server init", {
expect_true(is_disabled("lnk_shinyjs"))
})
test_that("downloadLink (enabled='auto') can be toggled off and back on", {
click_toggle("lnk_auto")
expect_true(is_disabled("lnk_auto"))
click_toggle("lnk_auto")
expect_false(is_disabled("lnk_auto"))
})
test_that("downloadLink (enabled=FALSE) can be toggled on and back off", {
click_toggle("lnk_off")
expect_false(is_disabled("lnk_off"))
click_toggle("lnk_off")
expect_true(is_disabled("lnk_off"))
})
test_that("downloadLink (enabled=TRUE) can be toggled off and back on", {
click_toggle("lnk_on")
expect_true(is_disabled("lnk_on"))
click_toggle("lnk_on")
expect_false(is_disabled("lnk_on"))
})
test_that("downloadLink (shinyjs-disabled) can be toggled on and back off", {
click_toggle("lnk_shinyjs")
expect_false(is_disabled("lnk_shinyjs"))
click_toggle("lnk_shinyjs")
expect_true(is_disabled("lnk_shinyjs"))
})

View File

@@ -0,0 +1,91 @@
test_that("downloadButton starts disabled with correct attributes", {
btn <- downloadButton("dl", "Download")
html <- as.character(btn)
expect_match(html, "class=.*disabled")
expect_match(html, 'aria-disabled="true"')
expect_match(html, 'tabindex="-1"')
expect_match(html, 'href=""')
})
test_that("downloadLink starts disabled with correct attributes", {
lnk <- downloadLink("dl", "Download")
html <- as.character(lnk)
expect_match(html, "class=.*disabled")
expect_match(html, 'aria-disabled="true"')
expect_match(html, 'tabindex="-1"')
expect_match(html, 'href=""')
})
test_that("downloadButton omits data-shiny-disable-auto-enable by default (enabled = 'auto')", {
btn <- downloadButton("dl", "Download")
html <- as.character(btn)
expect_no_match(html, "data-shiny-disable-auto-enable")
})
test_that("downloadButton has data-shiny-disable-auto-enable when enabled = FALSE", {
btn <- downloadButton("dl", "Download", enabled = FALSE)
html <- as.character(btn)
expect_match(html, "data-shiny-disable-auto-enable")
})
test_that("downloadButton starts enabled when enabled = TRUE", {
btn <- downloadButton("dl", "Download", enabled = TRUE)
html <- as.character(btn)
expect_no_match(html, "disabled")
expect_no_match(html, "aria-disabled")
expect_no_match(html, "tabindex")
expect_match(html, "data-shiny-disable-auto-enable")
})
test_that("downloadLink omits data-shiny-disable-auto-enable by default (enabled = 'auto')", {
lnk <- downloadLink("dl", "Download")
html <- as.character(lnk)
expect_no_match(html, "data-shiny-disable-auto-enable")
})
test_that("downloadLink has data-shiny-disable-auto-enable when enabled = FALSE", {
lnk <- downloadLink("dl", "Download", enabled = FALSE)
html <- as.character(lnk)
expect_match(html, "data-shiny-disable-auto-enable")
})
test_that("downloadLink starts enabled when enabled = TRUE", {
lnk <- downloadLink("dl", "Download", enabled = TRUE)
html <- as.character(lnk)
expect_no_match(html, "disabled")
expect_no_match(html, "aria-disabled")
expect_no_match(html, "tabindex")
expect_match(html, "data-shiny-disable-auto-enable")
})
test_that("downloadButton snapshot (enabled = 'auto')", {
expect_snapshot(downloadButton("dl", "Download"))
})
test_that("downloadButton snapshot (enabled = FALSE)", {
expect_snapshot(downloadButton("dl", "Download", enabled = FALSE))
})
test_that("downloadButton snapshot (enabled = TRUE)", {
expect_snapshot(downloadButton("dl", "Download", enabled = TRUE))
})
test_that("downloadLink snapshot (enabled = 'auto')", {
expect_snapshot(downloadLink("dl", "Download"))
})
test_that("downloadLink snapshot (enabled = FALSE)", {
expect_snapshot(downloadLink("dl", "Download", enabled = FALSE))
})
test_that("downloadLink snapshot (enabled = TRUE)", {
expect_snapshot(downloadLink("dl", "Download", enabled = TRUE))
})