From ff630e6b50f8d752c8c9a56e1a82dd65668a69c5 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 26 Apr 2023 14:15:00 -0700 Subject: [PATCH 1/3] In experimental, add ui.input_text_area with autoresize --- shiny/experimental/ui/__init__.py | 3 + shiny/experimental/ui/_input_text.py | 135 ++++++++++++++++++ .../experimental/www/textarea-autoresize.css | 6 + shiny/experimental/www/textarea-autoresize.js | 16 +++ 4 files changed, 160 insertions(+) create mode 100644 shiny/experimental/ui/_input_text.py create mode 100644 shiny/experimental/www/textarea-autoresize.css create mode 100644 shiny/experimental/www/textarea-autoresize.js diff --git a/shiny/experimental/ui/__init__.py b/shiny/experimental/ui/__init__.py index 4662bb040..3b860d14f 100644 --- a/shiny/experimental/ui/__init__.py +++ b/shiny/experimental/ui/__init__.py @@ -16,6 +16,7 @@ output_plot, output_ui, ) +from ._input_text import input_text_area __all__ = ( # Sidebar @@ -40,4 +41,6 @@ "output_image", "output_plot", "output_ui", + # input_text_area + "input_text_area", ) diff --git a/shiny/experimental/ui/_input_text.py b/shiny/experimental/ui/_input_text.py new file mode 100644 index 000000000..23e856d9c --- /dev/null +++ b/shiny/experimental/ui/_input_text.py @@ -0,0 +1,135 @@ +__all__ = ("input_text_area",) + +from pathlib import PurePath +from typing import Optional + +from htmltools import HTMLDependency, Tag, TagChild, css, div, tags + +from ..._docstring import add_example +from ..._namespaces import resolve_id +from ..._typing_extensions import Literal + + +@add_example() +def input_text_area( + id: str, + label: TagChild, + value: str = "", + *, + width: Optional[str] = None, + height: Optional[str] = None, + cols: Optional[int] = None, + rows: Optional[int] = None, + placeholder: Optional[str] = None, + resize: Optional[Literal["none", "both", "horizontal", "vertical"]] = None, + autoresize: bool = False, + autocomplete: Optional[str] = None, + spellcheck: Optional[Literal["true", "false"]] = None, +) -> Tag: + """ + Create a textarea input control for entry of unstructured text values. This is an + experimental version of shiny.ui.input_text_area that can automatically resize to + fit the input text. + + Parameters + ---------- + id + An input id. + label + An input label. + value + Initial value. + width + The CSS width, e.g. '400px', or '100%' + height + The CSS height, e.g. '400px', or '100%' + cols + Value of the visible character columns of the input, e.g. 80. This argument will + only take effect if there is not a CSS width rule defined for this element; such + a rule could come from the width argument of this function or from a containing + page layout such as :func:`~shiny.ui.page_fluid`. + rows + The value of the visible character rows of the input, e.g. 6. If the height + argument is specified, height will take precedence in the browser's rendering. + placeholder + A hint as to what can be entered into the control. + resize + Which directions the textarea box can be resized. Can be one of "both", "none", + "vertical", and "horizontal". The default, ``None``, will use the client + browser's default setting for resizing textareas. + autoresize + If True, then the textarea will automatically resize to fit the input text. + autocomplete + Whether to enable browser autocompletion of the text input (default is "off"). + If None, then it will use the browser's default behavior. Other possible values + include "on", "name", "username", and "email". See + https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete for + more. + spellcheck + Whether to enable browser spell checking of the text input (default is None). If + None, then it will use the browser's default behavior. + + Returns + ------- + : + A UI element + + Notes + ------ + .. admonition:: Server value + + A string containing the current text input. The default value is ``""`` unless + ``value`` is provided. + + See Also + ------- + ~shiny.ui.input_text + """ + + if resize and resize not in ["none", "both", "horizontal", "vertical"]: + raise ValueError("Invalid resize value: " + str(resize)) + + classes = ["form-control"] + if autoresize: + classes.append("textarea-autoresize") + if rows is None: + rows = 1 + + area = tags.textarea( + value, + id=resolve_id(id), + class_=" ".join(classes), + style=css(width=None if width else "100%", height=height, resize=resize), + placeholder=placeholder, + rows=rows, + cols=cols, + autocomplete=autocomplete, + spellcheck=spellcheck, + ) + + return div( + shiny_input_label(id, label), + area, + _autoresize_dependency() if autoresize else None, + class_="form-group shiny-input-container", + style=css(width=width), + ) + + +ex_www_path = PurePath(__file__).parent.parent / "www" + + +def _autoresize_dependency(): + return HTMLDependency( + "shiny-textarea-autoresize", + "0.0.0", + source={"package": "shiny", "subdir": str(ex_www_path)}, + script={"src": "textarea-autoresize.js"}, + stylesheet={"href": "textarea-autoresize.css"}, + ) + + +# Originally from ui._utils, but we can't seem to import ..ui._utils +def shiny_input_label(id: str, label: TagChild = None) -> Tag: + cls = "control-label" + ("" if label else " shiny-label-null") + return tags.label(label, class_=cls, id=id + "-label", for_=id) diff --git a/shiny/experimental/www/textarea-autoresize.css b/shiny/experimental/www/textarea-autoresize.css new file mode 100644 index 000000000..f97cff187 --- /dev/null +++ b/shiny/experimental/www/textarea-autoresize.css @@ -0,0 +1,6 @@ +.textarea-autoresize textarea.form-control { + padding: 5px 8px; + resize: none; + overflow-y: hidden; + height: auto; +} diff --git a/shiny/experimental/www/textarea-autoresize.js b/shiny/experimental/www/textarea-autoresize.js new file mode 100644 index 000000000..f9766ba29 --- /dev/null +++ b/shiny/experimental/www/textarea-autoresize.js @@ -0,0 +1,16 @@ +(() => { + function onDelegatedEvent(eventName, selector, callback) { + document.addEventListener(eventName, (e) => { + if (e.target.matches(selector)) { + callback(e); + } + }); + } + + onDelegatedEvent("input", "textarea.textarea-autoresize", (e) => { + const { target } = e; + // Automatically resize the textarea to fit its content. + target.style.height = "auto"; + target.style.height = target.scrollHeight + "px"; + }); +})(); From 0f5d962dbc4d3b5a9cfadada65843b4d07cecfa4 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 26 Apr 2023 14:24:23 -0700 Subject: [PATCH 2/3] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dd238bfc..cf510f395 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * The bootstrap HTMLDependency is now created using the dev version of `{bslib}` to get the latest features. (#462) +* Added `shiny.experimental.ui.input_text_area()`, which supports auto-resizing height to fit the content when `autoresize=True`. (#463) + ### Bug fixes ### Other changes From 0e698782359642021d289f3580a3ef5db559a724 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 26 Apr 2023 14:25:08 -0700 Subject: [PATCH 3/3] Bump version to 0.3.2.9002 --- shiny/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shiny/__init__.py b/shiny/__init__.py index 8314da029..91d624587 100644 --- a/shiny/__init__.py +++ b/shiny/__init__.py @@ -1,6 +1,6 @@ """A package for building reactive web applications.""" -__version__ = "0.3.2.9001" +__version__ = "0.3.2.9002" from ._shinyenv import is_pyodide as _is_pyodide