Skip to content

Commit a4344a5

Browse files
committed
feat(input_submit_button, input_submit_textarea): Add new input submit components
1 parent 7cf8050 commit a4344a5

18 files changed

+948
-22
lines changed

DESCRIPTION

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,15 @@ Suggests:
5151
magrittr,
5252
rappdirs,
5353
rmarkdown (>= 2.7),
54-
shiny (> 1.8.1),
54+
shiny (> 1.10.0.9000),
5555
testthat,
5656
thematic,
5757
tools,
5858
utils,
5959
withr,
6060
yaml
61+
Remotes:
62+
rstudio/shiny@feat/inputSubmitText
6163
Config/Needs/deploy:
6264
BH,
6365
chiflights22,
@@ -138,6 +140,7 @@ Collate:
138140
'fill.R'
139141
'imports.R'
140142
'input-dark-mode.R'
143+
'input-submit.R'
141144
'input-switch.R'
142145
'layout.R'
143146
'nav-items.R'

NAMESPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ export(font_face)
8787
export(font_google)
8888
export(font_link)
8989
export(input_dark_mode)
90+
export(input_submit_button)
91+
export(input_submit_textarea)
9092
export(input_switch)
9193
export(input_task_button)
9294
export(is.card_item)
@@ -153,6 +155,7 @@ export(toggle_switch)
153155
export(toggle_tooltip)
154156
export(tooltip)
155157
export(update_popover)
158+
export(update_submit_textarea)
156159
export(update_switch)
157160
export(update_task_button)
158161
export(update_tooltip)

NEWS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# bslib (development version)
22

3+
## New features
4+
5+
* Added a new `input_submit_textarea()` function. This input is similar in nature to `shiny::textAreaInput()`, but includes a submit button to only submit the text changes to the server on click. This is especially useful when the input text change triggers a long-running operation and/or the user wants to type longer-form input and review it before submitting it.
6+
7+
* Added a new `input_submit_button()` function. This input is similar in nature to `shiny::submitButton()`, but is more flexible and provides more visual feedback (i.e., progress). More specifically, it enables multiple submit buttons (each targetting a different set of input controls) in a single Shiny app.
8+
39
## Improvements and bug fixes
410

511
* `bs_theme_dependencies()` now avoids unecessarily copying internal package files to R's temporary directory more than once when preparing precompiled theme dependencies (e.g. for a standard `bs_theme()` theme). (#1184)

R/input-submit.R

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
#' Suspend input changes until a button is clicked
2+
#'
3+
#' Suspend changes to a particular set of input controls until a submit button
4+
#' is clicked. This is particularly useful for allowing the user to review their
5+
#' input(s) before sending them to the server for a potentially expensive
6+
#' operation. Note that, by default, all inputs that are children of the
7+
#' button's parent are deferred until the button is clicked. This can be changed
8+
#' by setting the `scope` argument to a CSS selector that matches the container
9+
#' of inputs you wish to suspend.
10+
#'
11+
#' @param id The input ID.
12+
#' @param label A label to place on the button.
13+
#' @param ... Arguments passed along to [input_task_button()].
14+
#' @param scope The scope of the submit button. Can be one of the following:
15+
#' - `NULL`: Inputs that are children of the button's parent are
16+
#' deferred until the button is clicked.
17+
#' - A CSS selector: Only inputs that are within the element matching the
18+
#' selector are deferred until the button is clicked.
19+
#'
20+
#' @seealso [input_submit_textarea()], [input_task_button()]
21+
#' @export
22+
input_submit_button <- function(id, label, ..., scope = NULL) {
23+
btn <- input_task_button(id, label, ...)
24+
25+
# Change type from "button" to "submit"
26+
btn$attribs$type <- "submit"
27+
28+
tagAppendAttributes(
29+
btn,
30+
class = "bslib-submit-button",
31+
`data-submit-scope` = scope
32+
)
33+
}
34+
35+
36+
# TODO: maybe update_task_button() should gain label/icon arguments
37+
# and then we can just call that here? Or just tell people to use
38+
# update_task_button() directly?
39+
40+
## @param id The input ID.
41+
## @param ... Currently ignored.
42+
## @param label The label of the button.
43+
## @param icon An optional icon to display next to the label while the button
44+
## is in ready state. See [fontawesome::fa_i()].
45+
## @param session The `session` object; using the default is recommended.
46+
## @rdname input_submit_button
47+
## @export
48+
#update_submit_button <- function(
49+
# id,
50+
# ...,
51+
# label = NULL,
52+
# icon = NULL,
53+
# session = get_current_session()
54+
#) {
55+
#
56+
#}
57+
58+
59+
60+
61+
#' Create a textarea input control with explicit submission
62+
#'
63+
#' Creates a textarea input where users can enter multi-line text and submit
64+
#' their input using a dedicated button or keyboard shortcut. This control is
65+
#' ideal when you want to capture finalized input, rather than reacting to every
66+
#' keystroke, making it useful for chat boxes, comments, or other scenarios
67+
#' where users may compose and review their text before submitting.
68+
#'
69+
#' @param id The input ID.
70+
#' @param placeholder A character string giving the user a hint as to what can
71+
#' be entered into the control.
72+
#' @param value The initial input text. Note that, unlike [textAreaInput()],
73+
#' this won't set a server-side value until the value is submitted.
74+
#' @param button A [tags] element to use for the submit button. It's recommended
75+
#' that this be a [input_task_button()] since it will automatically provide a
76+
#' busy indicator (and disable) until the next flush occurs. Note also that if
77+
#' the submit button launches a [ExtendedTask], this button can also be bound
78+
#' to the task ([bind_task_button()]) and/or manually updated for more
79+
#' accurate progress reporting ([update_task_button()]).
80+
#' @param submit_key A character string indicating what keyboard event should
81+
#' trigger the submit button. The default is `enter`, which will submit the
82+
#' input when the user presses the Enter/Return key. The `enter+modifier`
83+
#' option will submit the input when the user presses the Enter key while
84+
#' holding down Ctrl/Cmd.
85+
#'
86+
#' @return A textarea input control that can be added to a UI definition.
87+
#'
88+
#' @seealso [input_submit_button()], [input_task_button()]
89+
#'
90+
#' @examplesIf rlang::is_interactive()
91+
#'
92+
#' ui <- page_fluid(
93+
#' input_submit_textarea("text", "Enter some input..."),
94+
#' verbatimTextOutput("value")
95+
#' )
96+
#' server <- function(input, output) {
97+
#' output$value <- renderText({
98+
#' req(input$text)
99+
#' Sys.sleep(2)
100+
#' paste("You entered:", input$text)
101+
#' })
102+
#' }
103+
#' shinyApp(ui, server)
104+
#'
105+
#' @section Server value:
106+
#' A character string of the text input. The default value is `""` even if
107+
#' `value` is provided. The value will only be set/updated when the user submits
108+
#' the input by pressing the Enter key or clicking the submit button.
109+
#'
110+
#' @export
111+
input_submit_textarea <- function(
112+
id,
113+
placeholder,
114+
value = "",
115+
...,
116+
button = NULL,
117+
label = NULL,
118+
width = "min(600px, 100%)",
119+
submit_key = c("enter", "enter+modifier")
120+
) {
121+
rlang::check_dots_empty()
122+
123+
value <- shiny::restoreInput(id = id, default = value)
124+
if (length(value) != 1 || !is.character(value)) {
125+
stop("`value` must be a character string", call. = FALSE)
126+
}
127+
128+
submit_key <- rlang::arg_match(submit_key)
129+
needs_modifier <- isTRUE(submit_key == "enter+modifier")
130+
131+
if (is.null(button)) {
132+
if (needs_modifier) {
133+
btn_label <- "Submit ⌘ ⏎"
134+
btn_title <- "Press ⌘ + Enter to Submit"
135+
} else {
136+
btn_label <- "Submit ⏎"
137+
btn_title <- "Press Enter to Submit"
138+
}
139+
140+
button <- input_task_button(
141+
id = paste0(id, "_submit"),
142+
class = "btn-sm",
143+
label = btn_label,
144+
title = btn_title,
145+
`aria-label` = btn_title
146+
)
147+
}
148+
149+
if (!is_button_tag(button)) {
150+
stop("`button` must be a `tags$button()`", call. = FALSE)
151+
}
152+
153+
div(
154+
class = "shiny-input-container bslib-mb-spacing",
155+
style = css(
156+
# TODO: validateCssUnit() needs to handle more complex CSS
157+
width = if (is.numeric(width)) paste0(width, "px") else width,
158+
),
159+
shiny_input_label(id, label),
160+
div(
161+
class = "bslib-input-textsubmit",
162+
tags$textarea(
163+
id = id,
164+
class = "textarea-autoresize form-control",
165+
style = css(width = if (!is.null(width)) "100%"),
166+
placeholder = placeholder,
167+
`data-needs-modifier` = if (needs_modifier) "",
168+
rows = 1,
169+
value
170+
),
171+
button
172+
)
173+
)
174+
}
175+
176+
is_button_tag <- function(x) {
177+
if (!inherits(x, "shiny.tag")) {
178+
return(FALSE)
179+
}
180+
181+
isTRUE(x$name == "button") ||
182+
isTRUE(x$attribs$type == "button")
183+
}
184+
185+
#' @param value The value to set the user input to.
186+
#' @param placeholder The placeholder text for the user input.
187+
#' @param submit Whether to automatically submit the text for the user. Requires `value`.
188+
#' @param focus Whether to move focus to the input element. Requires `value`.
189+
#'
190+
#' @rdname input_submit_textarea
191+
#' @export
192+
update_submit_textarea <- function(
193+
id,
194+
...,
195+
value = NULL,
196+
placeholder = NULL,
197+
label = NULL,
198+
submit = FALSE,
199+
focus = FALSE,
200+
session = get_current_session()
201+
) {
202+
203+
rlang::check_dots_empty()
204+
205+
if (is.null(value) && (submit || focus)) {
206+
stop(
207+
"An input `value` must be provided when `submit` or `focus` are `TRUE`.",
208+
call. = FALSE
209+
)
210+
}
211+
212+
message <- dropNulls(list(
213+
value = value,
214+
placeholder = placeholder,
215+
label = label,
216+
submit = submit,
217+
focus = focus
218+
))
219+
220+
session$sendInputMessage(id, message)
221+
}

R/utils-shiny.R

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,15 @@ anyNamed <- function(x) {
5656
if (is.null(nms)) return(FALSE)
5757
any(nzchar(nms))
5858
}
59+
60+
61+
# Copy of shiny:::shinyInputLabel()
62+
shiny_input_label <- function(id, label = NULL) {
63+
tags$label(
64+
label,
65+
class = "control-label",
66+
class = if (is.null(label)) "shiny-label-null",
67+
id = paste0(id, "-label"),
68+
`for` = id
69+
)
70+
}

inst/components/dist/components.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)