Skip to content

feat: Add toolbar_download_button()#1298

Open
elnelson575 wants to merge 49 commits into
mainfrom
feat/toolbar-download-button
Open

feat: Add toolbar_download_button()#1298
elnelson575 wants to merge 49 commits into
mainfrom
feat/toolbar-download-button

Conversation

@elnelson575
Copy link
Copy Markdown
Contributor

@elnelson575 elnelson575 commented Mar 16, 2026

Closes #1292
NOTE: The gates can be removed when the latest version of shiny is released.

Summary

Adds toolbar_download_button() to the toolbar component family, providing a download button styled consistently with other toolbar inputs for use in card headers, footers, and other toolbar contexts.

  • Adds toolbar_download_button() R function that creates an <a> tag with Shiny's download machinery
  • Adds update_toolbar_download_button() for dynamically updating the disabled state
  • Adds TypeScript input binding for receiving update messages
  • Adds SCSS styles matching toolbar_input_button() patterns
  • Includes comprehensive tests with HTML snapshots

Dependencies:

Manual Test Cases:

Display:

  • Icon only
  • Icon + text
  • Text Only
  • Border
  • Other styling

Activation/Deactivation Logic

  • enabled = "auto" (default)
  • enabled = "TRUE"
  • enabled = "FALSE"

Updating:

  • Update label
  • Update icon
  • Update show/hide label
  • Multi-update (label + icon + show/hide)

Demo App

Screen.Recording.2026-05-29.at.12.48.59.PM.mov
App Code:
library(shiny)
  library(bslib)

  ui <- page_fluid(
    title = "toolbar_download_button() test",
    tags$style(HTML("
      .row-actions { display: flex; gap: .5rem; flex-wrap: wrap; margin: .5rem 0 1rem; }
      .row-actions .btn { padding: .25rem .5rem; }
    ")),
    tags$h3("Initial variations"),
    card(
      card_header(
        "enabled variations",
        toolbar(
          align = "right",
          toolbar_download_button("dl_auto", label = "auto (default)"),
          toolbar_download_button("dl_true", label = "enabled = TRUE", enabled = TRUE),
          toolbar_download_button("dl_false", label = "enabled = FALSE", enabled = FALSE)
        )
      ),
      tags$small(
        tags$code("auto"), " starts disabled then Shiny auto-enables; ",
        tags$code("TRUE"), " starts enabled (no auto-enable); ",
        tags$code("FALSE"), " starts disabled and stays that way."
      )
    ),
    card(
      card_header(
        "display variations",
        toolbar(
          align = "right",
          toolbar_download_button("dl_label", label = "Show label", show_label = TRUE),
          toolbar_download_button("dl_text_only", label = "Text only", show_label = TRUE, icon = NULL,
  tooltip = FALSE),
          toolbar_download_button("dl_border", label = "Bordered", border = TRUE),
          toolbar_download_button(
            "dl_tooltip",
            label = "Default label",
            tooltip = "Custom tooltip text"
          ),
          toolbar_download_button(
            "dl_icon",
            label = "Custom icon",
            icon = shiny::icon("file-csv")
          ),
          toolbar_download_button(
            "dl_success",
            label = "Export",
            show_label = TRUE,
            class = "btn-success"
          ),
          toolbar_download_button(
            "dl_outline",
            label = "Export",
            show_label = TRUE,
            class = "btn-outline-primary"
          )
        )
      ),
      tags$small("show_label, text-only (no icon), border, custom tooltip, custom icon, btn-success,
  btn-outline-primary.")
    ),
    card(
      card_header(
        "alignment test",
        toolbar(
          align = "right",
          toolbar_input_button(
            "align_btn",
            label = "Filter",
            icon = shiny::icon("filter"),
            show_label = TRUE
          ),
          toolbar_input_select(
            "align_select",
            label = "Region",
            choices = c("All", "North", "South", "East", "West"),
            icon = shiny::icon("globe")
          ),
          toolbar_download_button(
            "align_dl",
            label = "Export",
            show_label = TRUE
          )
        )
      ),
      tags$small("Checks icon + text button, icon select, and icon + text download button sit on the same
   baseline.")
    ),
    tags$hr(),
    tags$h3("Update mechanisms"),
    card(
      card_header(
        "Target",
        toolbar(
          align = "right",
          toolbar_download_button(
            "dl_target",
            label = "Target",
            show_label = TRUE
          )
        )
      ),
      tags$h6("disabled"),
      div(
        class = "row-actions",
        actionButton("disable_t", "disabled = TRUE", class = "btn-sm btn-secondary"),
        actionButton("disable_f", "disabled = FALSE", class = "btn-sm btn-primary")
      ),
      tags$h6("label"),
      div(
        class = "row-actions",
        actionButton("label_a", "label = \"Renamed!\"", class = "btn-sm"),
        actionButton("label_b", "label = \"Download CSV\"", class = "btn-sm"),
        actionButton("label_c", "label = \"Target\"", class = "btn-sm")
      ),
      tags$h6("show_label"),
      div(
        class = "row-actions",
        actionButton("show_t", "show_label = TRUE", class = "btn-sm"),
        actionButton("show_f", "show_label = FALSE", class = "btn-sm")
      ),
      tags$h6("icon"),
      div(
        class = "row-actions",
        actionButton("icon_dl", "icon = download", class = "btn-sm"),
        actionButton("icon_csv", "icon = file-csv", class = "btn-sm"),
        actionButton("icon_save", "icon = floppy-disk", class = "btn-sm")
      ),
      tags$h6("multi-update (label + show_label + icon in one call)"),
      div(
        class = "row-actions",
        actionButton("multi_csv", 'label="Export CSV" + show_label=TRUE + icon=file-csv', class = "btn-sm
   btn-outline-primary"),
        actionButton("multi_save", 'label="Save" + show_label=FALSE + icon=floppy-disk', class = "btn-sm
  btn-outline-primary"),
        actionButton("multi_reset", "reset to defaults", class = "btn-sm btn-outline-secondary")
      ),
      tags$h6("server log"),
      verbatimTextOutput("log")
    )
  )

  server <- function(input, output, session) {
    make_handler <- function(name) {
      downloadHandler(
        filename = function() paste0(name, ".txt"),
        content = function(file) writeLines(paste("hello from", name), file)
      )
    }
    for (id in c(
      "dl_auto", "dl_true", "dl_false",
      "dl_label", "dl_text_only", "dl_border", "dl_tooltip", "dl_icon", "dl_success", "dl_outline",
      "dl_target", "align_dl"
    )) {
      local({
        this_id <- id
        output[[this_id]] <- make_handler(this_id)
      })
    }

    log_lines <- reactiveVal(character())
    log_it <- function(msg) {
      line <- paste0(format(Sys.time(), "%H:%M:%S"), "  ", msg)
      message(line)
      log_lines(c(line, head(log_lines(), 19)))
    }
    output$log <- renderText(paste(log_lines(), collapse = "\n"))

    observeEvent(input$disable_t, {
      log_it("update: disabled = TRUE")
      update_toolbar_download_button("dl_target", disabled = TRUE)
    })
    observeEvent(input$disable_f, {
      log_it("update: disabled = FALSE")
      update_toolbar_download_button("dl_target", disabled = FALSE)
    })

    observeEvent(input$label_a, {
      log_it('update: label = "Renamed!"')
      update_toolbar_download_button("dl_target", label = "Renamed!")
    })
    observeEvent(input$label_b, {
      log_it('update: label = "Download CSV"')
      update_toolbar_download_button("dl_target", label = "Download CSV")
    })
    observeEvent(input$label_c, {
      log_it('update: label = "Target"')
      update_toolbar_download_button("dl_target", label = "Target")
    })

    observeEvent(input$show_t, {
      log_it("update: show_label = TRUE")
      update_toolbar_download_button("dl_target", show_label = TRUE)
    })
    observeEvent(input$show_f, {
      log_it("update: show_label = FALSE")
      update_toolbar_download_button("dl_target", show_label = FALSE)
    })

    observeEvent(input$icon_dl, {
      log_it("update: icon = download")
      update_toolbar_download_button("dl_target", icon = shiny::icon("download"))
    })
    observeEvent(input$icon_csv, {
      log_it("update: icon = file-csv")
      update_toolbar_download_button("dl_target", icon = shiny::icon("file-csv"))
    })
    observeEvent(input$icon_save, {
      log_it("update: icon = floppy-disk")
      update_toolbar_download_button("dl_target", icon = shiny::icon("floppy-disk"))
    })

    observeEvent(input$multi_csv, {
      log_it('multi-update: label="Export CSV", show_label=TRUE, icon=file-csv')
      update_toolbar_download_button(
        "dl_target",
        label = "Export CSV",
        show_label = TRUE,
        icon = shiny::icon("file-csv")
      )
    })
    observeEvent(input$multi_save, {
      log_it('multi-update: label="Save", show_label=FALSE, icon=floppy-disk')
      update_toolbar_download_button(
        "dl_target",
        label = "Save",
        show_label = FALSE,
        icon = shiny::icon("floppy-disk")
      )
    })
    observeEvent(input$multi_reset, {
      log_it('multi-update: reset to label="Target", show_label=TRUE, icon=download')
      update_toolbar_download_button(
        "dl_target",
        label = "Target",
        show_label = TRUE,
        icon = shiny::icon("download")
      )
    })
  }

  shinyApp(ui, server)


Test Plan

  • All 162 toolbar tests pass
  • TypeScript compiles without errors
  • HTML structure matches spec
  • Manual verification in Shiny app with downloadHandler()

@elnelson575 elnelson575 marked this pull request as draft March 16, 2026 19:03
Comment thread srcts/src/components/toolbarDownloadButton.ts Outdated
Comment thread inst/components/scss/toolbar.scss Outdated
Comment thread R/toolbar.R Outdated
Comment thread R/toolbar.R Outdated
Comment thread R/toolbar.R
Comment thread R/toolbar.R
Comment thread R/toolbar.R
Comment thread inst/components/scss/toolbar.scss
Comment thread inst/components/scss/toolbar.scss
Comment thread inst/components/scss/toolbar.scss
Comment thread inst/components/scss/toolbar.scss
Comment thread inst/components/scss/toolbar.scss Outdated
Comment thread inst/components/scss/toolbar.scss
Comment thread inst/components/scss/toolbar.scss
Comment thread R/toolbar.R Outdated
Comment thread R/toolbar.R Outdated
Comment thread R/toolbar.R
Comment thread srcts/src/components/toolbarDownloadButton.ts
elnelson575 and others added 10 commits May 29, 2026 17:32
Addresses issue #1292 - adds a download button styled for toolbars.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add base styles and data-type attribute selectors for .bslib-toolbar-download-button to match the existing toolbar input button styling. This includes hover/focus/active states, disabled state, and responsive sizing for label-only, icon-only, and combined icon+label button layouts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds comprehensive test coverage for toolbar_download_button():
- Basic structure validation (classes, button types)
- show_label parameter behavior
- disabled state with accessibility attributes
- border parameter (border-0 vs border-1)
- tooltip parameter (default, custom, and disabled)
- custom icon support

All tests follow existing patterns from toolbar_input_button() tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix border=TRUE to use Bootstrap's 'border' class instead of 'border-1' (border-1 only sets width, not style) for both toolbar_input_button and toolbar_download_button
- Add rel="noopener noreferrer" to target="_blank" anchor tag
- Add @examplesIf block to toolbar_download_button docs
- Add NEWS.md entry under new "New features" section
- Update snapshots to reflect border and rel changes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
elnelson575 and others added 22 commits May 29, 2026 17:34
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…yles

- Fix hover text color on toolbar_download_button: restructure SCSS mixin
  to avoid LibSass dropping the .btn-default guard with 3-level & nesting;
  rely on pointer-events:none on .disabled instead of :not(.disabled) guards
- Support user-supplied class via ... in toolbar_download_button(); extract
  from dots before forwarding to shiny::downloadButton() (which has class as
  a formal arg, unlike shiny::actionButton())
- Fix early-return bug in toolbarDownloadButton.ts that silently aborted icon
  and subsequent updates in a multi-field message
- Remove redundant snapshot and test cases in test-toolbar.R
- Simplify enabled normalization to match shiny::downloadButton() style:
  collapse 4-branch if/else into a vector-default guard + hard abort
- Add accessibility comments to label_id and icon_elem explaining why
  the aria-labelledby pattern and aria-hidden wrapping are necessary
- Add label_text emptiness check/warning to update_toolbar_download_button,
  matching update_toolbar_input_button
Instead of a mixin, write the shared .btn-default interaction styles as
explicit descendant selectors (no &) combined in a comma list. LibSass drops
the .btn-default guard when & is used inside a combined selector, but plain
descendant selectors compile correctly. A $_btn-not variable holds the
shared :not() exclusion list to avoid repeating it on every line.
…tton

Use border-1 to match the released behavior of toolbar_input_button on main.
…ector

Turns out the alleged LibSass combined-selector bug doesn't exist — it compiled
correctly all along. The earlier failure was from serving stale dist/components.css
before recompiling. Replace the $_btn-not interpolation workaround with a simple
comma-separated selector block.
…yles

- Move shared layout properties (flex, height, line-height, action-icon/label
  margins) into the combined input+download selector block to eliminate
  duplication
- Move color and text-decoration overrides inside the .btn-default:not(...)
  block so they are scoped to the default variant only, not btn-success etc.
- Reduce .bslib-toolbar-download-button block to only its unique styles
  (text-decoration base, disabled state)
…ants

text-decoration: none on hover is an <a>-tag concern, not .btn-default-specific.
Scoping it inside the .btn-default guard caused btn-success etc. to show an
underline on hover. Keep it in the broad .bslib-toolbar-download-button block.
@elnelson575 elnelson575 marked this pull request as ready for review May 29, 2026 21:53
@elnelson575 elnelson575 force-pushed the feat/toolbar-download-button branch from eed8dce to e2a889e Compare May 29, 2026 21:55
@elnelson575 elnelson575 requested a review from gadenbuie May 29, 2026 22:05
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.

toolbar_download_button()

2 participants