Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
24bbf1c
Minor updates
elnelson575 Nov 3, 2025
28e5645
Updated news
elnelson575 Nov 3, 2025
a545e54
Removing example:
elnelson575 Nov 3, 2025
1e4b493
Adding snaps
elnelson575 Nov 5, 2025
b8470d1
Update to toolbar testing
elnelson575 Nov 5, 2025
a0d1922
updates to dots
elnelson575 Nov 5, 2025
4e8cbe1
Adding input buttons
elnelson575 Nov 12, 2025
2a4c33b
Added disabled option
elnelson575 Nov 12, 2025
1996814
Updated code
elnelson575 Nov 14, 2025
7bb150a
Updating button spacing
elnelson575 Nov 14, 2025
0643779
Updated card header and font sizing
elnelson575 Nov 19, 2025
0367059
Updating tests
elnelson575 Nov 20, 2025
1596ecc
Add back deleted file
elnelson575 Nov 20, 2025
c84e8e8
Update headings in bs-theme-preset.md
elnelson575 Nov 20, 2025
b15b438
Update header format in bs-theme-preset-builtin.md
elnelson575 Nov 20, 2025
abba490
Update card.scss
elnelson575 Nov 20, 2025
fae3c88
Fixing diff
elnelson575 Nov 20, 2025
ccca94e
Fixing diff
elnelson575 Nov 20, 2025
05f14d0
Apply suggestions from code review
elnelson575 Nov 21, 2025
b1b54cd
Updating with edits based on comments
elnelson575 Nov 24, 2025
e03aada
Initial formatting
elnelson575 Nov 25, 2025
2c7801a
Adding docs and tests, refined
elnelson575 Nov 25, 2025
d41509d
Update NEWS.md
elnelson575 Nov 25, 2025
25ca378
Correcting diff
elnelson575 Dec 9, 2025
f01c0b1
Additional diff correction
elnelson575 Dec 9, 2025
c579706
Updates to labeling
elnelson575 Dec 16, 2025
074860e
Squashed commit of the following:
elnelson575 Dec 17, 2025
7e30bba
Update tests and docs
elnelson575 Dec 17, 2025
6c32b69
Updating switch label
elnelson575 Dec 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,10 @@ export(toggle_sidebar)
export(toggle_switch)
export(toggle_tooltip)
export(toolbar)
export(toolbar_divider)
export(toolbar_input_button)
export(toolbar_input_select)
export(toolbar_input_switch)
export(tooltip)
export(update_popover)
export(update_submit_textarea)
Expand Down
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

* Added a new `toolbar()` component for creating Bootstrap toolbars that can contain buttons, text, and other elements. (#1247)
* Added `toolbar_input_button()` for easily creating buttons to include in a `toolbar()`. (#1248)
* Added `toolbar_input_switch()` for easily creating switch inputs to include in a `toolbar()`. (#1257)
* Added `toolbar_input_select()`, a select input designed for use within a `toolbar()`. (#1249)
* Added `toolbar_divider()` for adding visual dividers with customizable width and spacing between toolbar elements. (#1259)

## Improvements and bug fixes

Expand Down
Binary file modified R/sysdata.rda
Binary file not shown.
313 changes: 294 additions & 19 deletions R/toolbar.R
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,20 @@ toolbar <- function(
#' )
#'
#' @param id The input ID.
#' @param icon An icon to display in the button. If provided without
#' `show_label = TRUE`, only the icon will be visible.
#' @param label The button label. Used as button text when `show_label = TRUE`,
#' or as an accessibility label when hidden. Also used as the default
#' tooltip text when `tooltip = TRUE`.
#' @param show_label Whether to show the label text in the button. If `FALSE`
#' (the default), only the icon is shown (if provided). If `TRUE`, the label
#' text is shown alongside the icon.
#' @param tooltip Tooltip text to display when hovering over the button. Can be:
#' * `TRUE` (default when `show_label = FALSE`) - shows a tooltip with the `label` text
#' @param icon An icon. If provided without `show_label = TRUE`, only the icon
#' will be visible.
#' @param label The input label. By default, `label` is not shown but is used by
#' `tooltip`. Set `show_label = TRUE` to show the label (see `tooltip` for
#' details on how this affects the tooltip behavior).
#' @param show_label Whether to show the label text. If `FALSE` (the default),
#' only the icon is shown (if provided). If `TRUE`, the label text is shown
#' alongside the icon.
#' @param tooltip Tooltip text to display when hovering over the input. Can be:
#' * `TRUE` (default when `show_label = FALSE`) - shows a tooltip with the
#' `label` text
#' * `FALSE` (default when `show_label = TRUE`) - no tooltip
#' * A character string - shows a tooltip with custom text
#' Defaults to `!show_label`.
#' * A character string - shows a tooltip with custom text Defaults to
#' `!show_label`.
#' @param ... Additional attributes to pass to the button.
#' @param disabled If `TRUE`, the button will not be clickable. Use
#' [shiny::updateActionButton()] to dynamically enable/disable the button.
Expand Down Expand Up @@ -111,15 +112,29 @@ toolbar_input_button <- function(

label_id <- paste0("btn-label-", p_randomInt(1000, 10000))

# We hide the label visually if `!show_label` but keep the label field for
# use with `aria-labelledby`. This ensures that ARIA will always use the
# label text. We found that screen readers will read out the icon's `aria-
# label` even if it is a descendent of an element with `aria-hidden=true`.
label_elem <- span(
id = label_id,
class = "bslib-toolbar-label",
hidden = if (!show_label) NA else NULL,
label
)

# And we wrap the icon to ensure that it is always treated as decorative
icon_elem <- span(
class = "bslib-toolbar-icon",
`aria-hidden` = "true",
style = "pointer-events: none",
icon,
)

button <- shiny::actionButton(
id,
# We hide the label visually if `!show_label` but keep the label field for
# use with `aria-labelledby`. This ensures that ARIA will always use the
# label text. We found that screen readers will read out the icon's `aria-
# label` even if it is a descendent of an element with `aria-hidden=true`.
label = span(id = label_id, hidden = if (!show_label) NA else NULL, label),
# And we wrap the icon to ensure that it is always treated as decorative
icon = span(icon, `aria-hidden` = "true", style = "pointer-events: none"),
label = label_elem,
icon = icon_elem,
disabled = disabled,
class = "bslib-toolbar-input-button btn-sm",
class = if (!border) "border-0" else "border-1",
Expand All @@ -143,3 +158,263 @@ toolbar_input_button <- function(

button
}

#' Toolbar Input Select
#'
#' @description
#' Create a select list input control that can be used to choose a single
#' item from a list of values, suitable for use within a [toolbar()].
#'
#' @examplesIf rlang::is_interactive()
#' toolbar(
#' align = "right",
#' toolbar_input_select(
#' id = "select",
#' label = "Choose option",
#' choices = c("Option 1", "Option 2", "Option 3"),
#' selected = "Option 2"
#' )
#' )
#'
#' # With custom tooltip
#' toolbar(
#' align = "right",
#' toolbar_input_select(
#' id = "select",
#' label = "Choose option",
#' choices = c("Option 1", "Option 2", "Option 3"),
#' tooltip = "Select your preferred option from the list"
#' )
#' )
#'
#' # With icon and tooltip
#' toolbar(
#' align = "right",
#' toolbar_input_select(
#' id = "select",
#' label = "Choose option",
#' choices = c("Option 1", "Option 2", "Option 3"),
#' icon = shiny::icon("filter"),
#' tooltip = "Filter the data"
#' )
#' )
#'
#' @param selected The initially selected value. If not provided, the first
#' choice will be selected by default.
#' @param ... Additional named arguments passed as attributes to the outer
#' container div.
#' @inheritParams toolbar_input_button
#' @inheritParams shiny::selectInput
#'
#' @return Returns a select input control suitable for use in a toolbar.
#'
#' @family Toolbar components
#' @export
toolbar_input_select <- function(
id,
label,
choices,
...,
selected = NULL,
icon = NULL,
show_label = FALSE,
tooltip = !show_label
) {
# Validate that ... contains only named arguments
dots <- separate_arguments(...)
if (length(dots$children) > 0) {
rlang::abort("All arguments in `...` must be named.")
}

# Validate that label is a non-empty string
# TODO: Use `check_string()`
if (!is.character(label) || length(label) != 1 || !nzchar(trimws(label))) {
rlang::abort("`label` must be a non-empty string.")
}

# Restore input for bookmarking
selected <- shiny::restoreInput(id = id, default = selected)

# Set selected to the first choice if no default or restored value
firstChoice <- asNamespace("shiny")[["firstChoice"]]
if (is.null(selected)) {
selected <- firstChoice(choices)
}

# Normalize choices using util function imported from Shiny
choicesWithNames <- asNamespace("shiny")[["choicesWithNames"]]
choices <- choicesWithNames(choices)

select_tag <- tags$select(
id = id,
class = "form-select form-select-sm",
selectOptions(choices, selected, inputId = id)
)

# Add optional icon before the select
icon_elem <- span(
icon,
style = "pointer-events: none",
class = "bslib-toolbar-icon",
`aria-hidden` = "true",
`role` = "none",
tabindex = "-1"
)

label_elem <- tags$label(
# shiny::selectInput() append `-label` to id for the label `for` attribute
id = sprintf("%s-label", id),
class = "control-label",
`for` = id,
icon_elem,
tags$span(
class = "bslib-toolbar-label",
class = if (!show_label) "visually-hidden",
label
)
)

if (isTRUE(tooltip)) {
# If tooltip is literally TRUE, use the label as the tooltip text, but hide
# it from screen readers since it repeats the label content.
tooltip <- tags$span(label, `aria-hidden` = "true")
}
if (isFALSE(tooltip)) {
tooltip <- NULL
}
if (!is.null(tooltip)) {
select_tag <- bslib::tooltip(
select_tag,
tooltip,
placement = "bottom"
)
}

div(
class = "bslib-toolbar-input-select shiny-input-container",
!!!dots$attribs,
label_elem,
select_tag
)
}

# This function was copied from shiny's `input-select.R` with a small change
selectOptions <- function(
choices,
selected = NULL,
inputId,
perfWarning = FALSE
) {
if (length(choices) >= 1000) {
# CHANGED: This warning differs to remove mention of sever-side options
rlang::warn(
sprintf(
paste0(
"Select input `%s` contains a large number of options; ",
"this may cause performance issues."
),
inputId
)
)
}

html <- mapply(
choices,
names(choices),
FUN = function(choice, label) {
if (is.list(choice)) {
# If sub-list, create an optgroup and recurse into the sublist
sprintf(
'<optgroup label="%s">\n%s\n</optgroup>',
htmlEscape(label, TRUE),
selectOptions(choice, selected, inputId, perfWarning)
)
} else {
# If single item, just return option string
sprintf(
'<option value="%s"%s>%s</option>',
htmlEscape(choice, TRUE),
if (choice %in% selected) " selected" else "",
htmlEscape(label)
)
}
}
)

HTML(paste(html, collapse = "\n"))
}

#' Toolbar: Add a divider to a toolbar
#'
#' @description
#' `toolbar_divider()` creates a visual divider line with customizable width
#' and spacing between toolbar elements.
#'
#' @param width A CSS length unit specifying the width of the divider line.
#' Defaults to `"2px"` for a sensible dividing line. Pass `0px` for no
#' divider line.
#' @param gap A CSS length unit defining the spacing around the divider.
#' Defaults to `"1rem"` for sensible fixed spacing.
#'
#' @examplesIf rlang::is_interactive()
#' toolbar(
#' toolbar_input_button(id = "left1", label = "Left"),
#' toolbar_divider(),
#' toolbar_input_button(id = "right1", label = "Right")
#' )
#'
#' toolbar(
#' toolbar_input_button(id = "a", label = "A"),
#' toolbar_divider(width = "5px", gap = "20px"),
#' toolbar_input_button(id = "b", label = "B")
#' )
#'
#' @family Toolbar components
#' @export
toolbar_divider <- function(..., width = NULL, gap = NULL) {
rlang::check_dots_empty()
width <- validateCssUnit(width)
gap <- validateCssUnit(gap)

div(
class = "bslib-toolbar-divider",
style = css(
# Sets the overall width of divider space
`--_divider-gap` = gap,
# Sets the width of the pseudo-element divider line, defaults to 2px
`--_divider-width` = width
),
`aria-hidden` = "true"
)
}


#' Add toolbar switch input
#'
#' @description
#' A switch input designed to fit well in small places such as in a [toolbar()].
#' #' toolbar_input_switch(id = "toggle", label = "Enable", value = TRUE),
#' toolbar_input_switch(id = "switch", value = FALSE)
#' )
#'
#' @param id The input ID.
#' @param label The label to display next to the switch. If `NULL`, no label
#' is displayed.
#' @param value The initial state of the switch. Default is `FALSE`.
#'
#' @return Returns a switch input suitable for use in a toolbar.
#'
#' @family Toolbar components
#' @export
toolbar_input_switch <- function(
id,
label = NULL,
value = FALSE
) {
input_switch(
id = id,
label = label,
value = value,
width = NULL
)
}
2 changes: 1 addition & 1 deletion inst/components/dist/components.css

Large diffs are not rendered by default.

Loading