Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hotkey shortcuts for text areas #922

Merged
merged 2 commits into from
Sep 6, 2024
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
11 changes: 11 additions & 0 deletions demo/fancy_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,9 @@ def chat_input():
key="chat_input",
min_rows=4,
on_blur=on_chat_input,
shortcuts={
me.Shortcut(shift=True, key="Enter"): on_submit_chat_msg,
},
placeholder="Enter your prompt",
style=me.Style(
background=me.theme_var("surface-container")
Expand Down Expand Up @@ -458,6 +461,7 @@ def on_click_example_user_query(e: me.ClickEvent):
state = me.state(State)
_, example_index = e.key.split("-")
state.input = _EXAMPLE_USER_QUERIES[int(example_index)]
me.focus_component(key="chat_input")


def on_click_thumb_up(e: me.ClickEvent):
Expand Down Expand Up @@ -550,6 +554,13 @@ def on_click_regenerate(e: me.ClickEvent):
yield


def on_submit_chat_msg(e: me.TextareaShortcutEvent):
state = me.state(State)
state.input = e.value
yield
yield from on_click_submit_chat_msg(me.ClickEvent(key=e.key, is_target=False))
richard-to marked this conversation as resolved.
Show resolved Hide resolved


def on_click_submit_chat_msg(e: me.ClickEvent):
"""Handles submitting a chat message."""
state = me.state(State)
Expand Down
45 changes: 43 additions & 2 deletions demo/textarea.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,30 @@
@me.stateclass
class State:
input: str = ""
output: str = ""


def on_blur(e: me.InputBlurEvent):
state = me.state(State)
state.input = e.value
state.output = e.value


def on_newline(e: me.TextareaShortcutEvent):
state = me.state(State)
state.input = e.value + "\n"


def on_submit(e: me.TextareaShortcutEvent):
state = me.state(State)
state.input = e.value
state.output = e.value


def on_clear(e: me.TextareaShortcutEvent):
state = me.state(State)
state.input = ""
state.output = ""


@me.page(
Expand All @@ -19,5 +38,27 @@ def on_blur(e: me.InputBlurEvent):
)
def app():
s = me.state(State)
me.textarea(label="Basic input", on_blur=on_blur)
me.text(text=s.input)
with me.box(style=me.Style(margin=me.Margin.all(15))):
me.text(
"Press enter to submit.",
style=me.Style(margin=me.Margin(bottom=15)),
)
me.text(
"Press shift+enter to create new line.",
style=me.Style(margin=me.Margin(bottom=15)),
)
me.text(
"Press shift+meta+enter to clear text.",
style=me.Style(margin=me.Margin(bottom=15)),
)
me.textarea(
label="Basic input",
value=s.input,
on_blur=on_blur,
shortcuts={
me.Shortcut(key="Enter"): on_submit,
me.Shortcut(shift=True, key="Enter"): on_newline,
me.Shortcut(shift=True, meta=True, key="Enter"): on_clear,
},
)
me.text(text=s.output)
4 changes: 4 additions & 0 deletions mesop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@
from mesop.components.input.input import EnterEvent as EnterEvent
from mesop.components.input.input import InputBlurEvent as InputBlurEvent
from mesop.components.input.input import InputEnterEvent as InputEnterEvent
from mesop.components.input.input import Shortcut as Shortcut
from mesop.components.input.input import (
TextareaShortcutEvent as TextareaShortcutEvent,
)
from mesop.components.input.input import input as input
from mesop.components.input.input import native_textarea as native_textarea
from mesop.components.input.input import textarea as textarea
Expand Down
1 change: 1 addition & 0 deletions mesop/components/input/e2e/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from . import input_app as input_app
from . import input_blur_app as input_blur_app
from . import textarea_shortcut_app as textarea_shortcut_app
45 changes: 45 additions & 0 deletions mesop/components/input/e2e/input_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,48 @@ test('test input on_blur works', async ({page}) => {
page.getByText('input_value_when_button_clicked: second_textarea'),
).toBeVisible();
});

test('test textarea shortcuts', async ({page}) => {
await page.goto('/components/input/e2e/textarea_shortcut_app');
await page.getByLabel('Textarea').fill('hi');
await page.keyboard.press('Enter');
await expect(await page.getByText('Submitted: hi')).toBeVisible();

await page.keyboard.down('Shift');
await page.keyboard.press('Enter');
await page.keyboard.up('Shift');
await expect(await page.getByText('Submitted: hi')).toBeVisible();

await page.getByLabel('Textarea').pressSequentially('hi');
await page.keyboard.press('Enter');
await expect(await page.getByText('Submitted: hi hi').textContent()).toEqual(
'Submitted: hi\nhi',
);

await page.keyboard.down('Meta');
await page.keyboard.press('s');
await page.keyboard.up('Meta');
await expect(
await page.getByText(
"Shortcut: Shortcut(key='s', shift=False, ctrl=False, alt=False, meta=True)",
),
).toBeVisible();

await page.keyboard.down('Control');
await page.keyboard.down('Alt');
await page.keyboard.press('Enter');
await page.keyboard.up('Control');
await page.keyboard.up('Alt');
await expect(
await page.getByText(
"Shortcut: Shortcut(key='Enter', shift=False, ctrl=True, alt=True, meta=False)",
),
).toBeVisible();

await page.keyboard.press('Escape');
await expect(
await page.getByText(
"Shortcut: Shortcut(key='Escape', shift=False, ctrl=False, alt=False, meta=False)",
),
).toBeVisible();
});
58 changes: 58 additions & 0 deletions mesop/components/input/e2e/textarea_shortcut_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import mesop as me


@me.stateclass
class State:
input: str = ""
output: str = ""
shortcut: str = ""


def on_newline(e: me.TextareaShortcutEvent):
state = me.state(State)
state.input = e.value + "\n"


def on_submit(e: me.TextareaShortcutEvent):
state = me.state(State)
state.input = e.value
state.output = e.value


def on_shortcut(e: me.TextareaShortcutEvent):
state = me.state(State)
state.shortcut = str(e.shortcut)


@me.page(path="/components/input/e2e/textarea_shortcut_app")
def app():
s = me.state(State)
with me.box(style=me.Style(margin=me.Margin.all(15))):
me.textarea(
label="Textarea",
value=s.input,
shortcuts={
me.Shortcut(key="Enter"): on_submit,
me.Shortcut(shift=True, key="Enter"): on_newline,
me.Shortcut(ctrl=True, alt=True, key="Enter"): on_shortcut,
me.Shortcut(meta=True, key="s"): on_shortcut,
me.Shortcut(key="Escape"): on_shortcut,
},
)

me.native_textarea(
placeholder="Native textarea",
value=s.input,
autosize=True,
min_rows=5,
shortcuts={
me.Shortcut(key="Enter"): on_submit,
me.Shortcut(shift=True, key="Enter"): on_newline,
me.Shortcut(ctrl=True, alt=True, key="Enter"): on_shortcut,
me.Shortcut(meta=True, key="s"): on_shortcut,
me.Shortcut(key="Escape"): on_shortcut,
},
)

me.text(text="Submitted: " + s.output)
me.text(text="Shortcut: " + s.shortcut)
2 changes: 2 additions & 0 deletions mesop/components/input/input.ng.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
[style]="getStyle()"
(input)="onInput($event)"
(blur)="onBlur($event)"
(keydown)="onKeyDown($event)"
[cdkTextareaAutosize]="config().getAutosize()"
[cdkAutosizeMinRows]="config().getMinRows()"
[cdkAutosizeMaxRows]="config().getMaxRows()"
Expand Down Expand Up @@ -35,6 +36,7 @@
(input)="onInput($event)"
(blur)="onBlur($event)"
[rows]="config().getRows()"
(keydown)="onKeyDown($event)"
[cdkTextareaAutosize]="config().getAutosize()"
[cdkAutosizeMinRows]="config().getMinRows()"
[cdkAutosizeMaxRows]="config().getMaxRows()"
Expand Down
16 changes: 15 additions & 1 deletion mesop/components/input/input.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ syntax = "proto2";

package mesop.components.input;

// Next id: 26
// Next id: 27
message InputType {
optional bool disabled = 1;
optional string id = 2;
Expand All @@ -27,7 +27,21 @@ message InputType {
optional bool autosize = 20;
optional int32 min_rows = 21;
optional int32 max_rows = 22;
repeated ShortcutHandler on_shortcut_handler = 26;
// Not exposed as public API.
optional bool is_textarea = 19;
optional bool is_native_textarea = 23;
}

message ShortcutHandler {
optional Shortcut shortcut = 1;
optional string handler_id = 2;
}

message Shortcut {
optional string key = 1;
optional bool shift = 2;
optional bool ctrl = 3;
optional bool alt = 4;
optional bool meta = 5;
}
72 changes: 72 additions & 0 deletions mesop/components/input/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,47 @@ class InputEnterEvent(MesopEvent):
)


@dataclass(kw_only=True, frozen=True)
class Shortcut:
"""Represents a keyboard shortcut combination."""

key: str
richard-to marked this conversation as resolved.
Show resolved Hide resolved
shift: bool = False
ctrl: bool = False
alt: bool = False
meta: bool = False


@dataclass(kw_only=True)
class TextareaShortcutEvent(MesopEvent):
"""Represents a shortcut keyboard event on a textarea component.

Attributes:
value: Input value.
shortcut: Key combination pressed.
key (str): key of the component that emitted this event.
"""

shortcut: Shortcut
value: str


register_event_mapper(
TextareaShortcutEvent,
lambda event, key: TextareaShortcutEvent(
key=key.key,
value=event.textarea_shortcut.string_value,
shortcut=Shortcut(
shift=event.textarea_shortcut.shortcut.shift,
ctrl=event.textarea_shortcut.shortcut.ctrl,
alt=event.textarea_shortcut.shortcut.alt,
meta=event.textarea_shortcut.shortcut.meta,
key=event.textarea_shortcut.shortcut.key,
),
),
)


@dataclass(kw_only=True)
class InputBlurEvent(MesopEvent):
"""Represents an inpur blur event (when a user loses focus of an input).
Expand Down Expand Up @@ -83,6 +124,8 @@ def textarea(
float_label: Literal["always", "auto"] = "auto",
subscript_sizing: Literal["fixed", "dynamic"] = "fixed",
hint_label: str = "",
shortcuts: dict[Shortcut, Callable[[TextareaShortcutEvent], Any]]
| None = None,
key: str | None = None,
):
"""Creates a Textarea component.
Expand All @@ -107,6 +150,7 @@ def textarea(
float_label: Whether the label should always float or float as the user types.
subscript_sizing: Whether the form field should reserve space for one line of hint/error text (default) or to have the spacing grow from 0px as needed based on the size of the hint/error content. Note that when using dynamic sizing, layout shifts will occur when hint/error text changes.
hint_label: Text for the form field hint.
shortcuts: Shortcut events to listen for.
key: The component [key](../components/index.md#component-key).
"""

Expand Down Expand Up @@ -138,6 +182,7 @@ def textarea(
on_input_handler_id=register_event_handler(on_input, event=InputEvent)
if on_input
else "",
on_shortcut_handler=_to_on_shortcut_handler(shortcuts),
),
style=style,
)
Expand Down Expand Up @@ -250,6 +295,8 @@ def native_textarea(
placeholder: str = "",
value: str = "",
readonly: bool = False,
shortcuts: dict[Shortcut, Callable[[TextareaShortcutEvent], Any]]
| None = None,
key: str | None = None,
):
"""Creates a browser native Textarea component. Intended for advanced use cases with maximum UI control.
Expand All @@ -265,6 +312,7 @@ def native_textarea(
placeholder: Placeholder value
value: Initial value.
readonly: Whether the element is readonly.
shortcuts: Shortcut events to listen for.
key: The component [key](../components/index.md#component-key).
"""

Expand All @@ -287,6 +335,30 @@ def native_textarea(
on_input_handler_id=register_event_handler(on_input, event=InputEvent)
if on_input
else "",
on_shortcut_handler=_to_on_shortcut_handler(shortcuts),
),
style=style,
)


def _to_on_shortcut_handler(
shortcuts: dict[Shortcut, Callable[[TextareaShortcutEvent], Any]] | None,
) -> list[input_pb.ShortcutHandler]:
if not shortcuts:
return []

return [
input_pb.ShortcutHandler(
shortcut=input_pb.Shortcut(
key=shortcut.key,
shift=shortcut.shift,
ctrl=shortcut.ctrl,
alt=shortcut.alt,
meta=shortcut.meta,
),
handler_id=register_event_handler(
event_handler, event=TextareaShortcutEvent
),
)
for shortcut, event_handler in shortcuts.items()
]
Loading
Loading