From 8ef193809c0973b186a7aadcf234d574fea202ea Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 28 Mar 2024 17:17:30 -0700 Subject: [PATCH] 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 --- reflex/components/el/elements/forms.py | 79 ++++++++++++++++++- reflex/components/el/elements/forms.pyi | 10 ++- .../radix/themes/components/text_area.pyi | 4 + 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/reflex/components/el/elements/forms.py b/reflex/components/el/elements/forms.py index c70398ef4..9e33f1a02 100644 --- a/reflex/components/el/elements/forms.py +++ b/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. diff --git a/reflex/components/el/elements/forms.pyi b/reflex/components/el/elements/forms.pyi index b665dbb3c..0916db76e 100644 --- a/reflex/components/el/elements/forms.pyi +++ b/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 diff --git a/reflex/components/radix/themes/components/text_area.pyi b/reflex/components/radix/themes/components/text_area.pyi index 3ac4d2daf..ab5dbd1d0 100644 --- a/reflex/components/radix/themes/components/text_area.pyi +++ b/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.