diff --git a/py/examples/textbox.py b/py/examples/textbox.py index e86295d7db..22b4de7af9 100644 --- a/py/examples/textbox.py +++ b/py/examples/textbox.py @@ -6,7 +6,7 @@ @app('/demo') async def serve(q: Q): - if q.args.show_inputs: + if q.args.show_inputs or q.args.textbox_submit: q.page['example'].items = [ ui.text(f'textbox={q.args.textbox}'), ui.text(f'textbox_disabled={q.args.textbox_disabled}'), @@ -20,6 +20,7 @@ async def serve(q: Q): ui.text(f'textbox_placeholder={q.args.textbox_placeholder}'), ui.text(f'textbox_disabled_placeholder={q.args.textbox_disabled_placeholder}'), ui.text(f'textbox_multiline={q.args.textbox_multiline}'), + ui.text(f'textbox_enter={q.args.textbox_enter}'), ui.button(name='show_form', label='Back', primary=True), ] else: @@ -36,6 +37,7 @@ async def serve(q: Q): ui.textbox(name='textbox_placeholder', label='With placeholder', placeholder='I need some input'), ui.textbox(name='textbox_disabled_placeholder', label='Disabled with placeholder', disabled=True, placeholder='I am disabled'), + ui.textbox(name='textbox_submit', label='Submits on enter pressed', icon='Search', submit=True), ui.textbox(name='textbox_multiline', label='Multiline textarea', multiline=True), ui.button(name='show_inputs', label='Submit', primary=True), ]) diff --git a/py/h2o_wave/types.py b/py/h2o_wave/types.py index 9af0aa771c..26fc740edc 100644 --- a/py/h2o_wave/types.py +++ b/py/h2o_wave/types.py @@ -946,6 +946,7 @@ def __init__( height: Optional[str] = None, visible: Optional[bool] = None, tooltip: Optional[str] = None, + submit: Optional[bool] = None, ): self.name = name """An identifying name for this component.""" @@ -983,6 +984,8 @@ def __init__( """True if the component should be visible. Defaults to true.""" self.tooltip = tooltip """An optional tooltip message displayed when a user clicks the help icon to the right of the component.""" + self.submit = submit + """True if the form should be submitted when enter key pressed.""" def dump(self) -> Dict: """Returns the contents of this object as a dict.""" @@ -1007,6 +1010,7 @@ def dump(self) -> Dict: height=self.height, visible=self.visible, tooltip=self.tooltip, + submit=self.submit, ) @staticmethod @@ -1032,6 +1036,7 @@ def load(__d: Dict) -> 'Textbox': __d_height: Any = __d.get('height') __d_visible: Any = __d.get('visible') __d_tooltip: Any = __d.get('tooltip') + __d_submit: Any = __d.get('submit') name: str = __d_name label: Optional[str] = __d_label placeholder: Optional[str] = __d_placeholder @@ -1050,6 +1055,7 @@ def load(__d: Dict) -> 'Textbox': height: Optional[str] = __d_height visible: Optional[bool] = __d_visible tooltip: Optional[str] = __d_tooltip + submit: Optional[bool] = __d_submit return Textbox( name, label, @@ -1069,6 +1075,7 @@ def load(__d: Dict) -> 'Textbox': height, visible, tooltip, + submit, ) diff --git a/py/h2o_wave/ui.py b/py/h2o_wave/ui.py index 098ec0789b..2d3b699302 100644 --- a/py/h2o_wave/ui.py +++ b/py/h2o_wave/ui.py @@ -450,6 +450,7 @@ def textbox( height: Optional[str] = None, visible: Optional[bool] = None, tooltip: Optional[str] = None, + submit: Optional[bool] = None, ) -> Component: """Create a text box. @@ -476,6 +477,7 @@ def textbox( height: The height of the text box, e.g. '100px'. Applicable only if `multiline` is true. visible: True if the component should be visible. Defaults to true. tooltip: An optional tooltip message displayed when a user clicks the help icon to the right of the component. + submit: True if the form should be submitted when enter key pressed. Returns: A `h2o_wave.types.Textbox` instance. """ @@ -498,6 +500,7 @@ def textbox( height, visible, tooltip, + submit, )) diff --git a/ui/src/textbox.test.tsx b/ui/src/textbox.test.tsx index b9d3b2c8e2..9e85492846 100644 --- a/ui/src/textbox.test.tsx +++ b/ui/src/textbox.test.tsx @@ -101,4 +101,45 @@ describe('Textbox.tsx', () => { expect(syncMock).not.toBeCalled() }) -}) \ No newline at end of file + + it('Calls sync on enter pressed - submit specified', () => { + const { getByTestId } = render() + const syncMock = jest.fn() + + T.qd.sync = syncMock + fireEvent.keyUp(getByTestId(name), { key: 'Enter', target: { value: 'text' } }) + + expect(syncMock).toBeCalled() + }) + + it('Does not call sync when key pressed is not enter - submit specified', () => { + const { getByTestId } = render() + const syncMock = jest.fn() + + T.qd.sync = syncMock + fireEvent.keyUp(getByTestId(name), { key: 'A', target: { value: 'text' } }) + + expect(syncMock).not.toBeCalled() + }) + + it('Does not call sync on enter pressed - submit not specified', () => { + const { getByTestId } = render() + const syncMock = jest.fn() + + T.qd.sync = syncMock + fireEvent.keyUp(getByTestId(name), { key: 'Enter', target: { value: 'text' } }) + + expect(syncMock).not.toBeCalled() + }) + + it('Does not call sync on enter - multiline and submit both are true', () => { + const { getByTestId } = render() + const syncMock = jest.fn() + + T.qd.sync = syncMock + fireEvent.keyUp(getByTestId(name), { key: 'Enter', target: { value: 'text' } }) + + expect(syncMock).not.toBeCalled() + }) + +}) diff --git a/ui/src/textbox.tsx b/ui/src/textbox.tsx index dbb883b623..7d1368be8b 100644 --- a/ui/src/textbox.tsx +++ b/ui/src/textbox.tsx @@ -62,6 +62,8 @@ export interface Textbox { visible?: B /** An optional tooltip message displayed when a user clicks the help icon to the right of the component. */ tooltip?: S + /** True if the form should be submitted when enter key is pressed. */ + submit?: B } const DEBOUNCE_TIMEOUT = 500 @@ -75,6 +77,13 @@ export const qd.args[m.name] = v ?? (m.value || '') if (m.trigger) qd.sync() }, + onKeyUp = ( {key, target}: React.KeyboardEvent, v?: S) => { + if (key == 'Enter' && target instanceof HTMLInputElement) { + v = v || target.value + qd.args[m.name] = v ?? (m.value || '') + qd.sync() + } + }, render = () => m.mask ? ( ) : ( @@ -108,8 +118,9 @@ export const multiline={m.multiline} type={m.password ? 'password' : undefined} onChange={m.trigger ? debounce(DEBOUNCE_TIMEOUT, onChange) : onChange} + onKeyUp={m.submit ? onKeyUp : undefined} /> ) return { render } - }) \ No newline at end of file + })