From 85a45bd1e50bf2aab5d97166c27ee017599af5dd Mon Sep 17 00:00:00 2001 From: Rodrigo Arede Date: Mon, 22 Jul 2024 20:01:47 +0100 Subject: [PATCH] Fix #2612: unexpected behaviour of the cursor When inserting a non valid character in the middle of a pattern, the cursor instantly jumps to the end of the word, instead of staying in that position. Added also some case tests. --- .../src/components/Input.react.js | 25 ++++--- .../integration/input/test_input_basics.py | 69 +++++++++++++++++++ 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/components/dash-core-components/src/components/Input.react.js b/components/dash-core-components/src/components/Input.react.js index d4656fddae..779f526439 100644 --- a/components/dash-core-components/src/components/Input.react.js +++ b/components/dash-core-components/src/components/Input.react.js @@ -184,16 +184,23 @@ export default class Input extends PureComponent { onChange() { const {debounce} = this.props; - if (debounce) { - if (Number.isFinite(debounce)) { - this.debounceEvent(debounce); - } - if (this.props.type !== 'number') { - this.setState({value: this.input.current.value}); + const input = this.input.current; + const cursorPosition = input.selectionStart; + const currentValue = input.value; + this.setState({value: currentValue}, () => { + if (debounce) { + if (Number.isFinite(debounce)) { + this.debounceEvent(debounce); + } + if (this.props.type !== 'number') { + setTimeout(() => { + input.setSelectionRange(cursorPosition, cursorPosition); + }, 0); + } + } else { + this.onEvent(); } - } else { - this.onEvent(); - } + }); } } diff --git a/components/dash-core-components/tests/integration/input/test_input_basics.py b/components/dash-core-components/tests/integration/input/test_input_basics.py index 853caef319..c76e4fa481 100644 --- a/components/dash-core-components/tests/integration/input/test_input_basics.py +++ b/components/dash-core-components/tests/integration/input/test_input_basics.py @@ -103,3 +103,72 @@ def test_inbs003_styles_are_scoped(dash_dcc): dash_outline_css = dash_input.value_of_css_property("outline") assert external_outline_css != dash_outline_css + +@pytest.mark.parametrize( + "initial_text, invalid_char, cursor_position_before, expected_text, expected_cursor_position", + [ + ("abcdddef", "/", 2, "ab/cdddef", 3), + ("abcdef", "$", 2, "ab$cdef", 3), + ("abcdef", "$", 3, "abc$def", 4), + ("abcdef", "A", 4, "abcdAef", 5), # valid character + ], +) +def test_inbs004_cursor_position_on_invalid_input( + dash_dcc, + initial_text, + invalid_char, + cursor_position_before, + expected_text, + expected_cursor_position, +): + app = Dash(__name__) + + app.layout = html.Div( + [ + dcc.Input( + id="test-input", + type="text", + placeholder="File name", + className="create_file_input", + pattern="[a-zA-Z_][a-zA-Z0-9_]*", + ), + html.Div(id="output"), + ] + ) + + dash_dcc.start_server(app) + input_elem = dash_dcc.find_element("#test-input") + + input_elem.send_keys(initial_text) + assert ( + input_elem.get_attribute("value") == initial_text + ), "Initial text should match" + + dash_dcc.driver.execute_script( + f""" + var elem = arguments[0]; + elem.setSelectionRange({cursor_position_before}, {cursor_position_before}); + elem.focus(); + """, + input_elem, + ) + + input_elem.send_keys(invalid_char) + + assert ( + input_elem.get_attribute("value") == expected_text + ), f"Input should be {expected_text}" + + cursor_position = dash_dcc.driver.execute_script( + """ + var elem = arguments[0]; + return elem.selectionStart; + """, + input_elem, + ) + + assert ( + cursor_position == expected_cursor_position + ), f"Cursor should be at position {expected_cursor_position}" + + assert dash_dcc.get_logs() == []