Fix: Allow users to opt out of shiny's download autoenable#4371
Fix: Allow users to opt out of shiny's download autoenable#4371elnelson575 wants to merge 68 commits intomainfrom
Conversation
f348912 to
35f71c4
Compare
2e1c842 to
c16218b
Compare
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>
37c1c71 to
f300cf4
Compare
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>
fab1e2b to
75a1439
Compare
- 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>
4ed4391 to
2f21da0
Compare
There was a problem hiding this comment.
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.
- 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>
645bcea to
e2fd3fc
Compare
Ah, good point. Updated, but left the video and sample code since I think they're helpful. |
There was a problem hiding this comment.
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
enabledargument ("auto"/TRUE/FALSE) todownloadButton()anddownloadLink(), and emit adata-shiny-disable-auto-enableattribute for non-"auto"modes. - Update the client download output binding to skip auto-enable when opted out or when
shinyjshas disabled the element, and block clicks on disabled download links. - Add unit + snapshot tests and a
shinytest2integration 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.
- 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>
afbab40 to
d3696b2
Compare
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Motivation
In v1.9.0 (#4041), Shiny began automatically enabling
downloadButton()/downloadLink()once the server'sdownloadHandleris 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 thedownloadHandlerinitializes.Closes #4119. Also likely fixes daattali/shinyjs#277.
Solution
Add an
enabledparameter todownloadButton()anddownloadLink()with three possible values:"auto"(default): Current behavior — starts disabled, auto-enables when thedownloadHandleris 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., viashinyjs).Additionally, when
enabled = "auto", Shiny now detects ifshinyjshas independently disabled the element (via theshinyjs-disabledclass) and skips the auto-enable in that case. This meansshinyjs::disabled()in the UI andshinyjs::disable()from the server both work correctly without any explicit opt-out.Implementation details
data-shiny-disable-auto-enableattribute is added to the element whenenabledisTRUEorFALSE(i.e., any non-"auto"value). The client-siderenderValuechecks for this attribute (and forshinyjs-disabled) before enabling.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