Skip to content

Commit 693b87c

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

18 files changed

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

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)