Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
14 changes: 9 additions & 5 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# shiny (development version)

## New features and improvements
## New features

* `textInput()`, `textAreaInput()`, `numericInput()` and `passwordInput()` all gain an `updateOn` option. `updateOn = "change"` is the default and previous behavior, where the input value updates immediately whenever the value changes. With `updateOn = "blur"`, the input value will update only when the text input loses focus or when the user presses Enter (or Cmd/Ctrl + Enter for `textAreaInput()`). (#4183)

* `textAreaInput()` gains a `autoresize` option, which automatically resizes the text area to fit its content.
Comment thread
cpsievert marked this conversation as resolved.
Outdated

## Improvements

* When auto-reload is enabled, Shiny now reloads the entire app when support files, like Shiny modules, additional script files, or web assets, change. To enable auto-reload, call `devmode(TRUE)` to enable Shiny's developer mode, or set `options(shiny.autoreload = TRUE)` to specifically enable auto-reload. You can choose which files are watched for changes with the `shiny.autoreload.pattern` option. (#4184)

* When busy indicators are enabled (i.e., `useBusyIndicators()`), Shiny now:
* Shows a spinner on recalculating htmlwidgets that have previously rendered an error (including `req()` and `validate()`). (#4172)
Expand All @@ -11,10 +19,6 @@

* Shiny's Typescript assets are now compiled to ES2021 instead of ES5. (#4066)

* `textInput()`, `textAreaInput()`, `numericInput()` and `passwordInput()` all gain an `updateOn` option. `updateOn = "change"` is the default and previous behavior, where the input value updates immediately whenever the value changes. With `updateOn = "blur"`, the input value will update only when the text input loses focus or when the user presses Enter (or Cmd/Ctrl + Enter for `textAreaInput()`). (#4183)

* When auto-reload is enabled, Shiny now reloads the entire app when support files, like Shiny modules, additional script files, or web assets, change. To enable auto-reload, call `devmode(TRUE)` to enable Shiny's developer mode, or set `options(shiny.autoreload = TRUE)` to specifically enable auto-reload. You can choose which files are watched for changes with the `shiny.autoreload.pattern` option. (#4184)

## Bug fixes

* Fixed a bug with modals where calling `removeModal()` too quickly after `showModal()` would fail to remove the modal if the remove modal message was received while the modal was in the process of being revealed. (#4173)
Expand Down
26 changes: 17 additions & 9 deletions R/input-textarea.R
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
#' @param resize Which directions the textarea box can be resized. Can be one of
#' `"both"`, `"none"`, `"vertical"`, and `"horizontal"`. The default, `NULL`,
#' will use the client browser's default setting for resizing textareas.
#' @param autoresize If `TRUE`, the textarea will automatically resize to fit
#' the input text.
#' @return A textarea input control that can be added to a UI definition.
#'
#' @family input elements
Expand Down Expand Up @@ -52,6 +54,7 @@ textAreaInput <- function(
placeholder = NULL,
resize = NULL,
...,
autoresize = FALSE,
updateOn = c("change", "blur")
) {
rlang::check_dots_empty()
Expand All @@ -63,22 +66,27 @@ textAreaInput <- function(
resize <- match.arg(resize, c("both", "none", "vertical", "horizontal"))
}

style <- css(
# The width is specified on the parent div.
width = if (!is.null(width)) "100%",
height = validateCssUnit(height),
resize = resize
)
classes <- c("shiny-input-textarea", "form-control")
if (autoresize) {
classes <- c(classes, "textarea-autoresize")
if (is.null(rows)) {
rows <- 1
}
}

div(
class = "form-group shiny-input-container",
style = css(width = validateCssUnit(width)),
shinyInputLabel(inputId, label),
style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
tags$textarea(
id = inputId,
class = "shiny-input-textarea form-control",
class = classes,
placeholder = placeholder,
style = style,
style = css(
width = if (!is.null(width)) "100%",
height = validateCssUnit(height),
resize = resize
),
rows = rows,
cols = cols,
`data-update-on` = updateOn,
Expand Down
49 changes: 49 additions & 0 deletions inst/www/shared/shiny.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions inst/www/shared/shiny.js.map

Large diffs are not rendered by default.

36 changes: 18 additions & 18 deletions inst/www/shared/shiny.min.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions inst/www/shared/shiny.min.js.map

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions inst/www/shared/shiny_scss/shiny.scss
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,14 @@ html.autoreload-enabled #shiny-disconnected-overlay.reloading {
width: 100%;
}

/* Styling for inputTextArea(autoresize=TRUE) */
.textarea-autoresize textarea.form-control {
padding: 5px 8px;
resize: none;
overflow-y: hidden;
height: auto;
}


#shiny-notification-panel {
position: fixed;
Expand Down
89 changes: 87 additions & 2 deletions srcts/src/bindings/input/textarea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,96 @@ import $ from "jquery";

import { TextInputBinding } from "./text";

class TextareaInputBinding extends TextInputBinding {
export class TextareaInputBinding extends TextInputBinding {
find(scope: HTMLElement): JQuery<HTMLElement> {
// Inputs now also have the .shiny-input-textarea class
return $(scope).find("textarea");
}
}

export { TextareaInputBinding };
/*************************************************************
* Code below this point is for textAreaInput(autoresize=TRUE)
************************************************************/

interface DOMEvent<T extends EventTarget> extends Event {
readonly target: T;
}

function onDelegatedEvent(
eventName: string,
selector: string,
callback: (target: HTMLTextAreaElement) => void
) {
document.addEventListener(eventName, (e) => {
const e2 = e as DOMEvent<HTMLTextAreaElement>;
if (e2.target.matches(selector)) {
callback(e2.target);
}
});
}

// Use a single intersectionObserver as they are slow to create / use.
let textAreaIntersectionObserver: IntersectionObserver | null = null;

function callUpdateHeightWhenTargetIsVisible(target: HTMLTextAreaElement) {
if (textAreaIntersectionObserver === null) {
// Create a single observer to watch for the textarea becoming visible
textAreaIntersectionObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
// Quit if the entry is not visible
if (!entry.isIntersecting) {
return;
}
// If the entry is visible (even if it's just a single pixel)
// Stop observing the target
textAreaIntersectionObserver?.unobserve(entry.target);

// Update the height of the textarea
updateHeight(entry.target as HTMLTextAreaElement);
});
});
}

textAreaIntersectionObserver.observe(target);
}

function updateHeight(target: HTMLTextAreaElement) {
if (target.scrollHeight > 0) {
// Automatically resize the textarea to fit its content.
target.style.height = "auto";
target.style.height = target.scrollHeight + "px";
} else {
// The textarea is not visible on the page, therefore it has a 0 scroll height.

// If we should autoresize the text area height, then we can wait for the textarea to
// become visible and call `updateHeight` again. Hopefully the scroll height is no
// longer 0
callUpdateHeightWhenTargetIsVisible(target);
}
}

// Update on change
onDelegatedEvent(
"input",
"textarea.textarea-autoresize",
(target: HTMLTextAreaElement) => {
updateHeight(target);
}
);

// Update on load
function updateOnLoad() {
if (document.readyState === "loading") {
// Document still loading, wait 10ms to check again.
setTimeout(updateOnLoad, 10);
return;
}

// document.readyState in ["interactive", "complete"];\
const textAreas = document.querySelectorAll(
"textarea.textarea-autoresize"
) as NodeListOf<HTMLTextAreaElement>;
textAreas.forEach(updateHeight);
}

updateOnLoad();
Comment thread
cpsievert marked this conversation as resolved.
Outdated