Skip to content

Fix: Allow users to opt out of shiny's download autoenable#4371

Open
elnelson575 wants to merge 68 commits intomainfrom
fix/autoenable-behavior
Open

Fix: Allow users to opt out of shiny's download autoenable#4371
elnelson575 wants to merge 68 commits intomainfrom
fix/autoenable-behavior

Conversation

@elnelson575
Copy link
Copy Markdown
Collaborator

@elnelson575 elnelson575 commented Apr 9, 2026

Motivation

In v1.9.0 (#4041), Shiny began automatically enabling downloadButton()/downloadLink() once the server's downloadHandler is ready. This is a good default — it prevents users from clicking a download button before the server can handle it.

However, this auto-enable creates a problem for apps that want to keep a download button disabled until some condition is met (e.g., the user selects a dataset first). Before #4041, apps could render the button as disabled and use shinyjs::enable()/shinyjs::disable() (or custom JS) to manage its state. Now, the auto-enable unconditionally overrides that disabled state as soon as the downloadHandler initializes.

Closes #4119. Also likely fixes daattali/shinyjs#277.

Solution

Add an enabled parameter to downloadButton() and downloadLink() with three possible values:

  • "auto" (default): Current behavior — starts disabled, auto-enables when the downloadHandler is ready. No breaking change.
  • TRUE: Starts enabled immediately, without waiting for the server. Useful when you know the handler will be ready and don't want the brief disabled flash.
  • FALSE: Starts disabled and Shiny will never auto-enable it. You manage the enabled/disabled state yourself (e.g., via shinyjs).

Additionally, when enabled = "auto", Shiny now detects if shinyjs has independently disabled the element (via the shinyjs-disabled class) and skips the auto-enable in that case. This means shinyjs::disabled() in the UI and shinyjs::disable() from the server both work correctly without any explicit opt-out.

Implementation details

  • A data-shiny-disable-auto-enable attribute is added to the element when enabled is TRUE or FALSE (i.e., any non-"auto" value). The client-side renderValue checks for this attribute (and for shinyjs-disabled) before enabling.
  • Clicks on disabled download links are now properly blocked via CSS pointer-events: none (previously, a disabled-looking anchor link could still be clicked).

Also used by rstudio/bslib#1298.

App example

Screen.Recording.2026-04-23.at.6.33.35.PM.mov
Sample demo app
library(shinyjs)

# This app tests all permutations of enabled x shinyjs disable method,
# for both downloadButton and downloadLink.

code_block <- function(...) {
  tags$pre(style = "background:#f5f5f5; padding:10px; margin:0;", tags$code(...))
}

case_row <- function(heading, description = NULL, controls, code) {
  tagList(
    h4(heading),
    if (!is.null(description)) p(description),
    fluidRow(
      column(4, controls),
      column(8, code_block(code))
    )
  )
}

ui <- fluidPage(
  useShinyjs(),

  h2("downloadButton: enabled x shinyjs permutations"),

  hr(),
  h3('enabled = "auto" (default)'),

  case_row(
    "Case 1: No shinyjs — should AUTO-ENABLE once server is ready",
    NULL,
    downloadButton("btn_1", "Download"),
    'downloadButton("btn_1", "Download")'
  ),

  br(),
  case_row(
    "Case 2: shinyjs::disabled() in UI — stays disabled until toggled",
    controls = tagList(
      actionButton("toggle_btn_2", "Toggle"),
      br(), br(),
      disabled(downloadButton("btn_2", "Download"))
    ),
    code = 'disabled(
  downloadButton("btn_2", "Download")
)

# server:
observeEvent(input$toggle_btn_2, {
  if (input$toggle_btn_2 %% 2 == 1) enable("btn_2") else disable("btn_2")
})'
  ),

  br(),
  case_row(
    "Case 3: shinyjs::disable() from server — auto-enables, then disable/enable on toggle",
    "When disabled, renderValue should not override it.",
    tagList(
      actionButton("toggle_btn_3", "Toggle"),
      br(), br(),
      downloadButton("btn_3", "Download")
    ),
    'downloadButton("btn_3", "Download")

# server:
observeEvent(input$toggle_btn_3, {
  if (input$toggle_btn_3 %% 2 == 1) disable("btn_3") else enable("btn_3")
})'
  ),

  hr(),
  h3("enabled = TRUE (explicit pre-enable)"),

  case_row(
    "Case 4: No shinyjs — starts enabled immediately (before server), stays enabled",
    NULL,
    downloadButton("btn_4", "Download", enabled = TRUE),
    'downloadButton("btn_4", "Download", enabled = TRUE)'
  ),

  br(),
  case_row(
    "Case 5: shinyjs::disabled() in UI — starts disabled, toggle enables/disables",
    controls = tagList(
      actionButton("toggle_btn_5", "Toggle"),
      br(), br(),
      disabled(downloadButton("btn_5", "Download", enabled = TRUE))
    ),
    code = 'disabled(
  downloadButton("btn_5", "Download", enabled = TRUE)
)

# server:
observeEvent(input$toggle_btn_5, {
  if (input$toggle_btn_5 %% 2 == 1) enable("btn_5") else disable("btn_5")
})'
  ),

  br(),
  case_row(
    "Case 6: shinyjs::disable() from server — starts enabled, disable/enable on toggle",
    "Disabled state should be preserved after renderValue fires.",
    tagList(
      actionButton("toggle_btn_6", "Toggle"),
      br(), br(),
      downloadButton("btn_6", "Download", enabled = TRUE)
    ),
    'downloadButton("btn_6", "Download", enabled = TRUE)

# server:
observeEvent(input$toggle_btn_6, {
  if (input$toggle_btn_6 %% 2 == 1) disable("btn_6") else enable("btn_6")
})'
  ),

  hr(),
  h3("enabled = FALSE (explicit opt-out)"),

  case_row(
    "Case 7: No shinyjs — should STAY DISABLED",
    NULL,
    downloadButton("btn_7", "Download", enabled = FALSE),
    'downloadButton("btn_7", "Download", enabled = FALSE)'
  ),

  br(),
  case_row(
    "Case 8: shinyjs::disabled() in UI — stays disabled until toggled",
    "Toggle should enable on first click, then disable on the next.",
    tagList(
      actionButton("toggle_btn_8", "Toggle"),
      br(), br(),
      disabled(downloadButton("btn_8", "Download", enabled = FALSE))
    ),
    'disabled(
  downloadButton("btn_8", "Download", enabled = FALSE)
)

# server:
observeEvent(input$toggle_btn_8, {
  if (input$toggle_btn_8 %% 2 == 1) enable("btn_8") else disable("btn_8")
})'
  ),

  br(),
  case_row(
    "Case 9: shinyjs::disable() from server — stays disabled until toggled",
    NULL,
    tagList(
      actionButton("toggle_btn_9", "Toggle"),
      br(), br(),
      downloadButton("btn_9", "Download", enabled = FALSE)
    ),
    'downloadButton("btn_9", "Download", enabled = FALSE)

# server:
observeEvent(input$toggle_btn_9, {
  if (input$toggle_btn_9 %% 2 == 1) enable("btn_9") else disable("btn_9")
})'
  ),

  h2("downloadLink: enabled x shinyjs permutations"),

  hr(),
  h3('enabled = "auto" (default)'),

  case_row(
    "Case 10: No shinyjs — should AUTO-ENABLE once server is ready",
    NULL,
    downloadLink("lnk_1", "Download"),
    'downloadLink("lnk_1", "Download")'
  ),

  br(),
  case_row(
    "Case 11: shinyjs::disabled() in UI — stays disabled until toggled",
    controls = tagList(
      actionButton("toggle_lnk_2", "Toggle"),
      br(), br(),
      disabled(downloadLink("lnk_2", "Download"))
    ),
    code = 'disabled(
  downloadLink("lnk_2", "Download")
)

# server:
observeEvent(input$toggle_lnk_2, {
  if (input$toggle_lnk_2 %% 2 == 1) enable("lnk_2") else disable("lnk_2")
})'
  ),

  br(),
  case_row(
    "Case 12: shinyjs::disable() from server — auto-enables, then disable/enable on toggle",
    "When disabled, renderValue should not override it.",
    tagList(
      actionButton("toggle_lnk_3", "Toggle"),
      br(), br(),
      downloadLink("lnk_3", "Download")
    ),
    'downloadLink("lnk_3", "Download")

# server:
observeEvent(input$toggle_lnk_3, {
  if (input$toggle_lnk_3 %% 2 == 1) disable("lnk_3") else enable("lnk_3")
})'
  ),

  hr(),
  h3("enabled = TRUE (explicit pre-enable)"),

  case_row(
    "Case 13: No shinyjs — starts enabled immediately (before server), stays enabled",
    NULL,
    downloadLink("lnk_4", "Download", enabled = TRUE),
    'downloadLink("lnk_4", "Download", enabled = TRUE)'
  ),

  br(),
  case_row(
    "Case 14: shinyjs::disabled() in UI — starts disabled, toggle enables/disables",
    controls = tagList(
      actionButton("toggle_lnk_5", "Toggle"),
      br(), br(),
      disabled(downloadLink("lnk_5", "Download", enabled = TRUE))
    ),
    code = 'disabled(
  downloadLink("lnk_5", "Download", enabled = TRUE)
)

# server:
observeEvent(input$toggle_lnk_5, {
  if (input$toggle_lnk_5 %% 2 == 1) enable("lnk_5") else disable("lnk_5")
})'
  ),

  br(),
  case_row(
    "Case 15: shinyjs::disable() from server — starts enabled, disable/enable on toggle",
    "Disabled state should be preserved after renderValue fires.",
    tagList(
      actionButton("toggle_lnk_6", "Toggle"),
      br(), br(),
      downloadLink("lnk_6", "Download", enabled = TRUE)
    ),
    'downloadLink("lnk_6", "Download", enabled = TRUE)

# server:
observeEvent(input$toggle_lnk_6, {
  if (input$toggle_lnk_6 %% 2 == 1) disable("lnk_6") else enable("lnk_6")
})'
  ),

  hr(),
  h3("enabled = FALSE (explicit opt-out)"),

  case_row(
    "Case 16: No shinyjs — should STAY DISABLED",
    NULL,
    downloadLink("lnk_7", "Download", enabled = FALSE),
    'downloadLink("lnk_7", "Download", enabled = FALSE)'
  ),

  br(),
  case_row(
    "Case 17: shinyjs::disabled() in UI — stays disabled until toggled",
    "Toggle should enable on first click, then disable on the next.",
    tagList(
      actionButton("toggle_lnk_8", "Toggle"),
      br(), br(),
      disabled(downloadLink("lnk_8", "Download", enabled = FALSE))
    ),
    'disabled(
  downloadLink("lnk_8", "Download", enabled = FALSE)
)

# server:
observeEvent(input$toggle_lnk_8, {
  if (input$toggle_lnk_8 %% 2 == 1) enable("lnk_8") else disable("lnk_8")
})'
  ),

  br(),
  case_row(
    "Case 18: shinyjs::disable() from server — stays disabled until toggled",
    NULL,
    tagList(
      actionButton("toggle_lnk_9", "Toggle"),
      br(), br(),
      downloadLink("lnk_9", "Download", enabled = FALSE)
    ),
    'downloadLink("lnk_9", "Download", enabled = FALSE)

# server:
observeEvent(input$toggle_lnk_9, {
  if (input$toggle_lnk_9 %% 2 == 1) enable("lnk_9") else disable("lnk_9")
})'
  )
)

server <- function(input, output, session) {
  make_handler <- function(label) {
    downloadHandler(
      filename = paste0(label, ".txt"),
      content = function(file) writeLines(paste("File from", label), file)
    )
  }

  for (i in 1:9) output[[paste0("btn_", i)]] <- make_handler(paste0("btn_", i))
  for (i in 1:9) output[[paste0("lnk_", i)]] <- make_handler(paste0("lnk_", i))

  observeEvent(input$toggle_btn_2, {
    if (input$toggle_btn_2 %% 2 == 1) enable("btn_2") else disable("btn_2")
  })
  observeEvent(input$toggle_btn_3, {
    if (input$toggle_btn_3 %% 2 == 1) disable("btn_3") else enable("btn_3")
  })
  observeEvent(input$toggle_btn_5, {
    if (input$toggle_btn_5 %% 2 == 1) enable("btn_5") else disable("btn_5")
  })
  observeEvent(input$toggle_btn_6, {
    if (input$toggle_btn_6 %% 2 == 1) disable("btn_6") else enable("btn_6")
  })
  observeEvent(input$toggle_btn_8, {
    if (input$toggle_btn_8 %% 2 == 1) enable("btn_8") else disable("btn_8")
  })
  observeEvent(input$toggle_btn_9, {
    if (input$toggle_btn_9 %% 2 == 1) enable("btn_9") else disable("btn_9")
  })
  observeEvent(input$toggle_lnk_2, {
    if (input$toggle_lnk_2 %% 2 == 1) enable("lnk_2") else disable("lnk_2")
  })
  observeEvent(input$toggle_lnk_3, {
    if (input$toggle_lnk_3 %% 2 == 1) disable("lnk_3") else enable("lnk_3")
  })
  observeEvent(input$toggle_lnk_5, {
    if (input$toggle_lnk_5 %% 2 == 1) enable("lnk_5") else disable("lnk_5")
  })
  observeEvent(input$toggle_lnk_6, {
    if (input$toggle_lnk_6 %% 2 == 1) disable("lnk_6") else enable("lnk_6")
  })
  observeEvent(input$toggle_lnk_8, {
    if (input$toggle_lnk_8 %% 2 == 1) enable("lnk_8") else disable("lnk_8")
  })
  observeEvent(input$toggle_lnk_9, {
    if (input$toggle_lnk_9 %% 2 == 1) enable("lnk_9") else disable("lnk_9")
  })
}

shinyApp(ui, server)

@elnelson575 elnelson575 linked an issue Apr 9, 2026 that may be closed by this pull request
@elnelson575 elnelson575 marked this pull request as ready for review April 10, 2026 03:40
@elnelson575 elnelson575 requested a review from cpsievert April 10, 2026 14:35
Comment thread R/bootstrap.R Outdated
@elnelson575 elnelson575 force-pushed the fix/autoenable-behavior branch from f348912 to 35f71c4 Compare April 23, 2026 23:21
@elnelson575 elnelson575 requested a review from cpsievert April 23, 2026 23:27
Comment thread srcts/src/bindings/output/downloadlink.ts
Comment thread srcts/src/bindings/output/downloadlink.ts
Comment thread srcts/src/bindings/output/downloadlink.ts Outdated
Comment thread DESCRIPTION Outdated
Comment thread R/bootstrap.R Outdated
Comment thread R/bootstrap.R Outdated
Comment thread tests/testthat/test-downloadButton-shinytest2.R Outdated
Comment thread DESCRIPTION Outdated
Comment thread DESCRIPTION Outdated
Comment thread tests/testthat/test-downloadButton-shinytest2.R Outdated
@elnelson575 elnelson575 force-pushed the fix/autoenable-behavior branch from 2e1c842 to c16218b Compare April 24, 2026 16:11
Comment thread R/bootstrap.R Outdated
Comment thread srcts/src/bindings/output/downloadlink.ts Outdated
Comment thread R/bootstrap.R Outdated
Comment thread DESCRIPTION Outdated
Comment thread tests/testthat/test-downloadButton-shinytest2.R Outdated
Comment thread R/bootstrap.R Outdated
Comment thread srcts/src/bindings/output/downloadlink.ts Outdated
Comment thread srcts/src/bindings/output/downloadlink.ts Outdated
Comment thread tests/testthat/apps/download-button/app.R Outdated
Comment thread tests/testthat/apps/download-button/app.R Outdated
Comment thread R/bootstrap.R Outdated
Comment thread R/bootstrap.R Outdated
Comment thread tests/testthat/apps/download-button/app.R Outdated
elnelson575 and others added 3 commits May 6, 2026 20:56
The rebase with -X theirs kept an intermediate version of the #3682
ResizeObserver commit that was already superseded in main. Reset
srcts/src/shiny/index.ts and srcts/src/utils/index.ts to main's
versions and rebuild the JS bundles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@elnelson575 elnelson575 force-pushed the fix/autoenable-behavior branch from 37c1c71 to f300cf4 Compare May 7, 2026 00:58
elnelson575 and others added 3 commits May 7, 2026 01:01
A GitHub Actions devtools::document() commit in the branch regenerated
all Rd files, adding noise to the PR diff. Reset all Rd files not
related to this feature (downloadButton/downloadLink) back to main.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@elnelson575 elnelson575 force-pushed the fix/autoenable-behavior branch from fab1e2b to 75a1439 Compare May 7, 2026 02:16
elnelson575 and others added 2 commits May 6, 2026 22:20
- Remove devtools from Config/Needs/check (not actually used in tests)
- Restore RoxygenNote: 7.3.3 and remove Config/roxygen2/version added
  by GitHub Actions devtools::document() commit
- Restore three NEWS bullet points dropped by -X theirs rebase

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@elnelson575 elnelson575 force-pushed the fix/autoenable-behavior branch from 4ed4391 to 2f21da0 Compare May 7, 2026 02:20
@elnelson575 elnelson575 requested a review from cpsievert May 7, 2026 02:23
Copy link
Copy Markdown
Collaborator

@cpsievert cpsievert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes are looking pretty good here. It'd be great if we could also improve the PR description. Taking a quick pass with Claude gave me this:

PR Description Draft
## Motivation

In v1.9.0 (#4041), Shiny began automatically enabling `downloadButton()`/`downloadLink()` once the server's `downloadHandler` is ready. This is a good default — it prevents users from clicking a download button before the server can handle it.

However, this auto-enable creates a problem for apps that want to keep a download button disabled until some condition is met (e.g., the user selects a dataset first). Before #4041, apps could render the button as disabled and use `shinyjs::enable()`/`shinyjs::disable()` (or custom JS) to manage its state. Now, the auto-enable unconditionally overrides that disabled state as soon as the `downloadHandler` initializes.

Reported in #4119. Also likely fixes https://github.com/daattali/shinyjs/issues/277.

## Solution

Add an `enabled` parameter to `downloadButton()` and `downloadLink()` with three possible values:

- **`"auto"` (default):** Current behavior — starts disabled, auto-enables when the `downloadHandler` is ready. No breaking change.
- **`TRUE`:** Starts enabled immediately, without waiting for the server. Useful when you know the handler will be ready and don't want the brief disabled flash.
- **`FALSE`:** Starts disabled and Shiny will *never* auto-enable it. You manage the enabled/disabled state yourself (e.g., via `shinyjs`).

Additionally, when `enabled = "auto"`, Shiny now detects if `shinyjs` has independently disabled the element (via the `shinyjs-disabled` class) and skips the auto-enable in that case. This means `shinyjs::disabled()` in the UI and `shinyjs::disable()` from the server both work correctly without any explicit opt-out.

## Implementation details

- A `data-shiny-disable-auto-enable` attribute is added to the element when `enabled` is `TRUE` or `FALSE` (i.e., any non-`"auto"` value). The client-side `renderValue` checks for this attribute (and for `shinyjs-disabled`) before enabling.
- Clicks on disabled download links are now properly blocked (previously, a disabled-looking link could still be clicked).

Also used by https://github.com/rstudio/bslib/pull/1298.

Comment thread NEWS.md Outdated
Comment thread tests/testthat/test-downloadButton.R Outdated
- Replace regex expect_match/expect_no_match with htmltools::tagGetAttribute()
  for more precise attribute assertions
- Update NEWS entry to cpsievert's suggested wording

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@elnelson575 elnelson575 force-pushed the fix/autoenable-behavior branch from 645bcea to e2fd3fc Compare May 7, 2026 15:24
@elnelson575
Copy link
Copy Markdown
Collaborator Author

Changes are looking pretty good here. It'd be great if we could also improve the PR description. Taking a quick pass with Claude gave me this:

PR Description Draft

Ah, good point. Updated, but left the video and sample code since I think they're helpful.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an opt-out mechanism for Shiny’s automatic enabling of downloadButton()/downloadLink() once the server-side downloadHandler is ready, restoring the ability for apps (and shinyjs) to manage disabled state without being overridden.

Changes:

  • Add an enabled argument ("auto"/TRUE/FALSE) to downloadButton() and downloadLink(), and emit a data-shiny-disable-auto-enable attribute for non-"auto" modes.
  • Update the client download output binding to skip auto-enable when opted out or when shinyjs has disabled the element, and block clicks on disabled download links.
  • Add unit + snapshot tests and a shinytest2 integration test app; regenerate/refresh a number of Rd files and shared JS/CSS artifacts.

Reviewed changes

Copilot reviewed 53 out of 56 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/testthat/test-zzz-st2-download.R Adds shinytest2 integration tests covering enabled/disabled permutations for downloads.
tests/testthat/test-downloadButton.R Adds unit tests and snapshots for new enabled behavior and attributes.
tests/testthat/helper-shinytest2.R Adds helper utilities for running shinytest2-based tests.
tests/testthat/_snaps/downloadButton.md Snapshot updates for new download markup/attributes.
srcts/src/bindings/output/downloadlink.ts Skips auto-enable when opted out / shinyjs-disabled; blocks clicks on disabled links.
R/bootstrap.R Implements enabled parameter and validation; adds data attribute and updated disabled markup.
NEWS.md Documents the new enabled parameter and behavior.
man/verticalLayout.Rd Rd regeneration/formatting updates.
man/varSelectInput.Rd Rd regeneration/formatting updates.
man/updateSliderInput.Rd Rd regeneration/formatting updates.
man/updateSelectInput.Rd Rd regeneration/formatting updates.
man/textInput.Rd Rd regeneration/formatting updates.
man/textAreaInput.Rd Rd regeneration/formatting updates.
man/submitButton.Rd Rd regeneration/formatting updates.
man/splitLayout.Rd Rd regeneration/formatting updates.
man/sliderInput.Rd Rd regeneration/formatting updates.
man/sidebarLayout.Rd Rd regeneration/formatting updates.
man/shiny-package.Rd Rd regeneration/formatting updates (authors list).
man/selectInput.Rd Rd regeneration/formatting updates.
man/repeatable.Rd Rd regeneration/formatting updates.
man/renderPlot.Rd Rd regeneration/formatting updates.
man/reexports.Rd Rd regeneration/formatting updates.
man/reactiveValuesToList.Rd Rd regeneration/formatting updates.
man/reactive.Rd Rd regeneration/formatting updates.
man/radioButtons.Rd Rd regeneration/formatting updates.
man/Progress.Rd Rd regeneration/formatting updates.
man/plotPNG.Rd Rd regeneration/formatting updates.
man/passwordInput.Rd Rd regeneration/formatting updates.
man/numericInput.Rd Rd regeneration/formatting updates.
man/NS.Rd Rd regeneration/formatting updates.
man/navbarPage.Rd Rd regeneration/formatting updates.
man/MockShinySession.Rd Rd regeneration/formatting updates.
man/markdown.Rd Rd regeneration/formatting updates.
man/isolate.Rd Rd regeneration/formatting updates.
man/icon.Rd Rd regeneration/formatting updates.
man/fluidPage.Rd Rd regeneration/formatting updates.
man/flowLayout.Rd Rd regeneration/formatting updates.
man/fixedPage.Rd Rd regeneration/formatting updates.
man/fillPage.Rd Rd regeneration/formatting updates.
man/fileInput.Rd Rd regeneration/formatting updates.
man/ExtendedTask.Rd Rd regeneration/formatting updates.
man/downloadButton.Rd Documents new enabled argument for download controls.
man/devmode.Rd Rd regeneration/formatting updates.
man/dateRangeInput.Rd Rd regeneration/formatting updates.
man/dateInput.Rd Rd regeneration/formatting updates.
man/createRenderFunction.Rd Rd regeneration/formatting updates.
man/checkboxInput.Rd Rd regeneration/formatting updates.
man/checkboxGroupInput.Rd Rd regeneration/formatting updates.
man/actionButton.Rd Rd regeneration/formatting updates.
inst/www/shared/shiny.min.js Updated built/minified JS bundle reflecting download binding changes.
inst/www/shared/shiny.min.css Updated built/minified CSS (disabled download link pointer-events).
inst/www/shared/shiny.js Updated built JS bundle reflecting download binding changes.
inst/www/shared/shiny_scss/shiny.scss Adds CSS to block clicks on disabled download links via pointer-events.
DESCRIPTION Updates check-related config metadata.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread srcts/src/bindings/output/downloadlink.ts Outdated
Comment thread tests/testthat/helper-shinytest2.R
- Prevent early clicks on enabled=TRUE buttons before renderValue sets
  the URL (href="" would otherwise navigate to the current page)
- Add skip_if_not_installed("shinyjs") to skip_if_not_shinytest2() so
  tests skip cleanly in envs where shinyjs is not available

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@elnelson575 elnelson575 force-pushed the fix/autoenable-behavior branch from afbab40 to d3696b2 Compare May 7, 2026 19:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: shinyjs::disabled() doesn't fully prevent downloads from shiny::downloadButton() Allow creating disabled downloadButton()/downloadLink()

4 participants