From 5c08d956db584fe09cea6f553bd353affab94bc3 Mon Sep 17 00:00:00 2001 From: VijithaEkanayake Date: Thu, 17 Dec 2020 21:35:20 +0530 Subject: [PATCH 1/4] Add enter handler for textbox Fixes: https://github.com/h2oai/wave/issues/372 --- py/examples/textbox.py | 6 ++++++ ui/src/textbox.test.tsx | 33 ++++++++++++++++++++++++++++++++- ui/src/textbox.tsx | 9 +++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/py/examples/textbox.py b/py/examples/textbox.py index e86295d7db..2872b7cd9e 100644 --- a/py/examples/textbox.py +++ b/py/examples/textbox.py @@ -22,6 +22,11 @@ async def serve(q: Q): ui.text(f'textbox_multiline={q.args.textbox_multiline}'), ui.button(name='show_form', label='Back', primary=True), ] + elif q.args.enter_key_handler: + q.page['example'].items = [ + ui.text(f'textbox_enter_key_handler={q.args.enter_key_handler}'), + ui.button(name='show_form', label='Back', primary=True), + ] else: q.page['example'] = ui.form_card(box='1 1 4 10', items=[ ui.textbox(name='textbox', label='Standard'), @@ -36,6 +41,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='enter_key_handler', label='Submits the textbox value on Enter key', icon='Search'), ui.textbox(name='textbox_multiline', label='Multiline textarea', multiline=True), ui.button(name='show_inputs', label='Submit', primary=True), ]) diff --git a/ui/src/textbox.test.tsx b/ui/src/textbox.test.tsx index b9d3b2c8e2..8734eb48a4 100644 --- a/ui/src/textbox.test.tsx +++ b/ui/src/textbox.test.tsx @@ -101,4 +101,35 @@ describe('Textbox.tsx', () => { expect(syncMock).not.toBeCalled() }) -}) \ No newline at end of file + + it('Calls sync on key up - When the key is Enter key', () => { + 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 on key up - When the key is not Enter key', () => { + 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 key up - When multiline is 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..2f7bd60a4b 100644 --- a/ui/src/textbox.tsx +++ b/ui/src/textbox.tsx @@ -75,6 +75,13 @@ export const qd.args[m.name] = v ?? (m.value || '') if (m.trigger) qd.sync() }, + onKeyUp = ( event: React.KeyboardEvent, v?: string) => { + if ((event).key == "Enter" && event.target instanceof HTMLInputElement) { + v = v || (event.target as HTMLInputElement).value + qd.args[m.name] = v ?? (m.value || '') + qd.sync() + } + }, render = () => m.mask ? ( ) : ( @@ -108,6 +116,7 @@ export const multiline={m.multiline} type={m.password ? 'password' : undefined} onChange={m.trigger ? debounce(DEBOUNCE_TIMEOUT, onChange) : onChange} + onKeyUp={onKeyUp} /> ) From 0b50eb34c8214c41fdbe56e47d65a4437f865276 Mon Sep 17 00:00:00 2001 From: Vijitha Ekanayake Date: Fri, 18 Dec 2020 16:45:48 +0530 Subject: [PATCH 2/4] Update ui/src/textbox.test.tsx Co-authored-by: mturoci <64769322+mturoci@users.noreply.github.com> --- ui/src/textbox.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/textbox.test.tsx b/ui/src/textbox.test.tsx index 8734eb48a4..f9851d4292 100644 --- a/ui/src/textbox.test.tsx +++ b/ui/src/textbox.test.tsx @@ -102,7 +102,7 @@ describe('Textbox.tsx', () => { expect(syncMock).not.toBeCalled() }) - it('Calls sync on key up - When the key is Enter key', () => { + it('Calls sync on enter pressed', () => { const { getByTestId } = render() const syncMock = jest.fn() From 6737d9f08355c4fd8ef091e86ad2404314962b74 Mon Sep 17 00:00:00 2001 From: Vijitha Ekanayake Date: Fri, 18 Dec 2020 16:47:03 +0530 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: mturoci <64769322+mturoci@users.noreply.github.com> --- ui/src/textbox.test.tsx | 4 ++-- ui/src/textbox.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/src/textbox.test.tsx b/ui/src/textbox.test.tsx index f9851d4292..71eb12ab68 100644 --- a/ui/src/textbox.test.tsx +++ b/ui/src/textbox.test.tsx @@ -112,7 +112,7 @@ describe('Textbox.tsx', () => { expect(syncMock).toBeCalled() }) - it('Does not call sync on key up - When the key is not Enter key', () => { + it('Does not call sync when key pressed is not enter', () => { const { getByTestId } = render() const syncMock = jest.fn() @@ -122,7 +122,7 @@ describe('Textbox.tsx', () => { expect(syncMock).not.toBeCalled() }) - it('Does not call sync on key up - When multiline is true', () => { + it('Does not call sync on enter - multiline is true', () => { const { getByTestId } = render() const syncMock = jest.fn() diff --git a/ui/src/textbox.tsx b/ui/src/textbox.tsx index 2f7bd60a4b..7d9c5644c6 100644 --- a/ui/src/textbox.tsx +++ b/ui/src/textbox.tsx @@ -75,9 +75,9 @@ export const qd.args[m.name] = v ?? (m.value || '') if (m.trigger) qd.sync() }, - onKeyUp = ( event: React.KeyboardEvent, v?: string) => { - if ((event).key == "Enter" && event.target instanceof HTMLInputElement) { - v = v || (event.target as HTMLInputElement).value + 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() } @@ -121,4 +121,4 @@ export const ) return { render } - }) \ No newline at end of file + }) From 54ea7d5db2a325ed77586bae241cda40197468e3 Mon Sep 17 00:00:00 2001 From: VijithaEkanayake Date: Fri, 18 Dec 2020 19:30:55 +0530 Subject: [PATCH 4/4] Introduce submit attribute to textbox The implementation conditionally calls the keyup handler based on the values of the submit attribute. Fixes: https://github.com/h2oai/wave/issues/372 --- py/examples/textbox.py | 10 +++------- py/h2o_wave/types.py | 7 +++++++ py/h2o_wave/ui.py | 3 +++ ui/src/textbox.test.tsx | 22 ++++++++++++++++------ ui/src/textbox.tsx | 6 ++++-- 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/py/examples/textbox.py b/py/examples/textbox.py index 2872b7cd9e..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,11 +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.button(name='show_form', label='Back', primary=True), - ] - elif q.args.enter_key_handler: - q.page['example'].items = [ - ui.text(f'textbox_enter_key_handler={q.args.enter_key_handler}'), + ui.text(f'textbox_enter={q.args.textbox_enter}'), ui.button(name='show_form', label='Back', primary=True), ] else: @@ -41,7 +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='enter_key_handler', label='Submits the textbox value on Enter key', icon='Search'), + 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 71eb12ab68..9e85492846 100644 --- a/ui/src/textbox.test.tsx +++ b/ui/src/textbox.test.tsx @@ -102,8 +102,8 @@ describe('Textbox.tsx', () => { expect(syncMock).not.toBeCalled() }) - it('Calls sync on enter pressed', () => { - const { getByTestId } = render() + it('Calls sync on enter pressed - submit specified', () => { + const { getByTestId } = render() const syncMock = jest.fn() T.qd.sync = syncMock @@ -112,8 +112,8 @@ describe('Textbox.tsx', () => { expect(syncMock).toBeCalled() }) - it('Does not call sync when key pressed is not enter', () => { - const { getByTestId } = render() + it('Does not call sync when key pressed is not enter - submit specified', () => { + const { getByTestId } = render() const syncMock = jest.fn() T.qd.sync = syncMock @@ -122,8 +122,18 @@ describe('Textbox.tsx', () => { expect(syncMock).not.toBeCalled() }) - it('Does not call sync on enter - multiline is true', () => { - const { getByTestId } = render() + 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 diff --git a/ui/src/textbox.tsx b/ui/src/textbox.tsx index 7d9c5644c6..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 @@ -95,7 +97,7 @@ export const disabled={m.disabled} readOnly={m.readonly} onChange={m.trigger ? debounce(DEBOUNCE_TIMEOUT, onChange) : onChange} - onKeyUp={onKeyUp} + onKeyUp={m.submit ? onKeyUp : undefined} /> ) : ( @@ -116,7 +118,7 @@ export const multiline={m.multiline} type={m.password ? 'password' : undefined} onChange={m.trigger ? debounce(DEBOUNCE_TIMEOUT, onChange) : onChange} - onKeyUp={onKeyUp} + onKeyUp={m.submit ? onKeyUp : undefined} /> )