Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to Shiny for Python will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [UNRELEASED]

### New features

* Added a new `input_submit_textarea()` input element, which is similar to `input_text_area()`, 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. (#2099)

## [1.5.0] - 2025-09-11

### New features
Expand Down
2 changes: 2 additions & 0 deletions docs/_quartodoc-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ quartodoc:
- ui.input_numeric
- ui.input_text
- ui.input_text_area
- ui.input_submit_textarea
- ui.input_password
- ui.input_action_button
- ui.input_action_link
Expand Down Expand Up @@ -166,6 +167,7 @@ quartodoc:
- ui.update_text
- name: ui.update_text_area
dynamic: "shiny.ui.update_text"
- ui.update_submit_textarea
- ui.update_navset
- ui.update_action_button
- ui.update_action_link
Expand Down
2 changes: 2 additions & 0 deletions docs/_quartodoc-express.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ quartodoc:
- express.ui.input_numeric
- express.ui.input_text
- express.ui.input_text_area
- express.ui.input_submit_textarea
- express.ui.input_password
- express.ui.input_action_button
- express.ui.input_action_link
Expand Down Expand Up @@ -126,6 +127,7 @@ quartodoc:
- express.ui.update_numeric
- express.ui.update_text
- express.ui.update_text_area
- express.ui.update_submit_textarea
- express.ui.update_navset
- express.ui.update_action_button
- express.ui.update_action_link
Expand Down
1 change: 1 addition & 0 deletions docs/_quartodoc-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ quartodoc:
- playwright.controller.InputSelectize
- playwright.controller.InputSlider
- playwright.controller.InputSliderRange
- playwright.controller.InputSubmitTextarea
- playwright.controller.InputSwitch
- playwright.controller.InputTaskButton
- playwright.controller.InputText
Expand Down
2 changes: 1 addition & 1 deletion shiny/_versions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
shiny_html_deps = "1.11.1.9000"
shiny_html_deps = "1.11.1.9001"
bslib = "0.9.0.9000"
htmltools = "0.5.8.9000"
bootstrap = "5.3.1"
Expand Down
22 changes: 22 additions & 0 deletions shiny/api-examples/input_submit_textarea/app-core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import time

from shiny import App, Inputs, Outputs, Session, render, ui

app_ui = ui.page_fluid(
ui.input_submit_textarea("text", placeholder="Enter some input..."),
ui.output_text("value"),
)


def server(input: Inputs, output: Outputs, session: Session):
@render.text
def value():
if "text" in input:
# Simulate processing time
time.sleep(2)
return f"You entered: {input.text()}"
else:
return "Submit some input to see it here."


app = App(app_ui, server)
15 changes: 15 additions & 0 deletions shiny/api-examples/input_submit_textarea/app-express.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import time

from shiny.express import input, render, ui

ui.input_submit_textarea("text", placeholder="Enter some input...")


@render.text
def value():
if "text" in input:
# Simulate processing time
time.sleep(2)
return f"You entered: {input.text()}"
else:
return "Submit some input to see it here."
45 changes: 45 additions & 0 deletions shiny/api-examples/update_submit_textarea/app-core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from shiny import App, Inputs, Outputs, Session, reactive, render, ui

app_ui = ui.page_fluid(
ui.input_submit_textarea(
"comment",
label="Your Comment",
placeholder="Type your comment here...",
rows=2,
toolbar=[
ui.input_action_button("clear", "Clear", class_="btn-sm btn-danger"),
ui.input_action_button("template", "Use Template", class_="btn-sm"),
],
),
ui.output_text("submitted_comment"),
)


def server(input: Inputs, output: Outputs, session: Session):
@reactive.effect
@reactive.event(input.clear)
def _():
ui.update_submit_textarea(
"comment",
value="",
placeholder="Type your comment here...",
)

@reactive.effect
@reactive.event(input.template)
def _():
ui.update_submit_textarea(
"comment",
value="Thank you for your feedback. We appreciate your input!",
placeholder="",
label="Your Comment (Template Applied)",
)

@render.text
def submitted_comment():
if "comment" in input:
return f"Submitted: {input.comment()}"
return "No comment submitted yet."


app = App(app_ui, server)
41 changes: 41 additions & 0 deletions shiny/api-examples/update_submit_textarea/app-express.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from shiny import reactive, render
from shiny.express import input, ui

ui.input_submit_textarea(
"comment",
label="Your Comment",
placeholder="Type your comment here...",
rows=2,
toolbar=[
ui.input_action_button("clear", "Clear", class_="btn-sm btn-danger"),
ui.input_action_button("template", "Use Template", class_="btn-sm"),
],
)


@reactive.effect
@reactive.event(input.clear)
def _():
ui.update_submit_textarea(
"comment",
value="",
placeholder="Type your comment here...",
)


@reactive.effect
@reactive.event(input.template)
def _():
ui.update_submit_textarea(
"comment",
value="Thank you for your feedback. We appreciate your input!",
placeholder="",
label="Your Comment (Template Applied)",
)


@render.text
def submitted_comment():
if "comment" in input:
return f"Submitted: {input.comment()}"
return "No comment submitted yet."
4 changes: 4 additions & 0 deletions shiny/express/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
input_select,
input_selectize,
input_slider,
input_submit_textarea,
input_switch,
input_task_button,
input_text,
Expand Down Expand Up @@ -102,6 +103,7 @@
update_selectize,
update_sidebar,
update_slider,
update_submit_textarea,
update_switch,
update_task_button,
update_text,
Expand Down Expand Up @@ -220,6 +222,7 @@
"input_select",
"input_selectize",
"input_slider",
"input_submit_textarea",
"bind_task_button",
"input_task_button",
"input_text",
Expand All @@ -243,6 +246,7 @@
"update_select",
"update_selectize",
"update_slider",
"update_submit_textarea",
"update_task_button",
"update_text",
"update_text_area",
Expand Down
2 changes: 2 additions & 0 deletions shiny/playwright/controller/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
InputDateRange,
InputNumeric,
InputPassword,
InputSubmitTextarea,
InputText,
InputTextArea,
)
Expand Down Expand Up @@ -83,6 +84,7 @@
"InputSelectize",
"InputSlider",
"InputSliderRange",
"InputSubmitTextarea",
"InputSwitch",
"InputTaskButton",
"InputText",
Expand Down
107 changes: 107 additions & 0 deletions shiny/playwright/controller/_input_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -966,3 +966,110 @@ def expect_autoclose(
# TODO-future; Composable expectations
self.date_start.expect_autoclose(value, timeout=timeout)
self.date_end.expect_autoclose(value, timeout=timeout)


class InputSubmitTextarea(
_SetTextM,
WidthContainerStyleM,
_ExpectTextInputValueM,
_ExpectPlaceholderAttrM,
UiWithLabel,
):
"""Controller for :func:`shiny.ui.input_submit_textarea`."""

loc_button: Locator
"""Playwright `Locator` for the submit button."""

def __init__(self, page: Page, id: str) -> None:
"""
Initializes the input submit textarea.

Parameters
----------
page
The page where the input submit textarea is located.
id
The id of the input submit textarea.
"""
super().__init__(
page,
id=id,
loc=f"textarea#{id}.form-control",
)
self.loc_button = self.loc_container.locator(".bslib-submit-textarea-btn")

def set(self, value: str, *, submit: bool = False, timeout: Timeout = None) -> None:
"""
Sets the text value in the textarea.

Parameters
----------
value
The text to set.
submit
Whether to click the submit button after setting the text. Defaults to `False`.
timeout
The maximum time to wait for the text to be set. Defaults to `None`.
"""
set_text(self.loc, value, timeout=timeout)
if submit:
self.loc_button.click(timeout=timeout)

def submit(self, *, timeout: Timeout = None) -> None:
"""
Clicks the submit button.

Parameters
----------
timeout
The maximum time to wait for the click. Defaults to `None`.
"""
self.loc_button.click(timeout=timeout)

def expect_rows(self, value: AttrValue, *, timeout: Timeout = None) -> None:
"""
Expect the `rows` attribute of the input submit textarea to have a specific value.

Parameters
----------
value
The expected value of the `rows` attribute.
timeout
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
"""
_expect_attribute_to_have_value(self.loc, "rows", value=value, timeout=timeout)

def expect_data_needs_modifier(
self, value: bool, *, timeout: Timeout = None
) -> None:
"""
Expect the `data-needs-modifier` attribute to be present or absent.

Parameters
----------
value
If `True`, expects the attribute to be present. If `False`, expects it to be absent.
timeout
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
"""
_expect_attribute_to_have_value(
self.loc,
"data-needs-modifier",
value="" if value else None,
timeout=timeout,
)

def expect_button_label(
self, value: PatternOrStr, *, timeout: Timeout = None
) -> None:
"""
Expect the submit button to have a specific label.

Parameters
----------
value
The expected label text.
timeout
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
"""
playwright_expect(self.loc_button).to_contain_text(value, timeout=timeout)
4 changes: 4 additions & 0 deletions shiny/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
from ._input_password import input_password
from ._input_select import input_select, input_selectize
from ._input_slider import AnimationOptions, SliderStepArg, SliderValueArg, input_slider
from ._input_submit_textarea import input_submit_textarea, update_submit_textarea
from ._input_task_button import bind_task_button, input_task_button
from ._input_text import input_text, input_text_area
from ._input_update import (
Expand Down Expand Up @@ -251,6 +252,9 @@
"SliderValueArg",
"SliderStepArg",
"AnimationOptions",
# _input_submit_textarea
"input_submit_textarea",
"update_submit_textarea",
# _input_task_button
"bind_task_button",
"input_task_button",
Expand Down
Loading
Loading