Skip to content

Commit f237de5

Browse files
authored
Fix front-end action button label updating logic (#4242)
* Close #4239: fix front-end action button label updating logic (follow up to #3996) * Update news * Use a separator instead of putting markup in attributes * `yarn build` (GitHub Actions) * Address feedback * Cleanup * Refactor into a single method to split icon/label * `yarn build` (GitHub Actions) * Better naming * Add some padding to the separator * Add some unit tests for R logic * Update NEWS.md * Update NEWS.md * Update NEWS.md * Update NEWS.md * Increase backcompat (keep same R structure when no icon is provided) * Refine comment --------- Co-authored-by: cpsievert <[email protected]>
1 parent 8c7abba commit f237de5

File tree

15 files changed

+241
-85
lines changed

15 files changed

+241
-85
lines changed

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ Collate:
202202
'test.R'
203203
'update-input.R'
204204
'utils-lang.R'
205+
'utils-tags.R'
205206
'version_bs_date_picker.R'
206207
'version_ion_range_slider.R'
207208
'version_jquery.R'

NEWS.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
# shiny (development version)
22

3+
## New features
4+
5+
* The `icon` argument of `actionButton()`, `downloadButton()`, etc. now accepts values other than `shiny::icon()` (like `fontawesome::fa()` and `bsicons::bs_icon()`). (#4242)
6+
7+
## Improvements
8+
9+
* Padding is now provided between the `icon` and `label` of an `actionButton()`. (#4242)
310
## Bug fixes
411

5-
* Fixed a regression in v1.11.0 where `InputBinding`'s that didn't pass a value to their `subscribe` callback where to no longer working. (#4243)
12+
* Fixed a regression in v1.11.0 where `InputBinding` implementations that don't pass a value to their `subscribe` callback were no longer notifying Shiny of input changes. (#4243)
13+
14+
* `updateActionButton()` and `updateActionLink()` once again handle `label` updates correctly (which can now include HTML). (#4242)
615

716
# shiny 1.11.0
817

R/input-action.R

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,14 @@ actionButton <- function(inputId, label, icon = NULL, width = NULL,
5656

5757
value <- restoreInput(id = inputId, default = NULL)
5858

59-
tags$button(id=inputId,
59+
tags$button(
60+
id = inputId,
6061
style = css(width = validateCssUnit(width)),
61-
type="button",
62-
class="btn btn-default action-button",
62+
type = "button",
63+
class = "btn btn-default action-button",
6364
`data-val` = value,
6465
disabled = if (isTRUE(disabled)) NA else NULL,
65-
list(validateIcon(icon), label),
66+
get_action_children(label, icon),
6667
...
6768
)
6869
}
@@ -72,30 +73,52 @@ actionButton <- function(inputId, label, icon = NULL, width = NULL,
7273
actionLink <- function(inputId, label, icon = NULL, ...) {
7374
value <- restoreInput(id = inputId, default = NULL)
7475

75-
tags$a(id=inputId,
76-
href="#",
77-
class="action-button",
76+
tags$a(
77+
id = inputId,
78+
href = "#",
79+
class = "action-button",
7880
`data-val` = value,
79-
list(validateIcon(icon), label),
81+
get_action_children(label, icon),
8082
...
8183
)
8284
}
8385

86+
get_action_children <- function(label, icon) {
87+
icon <- validateIcon(icon)
8488

85-
# Check that the icon parameter is valid:
86-
# 1) Check if the user wants to actually add an icon:
87-
# -- if icon=NULL, it means leave the icon unchanged
88-
# -- if icon=character(0), it means don't add an icon or, more usefully,
89-
# remove the previous icon
90-
# 2) If so, check that the icon has the right format (this does not check whether
91-
# it is a *real* icon - currently that would require a massive cross reference
92-
# with the "font-awesome" and the "glyphicon" libraries)
89+
if (length(icon) > 0) {
90+
# The separator elements helps us distinguish between the icon and label
91+
# when dynamically updating the button/link. Ideally, we would wrap each
92+
# in a container element, but is currently done with a separator to help
93+
# minimize the chance of breaking existing code.
94+
tagList(
95+
icon,
96+
tags$span(class = "shiny-icon-separator"),
97+
label
98+
)
99+
} else {
100+
# Technically, we don't need the `icon` here, but keeping it maintains
101+
# backwards compatibility of `btn$children[[1]][[2]]` to get the label.
102+
# The shinyGovstyle package is at least one example of this.
103+
tagList(icon, label)
104+
}
105+
}
106+
107+
# Throw an informative warning if icon isn't html-ish
93108
validateIcon <- function(icon) {
94-
if (is.null(icon) || identical(icon, character(0))) {
95-
return(icon)
96-
} else if (inherits(icon, "shiny.tag") && icon$name == "i") {
109+
if (length(icon) == 0) {
97110
return(icon)
98-
} else {
99-
stop("Invalid icon. Use Shiny's 'icon()' function to generate a valid icon")
100111
}
112+
113+
if (!isTagLike(icon)) {
114+
rlang::warn(
115+
c(
116+
"It appears that a non-HTML value was provided to `icon`.",
117+
i = "Try using a `shiny::icon()` (or an equivalent) to get an icon."
118+
),
119+
class = "shiny-validate-icon"
120+
)
121+
}
122+
123+
icon
101124
}

R/update-input.R

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,9 @@ updateCheckboxInput <- function(session = getDefaultReactiveDomain(), inputId, l
181181
updateActionButton <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, icon = NULL, disabled = NULL) {
182182
validate_session_object(session)
183183

184-
if (!is.null(icon)) icon <- as.character(validateIcon(icon))
185184
message <- dropNulls(list(
186185
label = if (!is.null(label)) processDeps(label, session),
187-
icon = icon,
186+
icon = if (!is.null(icon)) processDeps(validateIcon(icon), session),
188187
disabled = disabled
189188
))
190189
session$sendInputMessage(inputId, message)

R/utils-tags.R

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Check if `x` is a tag(), tagList(), or HTML()
2+
# @param strict If `FALSE`, also consider a normal list() of 'tags' to be a tag list.
3+
isTagLike <- function(x, strict = FALSE) {
4+
isTag(x) || isTagList(x, strict = strict) || isTRUE(attr(x, "html"))
5+
}
6+
7+
isTag <- function(x) {
8+
inherits(x, "shiny.tag")
9+
}
10+
11+
isTagList <- function(x, strict = TRUE) {
12+
if (strict) {
13+
return(inherits(x, "shiny.tag.list"))
14+
}
15+
16+
if (!is.list(x)) {
17+
return(FALSE)
18+
}
19+
20+
all(vapply(x, isTagLike, logical(1)))
21+
}

inst/www/shared/shiny.js

Lines changed: 28 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

inst/www/shared/shiny.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)