Skip to content

Commit ba1be9a

Browse files
cpsievertgadenbuieCopilot
authored
feat: Add input_submit_textarea() input element (#1204)
* feat(input_submit_button, input_submit_textarea): Add new input submit components * `air format` (GitHub Actions) * Resave distributed files (GitHub Action) * Resave data (GitHub Action) * Update to latest Shiny * shiny remote no longer needed * `usethis::use_tidy_description()` (GitHub Actions) * Resave distributed files (GitHub Action) * Resave data (GitHub Action) * Update website deps (GitHub Action) * Bump shiny requirement * Pass on adding input_submit_button() (at least for now) * Make function signature more consistent with other inputs * Support HTML label updates * Add autoresize logic * Fix shiny requirement * `usethis::use_tidy_description()` (GitHub Actions) * Resave data (GitHub Action) * Update website deps (GitHub Action) * Avoid non-ASCII characters * CSS tweaks to better align with chat input styles * `usethis::use_tidy_description()` (GitHub Actions) * `devtools::document()` (GitHub Actions) * Update website deps (GitHub Action) * Doc improvements * Strict equality check * Better scss file name * `usethis::use_tidy_description()` (GitHub Actions) * Update website deps (GitHub Action) * Platform specific modifier in button label * Get check passing * `usethis::use_tidy_description()` (GitHub Actions) * `yarn build` (GitHub Actions) * Update website deps (GitHub Action) * Add to reference * `usethis::use_tidy_description()` (GitHub Actions) * Update website deps (GitHub Action) * Update R/input-submit.R Co-authored-by: Garrick Aden-Buie <[email protected]> * Update inst/components/scss/input_submit_textarea.scss Co-authored-by: Garrick Aden-Buie <[email protected]> * `usethis::use_tidy_description()` (GitHub Actions) * `devtools::document()` (GitHub Actions) * Resave distributed files (GitHub Action) * Update website deps (GitHub Action) * Import label markup logic from shiny * Simplify markup * `air format` (GitHub Actions) * `usethis::use_tidy_description()` (GitHub Actions) * Update website deps (GitHub Action) * Introduce a modifier key container; add explicit class to button * `usethis::use_tidy_description()` (GitHub Actions) * Update website deps (GitHub Action) * Revert "Simplify markup" This reverts commit 86eec98. * Change default to enter+modifier * `usethis::use_tidy_description()` (GitHub Actions) * Resave data (GitHub Action) * Update website deps (GitHub Action) * Move the component identifier CSS class to top-level * Update documentation * Manually insert new lines for Alt+Enter; Query submit button everytime we need it; refactor JS logic * Improved styling/logic for submit key hint * Allow children/attributes to be passed through ...; add rows argument; refactor/simplify CSS rules * Better busy UI Co-authored-by: Garrick Aden-Buie <[email protected]> * Rename top-level .bslib-submit-textarea to .bslib-input-submit-textarea * `usethis::use_tidy_description()` (GitHub Actions) * Resave distributed files (GitHub Action) * Resave data (GitHub Action) * Update website deps (GitHub Action) * Add toolbar argument; require ... to be named; add gap between toolbar elements * Focus textarea when (inner) container is clicked * Delete unused code * `air format` (GitHub Actions) * `usethis::use_tidy_description()` (GitHub Actions) * Update website deps (GitHub Action) * Address feedback * chore: organize Rd content * fix: support trailing comma * Add some basic snapshot tests * Apply suggestions from code review Co-authored-by: Copilot <[email protected]> * `air format` (GitHub Actions) * `devtools::document()` (GitHub Actions) * `yarn build` (GitHub Actions) * Resave data (GitHub Action) --------- Co-authored-by: cpsievert <[email protected]> Co-authored-by: Garrick Aden-Buie <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 7503f2e commit ba1be9a

File tree

20 files changed

+1098
-20
lines changed

20 files changed

+1098
-20
lines changed

DESCRIPTION

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Suggests:
5252
magrittr,
5353
rappdirs,
5454
rmarkdown (>= 2.7),
55-
shiny (> 1.8.1),
55+
shiny (>= 1.11.1),
5656
testthat,
5757
thematic,
5858
tools,
@@ -99,6 +99,7 @@ Collate:
9999
'fill.R'
100100
'imports.R'
101101
'input-dark-mode.R'
102+
'input-submit.R'
102103
'input-switch.R'
103104
'layout.R'
104105
'nav-items.R'

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export(font_face)
8787
export(font_google)
8888
export(font_link)
8989
export(input_dark_mode)
90+
export(input_submit_textarea)
9091
export(input_switch)
9192
export(input_task_button)
9293
export(is.card_item)
@@ -153,6 +154,7 @@ export(toggle_switch)
153154
export(toggle_tooltip)
154155
export(tooltip)
155156
export(update_popover)
157+
export(update_submit_textarea)
156158
export(update_switch)
157159
export(update_task_button)
158160
export(update_tooltip)

NEWS.md

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

3+
## New features
4+
5+
* Added a new `input_submit_textarea()` input element, which is similar 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+
37
## Improvements and bug fixes
48

59
* `bs_theme_dependencies()` now avoids unnecessarily 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: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
#' Create a textarea input control with explicit submission
2+
#'
3+
#' Creates a textarea input where users can enter multi-line text and submit
4+
#' their input using a dedicated button or keyboard shortcut. This control is
5+
#' ideal when you want to capture finalized input, rather than reacting to every
6+
#' keystroke, making it useful for chat boxes, comments, or other scenarios
7+
#' where users may compose and review their text before submitting.
8+
#'
9+
#' @section Server value:
10+
#' The server receives a character string containing the user's text input.
11+
#'
12+
#' **Important:** The initial server value is always `""` (empty string),
13+
#' regardless of any `value` parameter provided to `input_submit_textarea()`.
14+
#' The server value updates only when the user explicitly submits the input by
15+
#' either pressing the Enter key (possibly with a modifier key) or clicking the
16+
#' submit button.
17+
#'
18+
#' @param id The input ID.
19+
#' @param label The label to display above the input control. If `NULL`, no
20+
#' label is displayed.
21+
#' @param ... Additional attributes to apply to the underlying `<textarea>`
22+
#' element (e.g., spellcheck, autocomplete, etc).
23+
#' @param placeholder The placeholder text to display when the input is empty.
24+
#' This can be used to provide a hint or example of the expected input.
25+
#' @param value The initial input text. Note that, unlike [shiny::textAreaInput()],
26+
#' this won't set a server-side value until the value is explicitly submitted.
27+
#' @param width Any valid CSS unit (e.g., `width="100%"`).
28+
#' @param rows The number of rows (i.e., height) of the textarea. This essentially
29+
#' sets the minimum height -- the textarea can grow taller as the user
30+
#' enters more text.
31+
#' @param button A [htmltools::tags] element to use for the submit button. It's recommended
32+
#' that this be a [input_task_button()] since it will automatically provide a
33+
#' busy indicator (and disable) until the next flush occurs. Note also that if
34+
#' the submit button launches a [shiny::ExtendedTask], this button can also be bound
35+
#' to the task ([bind_task_button()]) and/or manually updated for more
36+
#' accurate progress reporting ([update_task_button()]).
37+
#' @param toolbar A list of optional UI elements (e.g., links, icons) to
38+
#' display next to the submit button.
39+
#' @param submit_key A character string indicating what keyboard event should
40+
#' trigger the submit button. The default is `enter+modifier`, which requires
41+
#' the user to hold down Ctrl (or Cmd on Mac) before pressing Enter to
42+
#' submit. This helps prevent accidental submissions. To allow submission with
43+
#' just the Enter key, use `enter`. In this case, the user can still insert
44+
#' new lines using Shift+Enter or Alt+Enter.
45+
#'
46+
#' @return A textarea input control that can be added to a UI definition.
47+
#' @seealso [update_submit_textarea()], [input_task_button()]
48+
#'
49+
#' @examplesIf rlang::is_interactive()
50+
#' library(shiny)
51+
#' library(bslib)
52+
#'
53+
#' ui <- page_fluid(
54+
#' input_submit_textarea("text", placeholder = "Enter some input..."),
55+
#' verbatimTextOutput("value")
56+
#' )
57+
#' server <- function(input, output) {
58+
#' output$value <- renderText({
59+
#' req(input$text)
60+
#' Sys.sleep(2)
61+
#' paste("You entered:", input$text)
62+
#' })
63+
#' }
64+
#' shinyApp(ui, server)
65+
#'
66+
#' @export
67+
input_submit_textarea <- function(
68+
id,
69+
label = NULL,
70+
...,
71+
placeholder = NULL,
72+
value = "",
73+
width = "min(680px, 100%)",
74+
rows = 1,
75+
button = NULL,
76+
toolbar = NULL,
77+
submit_key = c("enter+modifier", "enter")
78+
) {
79+
rlang::check_installed("shiny", version = "1.11.1")
80+
args <- rlang::list2(...)
81+
if (any_unnamed(args)) {
82+
abort(c(
83+
"All `...` arguments must be named",
84+
"i" = "Did you mean to pass UI elements to `toolbar`?"
85+
))
86+
}
87+
88+
value <- shiny::restoreInput(id = id, default = value)
89+
if (length(value) != 1 || !is.character(value)) {
90+
abort("`value` must be a character string")
91+
}
92+
93+
submit_key <- rlang::arg_match(submit_key)
94+
needs_modifier <- isTRUE(submit_key == "enter+modifier")
95+
96+
if (is.null(button)) {
97+
button <- input_task_button(
98+
id = paste0(id, "_submit"),
99+
class = "btn-sm",
100+
label = span(class = "bslib-submit-key", "\U23CE"),
101+
icon = "Submit",
102+
label_busy = div(
103+
class = "spinner-border spinner-border-sm ms-2",
104+
role = "status",
105+
span(class = "visually-hidden", "Processing...")
106+
),
107+
icon_busy = "Submit",
108+
title = "Press Enter to Submit",
109+
`aria-label` = "Press Enter to Submit"
110+
)
111+
}
112+
113+
if (!is_button_tag(button)) {
114+
abort("`button` must be a `tags$button()`")
115+
}
116+
117+
button <- tagAppendAttributes(button, class = "bslib-submit-textarea-btn")
118+
119+
div(
120+
class = "bslib-input-submit-textarea shiny-input-container bslib-mb-spacing",
121+
style = css(
122+
# TODO: validateCssUnit() needs to handle more complex CSS
123+
width = if (is.numeric(width)) paste0(width, "px") else width,
124+
),
125+
shiny_input_label(id, label),
126+
div(
127+
class = "bslib-submit-textarea-container",
128+
tags$textarea(
129+
id = id,
130+
class = "form-control",
131+
style = css(width = if (!is.null(width)) "100%"),
132+
placeholder = placeholder,
133+
`data-needs-modifier` = if (needs_modifier) "",
134+
rows = rows,
135+
!!!args,
136+
value
137+
),
138+
tags$footer(
139+
div(toolbar, class = "bslib-toolbar"),
140+
button
141+
)
142+
)
143+
)
144+
}
145+
146+
is_button_tag <- function(x) {
147+
if (!inherits(x, "shiny.tag")) {
148+
return(FALSE)
149+
}
150+
151+
isTRUE(x$name == "button") ||
152+
isTRUE(x$attribs$type == "button")
153+
}
154+
155+
#' @param value The value to set the user input to.
156+
#' @param placeholder The placeholder text for the user input.
157+
#' @param submit Whether to automatically submit the text for the user. Requires `value`.
158+
#' @param focus Whether to move focus to the input element. Requires `value`.
159+
#' @param session The `session` object; using the default is recommended.
160+
#'
161+
#' @rdname input_submit_textarea
162+
#' @export
163+
update_submit_textarea <- function(
164+
id,
165+
...,
166+
value = NULL,
167+
placeholder = NULL,
168+
label = NULL,
169+
submit = FALSE,
170+
focus = FALSE,
171+
session = get_current_session()
172+
) {
173+
rlang::check_dots_empty()
174+
175+
if (is.null(value) && (submit || focus)) {
176+
stop(
177+
"An input `value` must be provided when `submit` or `focus` are `TRUE`.",
178+
call. = FALSE
179+
)
180+
}
181+
182+
message <- dropNulls(list(
183+
value = value,
184+
placeholder = placeholder,
185+
label = if (!is.null(label)) processDeps(label, session),
186+
submit = submit,
187+
focus = focus
188+
))
189+
190+
session$sendInputMessage(id, message)
191+
}

R/sysdata.rda

-41 Bytes
Binary file not shown.

R/utils-shiny.R

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,8 @@ anyNamed <- function(x) {
6464
}
6565
any(nzchar(nms))
6666
}
67+
68+
69+
shiny_input_label <- function(id, label = NULL) {
70+
getFromNamespace("shinyInputLabel", "shiny")(id, label)
71+
}

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)