Skip to content

Commit

Permalink
Add hotkey shortcuts for text areas (#922)
Browse files Browse the repository at this point in the history
Also updates fancy_chat and textarea demos to show usage of shortcuts
  • Loading branch information
richard-to committed Sep 6, 2024
1 parent a7c7a77 commit a0585a3
Show file tree
Hide file tree
Showing 11 changed files with 358 additions and 5 deletions.
15 changes: 15 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,7 +554,18 @@ 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 _submit_chat_msg()


def on_click_submit_chat_msg(e: me.ClickEvent):
yield from _submit_chat_msg()


def _submit_chat_msg():
"""Handles submitting a chat message."""
state = me.state(State)
if state.in_progress or not state.input:
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
89 changes: 89 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,92 @@ 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');
const textbox = page.getByLabel('Textarea');
await textbox.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 textbox.pressSequentially('hi');
await page.keyboard.press('Enter');
await expect(await page.getByText('Submitted: hi hi')).toBeVisible();

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();
});

test('test native textarea shortcuts', async ({page}) => {
await page.goto('/components/input/e2e/textarea_shortcut_app');
const textbox = page.getByPlaceholder('Native textarea');

await textbox.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 textbox.pressSequentially('hi');
await page.keyboard.press('Enter');
await expect(await page.getByText('Submitted: hi hi')).toBeVisible();

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;
}
Loading

0 comments on commit a0585a3

Please sign in to comment.