Преглед изворни кода

textarea: expose auto_height and enter_key_submit props (#2884)

* textarea: expose auto_height and enter_key_submit props

These two props improve the workflow for chat apps and other situations where
we want multiline input.

auto_height: resize the textarea based on its contents

enter_key_submit: pressing enter submits the enclosing form (shift+enter
inserts new lines)

Fix #1231

* Update pyi
Masen Furer пре 1 година
родитељ
комит
8ef193809c

+ 78 - 1
reflex/components/el/elements/forms.py

@@ -2,7 +2,7 @@
 from __future__ import annotations
 
 from hashlib import md5
-from typing import Any, Dict, Iterator, Union
+from typing import Any, Dict, Iterator, Set, Union
 
 from jinja2 import Environment
 
@@ -500,6 +500,36 @@ class Select(BaseHTML):
         }
 
 
+AUTO_HEIGHT_JS = """
+const autoHeightOnInput = (e, is_enabled) => {
+    if (is_enabled) {
+        const el = e.target;
+        el.style.overflowY = "hidden";
+        el.style.height = "auto";
+        el.style.height = (e.target.scrollHeight) + "px";
+        if (el.form && !el.form.data_resize_on_reset) {
+            el.form.addEventListener("reset", () => window.setTimeout(() => autoHeightOnInput(e, is_enabled), 0))
+            el.form.data_resize_on_reset = true;
+        }
+    }
+}
+"""
+
+
+ENTER_KEY_SUBMIT_JS = """
+const enterKeySubmitOnKeyDown = (e, is_enabled) => {
+    if (is_enabled && e.which === 13 && !e.shiftKey) {
+        e.preventDefault();
+        if (!e.repeat) {
+            if (e.target.form) {
+                e.target.form.requestSubmit();
+            }
+        }
+    }
+}
+"""
+
+
 class Textarea(BaseHTML):
     """Display the textarea element."""
 
@@ -511,6 +541,9 @@ class Textarea(BaseHTML):
     # Automatically focuses the textarea when the page loads
     auto_focus: Var[Union[str, int, bool]]
 
+    # Automatically fit the content height to the text (use min-height with this prop)
+    auto_height: Var[bool]
+
     # Visible width of the text control, in average character widths
     cols: Var[Union[str, int, bool]]
 
@@ -520,6 +553,9 @@ class Textarea(BaseHTML):
     # Disables the textarea
     disabled: Var[Union[str, int, bool]]
 
+    # Enter key submits form (shift-enter adds new line)
+    enter_key_submit: Var[bool]
+
     # Associates the textarea with a form (by id)
     form: Var[Union[str, int, bool]]
 
@@ -550,6 +586,47 @@ class Textarea(BaseHTML):
     # How the text in the textarea is to be wrapped when submitting the form
     wrap: Var[Union[str, int, bool]]
 
+    def _exclude_props(self) -> list[str]:
+        return super()._exclude_props() + [
+            "auto_height",
+            "enter_key_submit",
+        ]
+
+    def get_custom_code(self) -> Set[str]:
+        """Include the custom code for auto_height and enter_key_submit functionality.
+
+        Returns:
+            The custom code for the component.
+        """
+        custom_code = super().get_custom_code()
+        if self.auto_height is not None:
+            custom_code.add(AUTO_HEIGHT_JS)
+        if self.enter_key_submit is not None:
+            custom_code.add(ENTER_KEY_SUBMIT_JS)
+        return custom_code
+
+    def _render(self) -> Tag:
+        tag = super()._render()
+        if self.enter_key_submit is not None:
+            if "on_key_down" in self.event_triggers:
+                raise ValueError(
+                    "Cannot combine `enter_key_submit` with `on_key_down`.",
+                )
+            tag.add_props(
+                on_key_down=Var.create_safe(
+                    f"(e) => enterKeySubmitOnKeyDown(e, {self.enter_key_submit._var_name_unwrapped})",
+                    _var_is_local=False,
+                )._replace(merge_var_data=self.enter_key_submit._var_data),
+            )
+        if self.auto_height is not None:
+            tag.add_props(
+                on_input=Var.create_safe(
+                    f"(e) => autoHeightOnInput(e, {self.auto_height._var_name_unwrapped})",
+                    _var_is_local=False,
+                )._replace(merge_var_data=self.auto_height._var_data),
+            )
+        return tag
+
     def get_event_triggers(self) -> Dict[str, Any]:
         """Get the event triggers that pass the component's value to the handler.
 

+ 9 - 1
reflex/components/el/elements/forms.pyi

@@ -8,7 +8,7 @@ from reflex.vars import Var, BaseVar, ComputedVar
 from reflex.event import EventChain, EventHandler, EventSpec
 from reflex.style import Style
 from hashlib import md5
-from typing import Any, Dict, Iterator, Union
+from typing import Any, Dict, Iterator, Set, Union
 from jinja2 import Environment
 from reflex.components.el.element import Element
 from reflex.components.tags.tag import Tag
@@ -2018,7 +2018,11 @@ class Select(BaseHTML):
         """
         ...
 
+AUTO_HEIGHT_JS = '\nconst autoHeightOnInput = (e, is_enabled) => {\n    if (is_enabled) {\n        const el = e.target;\n        el.style.overflowY = "hidden";\n        el.style.height = "auto";\n        el.style.height = (e.target.scrollHeight) + "px";\n        if (el.form && !el.form.data_resize_on_reset) {\n            el.form.addEventListener("reset", () => window.setTimeout(() => autoHeightOnInput(e, is_enabled), 0))\n            el.form.data_resize_on_reset = true;\n        }\n    }\n}\n'
+ENTER_KEY_SUBMIT_JS = "\nconst enterKeySubmitOnKeyDown = (e, is_enabled) => {\n    if (is_enabled && e.which === 13 && !e.shiftKey) {\n        e.preventDefault();\n        if (!e.repeat) {\n            if (e.target.form) {\n                e.target.form.requestSubmit();\n            }\n        }\n    }\n}\n"
+
 class Textarea(BaseHTML):
+    def get_custom_code(self) -> Set[str]: ...
     def get_event_triggers(self) -> Dict[str, Any]: ...
     @overload
     @classmethod
@@ -2031,6 +2035,7 @@ class Textarea(BaseHTML):
         auto_focus: Optional[
             Union[Var[Union[str, int, bool]], Union[str, int, bool]]
         ] = None,
+        auto_height: Optional[Union[Var[bool], bool]] = None,
         cols: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
         dirname: Optional[
             Union[Var[Union[str, int, bool]], Union[str, int, bool]]
@@ -2038,6 +2043,7 @@ class Textarea(BaseHTML):
         disabled: Optional[
             Union[Var[Union[str, int, bool]], Union[str, int, bool]]
         ] = None,
+        enter_key_submit: Optional[Union[Var[bool], bool]] = None,
         form: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
         max_length: Optional[
             Union[Var[Union[str, int, bool]], Union[str, int, bool]]
@@ -2168,9 +2174,11 @@ class Textarea(BaseHTML):
             *children: The children of the component.
             auto_complete: Whether the form control should have autocomplete enabled
             auto_focus: Automatically focuses the textarea when the page loads
+            auto_height: Automatically fit the content height to the text (use min-height with this prop)
             cols: Visible width of the text control, in average character widths
             dirname: Name part of the textarea to submit in 'dir' and 'name' pair when form is submitted
             disabled: Disables the textarea
+            enter_key_submit: Enter key submits form (shift-enter adds new line)
             form: Associates the textarea with a form (by id)
             max_length: Maximum number of characters allowed in the textarea
             min_length: Minimum number of characters required in the textarea

+ 4 - 0
reflex/components/radix/themes/components/text_area.pyi

@@ -108,7 +108,9 @@ class TextArea(RadixThemesComponent, el.Textarea):
         rows: Optional[Union[Var[str], str]] = None,
         value: Optional[Union[Var[str], str]] = None,
         wrap: Optional[Union[Var[str], str]] = None,
+        auto_height: Optional[Union[Var[bool], bool]] = None,
         cols: Optional[Union[Var[Union[str, int, bool]], Union[str, int, bool]]] = None,
+        enter_key_submit: Optional[Union[Var[bool], bool]] = None,
         access_key: Optional[
             Union[Var[Union[str, int, bool]], Union[str, int, bool]]
         ] = None,
@@ -232,7 +234,9 @@ class TextArea(RadixThemesComponent, el.Textarea):
             rows: Visible number of lines in the text control
             value: The controlled value of the textarea, read only unless used with on_change
             wrap: How the text in the textarea is to be wrapped when submitting the form
+            auto_height: Automatically fit the content height to the text (use min-height with this prop)
             cols: Visible width of the text control, in average character widths
+            enter_key_submit: Enter key submits form (shift-enter adds new line)
             access_key:  Provides a hint for generating a keyboard shortcut for the current element.
             auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
             content_editable: Indicates whether the element's content is editable.