From 0ad82c9566c377f892c4f45db75911e4d1b79b05 Mon Sep 17 00:00:00 2001 From: Tyler Baur Date: Fri, 29 Mar 2024 10:04:13 -0500 Subject: [PATCH 1/2] add close on select prop --- .../src/components/Dropdown.react.js | 7 ++ .../src/fragments/Dropdown.react.js | 7 +- .../dropdown/test_close_on_select.py | 113 ++++++++++++++++++ 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 components/dash-core-components/tests/integration/dropdown/test_close_on_select.py diff --git a/components/dash-core-components/src/components/Dropdown.react.js b/components/dash-core-components/src/components/Dropdown.react.js index 37111fc338..28382e83a6 100644 --- a/components/dash-core-components/src/components/Dropdown.react.js +++ b/components/dash-core-components/src/components/Dropdown.react.js @@ -118,6 +118,12 @@ Dropdown.propTypes = { */ multi: PropTypes.bool, + /** + * If false and multi=true, then selecting a value will leave the menu open + * to select more values + */ + close_on_select: PropTypes.bool, + /** * Whether or not the dropdown is "clearable", that is, whether or * not a small "x" appears on the right of the dropdown that removes @@ -229,6 +235,7 @@ Dropdown.defaultProps = { clearable: true, disabled: false, multi: false, + close_on_select: true, searchable: true, optionHeight: 35, maxHeight: 200, diff --git a/components/dash-core-components/src/fragments/Dropdown.react.js b/components/dash-core-components/src/fragments/Dropdown.react.js index 0464fb8ff4..5aef204729 100644 --- a/components/dash-core-components/src/fragments/Dropdown.react.js +++ b/components/dash-core-components/src/fragments/Dropdown.react.js @@ -24,6 +24,7 @@ const TOKENIZER = { const RDProps = [ 'multi', + 'close_on_select', 'clearable', 'searchable', 'search_value', @@ -41,6 +42,7 @@ const Dropdown = props => { clearable, searchable, multi, + close_on_select, options, setProps, style, @@ -53,7 +55,9 @@ const Dropdown = props => { if (!persistentOptions || !isEqual(options, persistentOptions.current)) { persistentOptions.current = options; } - + if (!multi) { + setProps({close_on_select: true}); + } const [sanitizedOptions, filterOptions] = useMemo(() => { let sanitized = sanitizeOptions(options); @@ -155,6 +159,7 @@ const Dropdown = props => { filterOptions={filterOptions} options={sanitizedOptions} value={value} + closeOnSelect={close_on_select} onChange={onChange} onInputChange={onInputChange} backspaceRemoves={clearable} diff --git a/components/dash-core-components/tests/integration/dropdown/test_close_on_select.py b/components/dash-core-components/tests/integration/dropdown/test_close_on_select.py new file mode 100644 index 0000000000..d24862b6b1 --- /dev/null +++ b/components/dash-core-components/tests/integration/dropdown/test_close_on_select.py @@ -0,0 +1,113 @@ +import json + +from dash import Dash, Input, Output, dcc, html +from selenium.webdriver.common.keys import Keys + + +def test_ddcos001_multi_stay_open(dash_dcc): + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.Dropdown( + id="multi-dropdown", + options=[ + {"label": "New York City", "value": "NYC"}, + {"label": "Montreal", "value": "MTL"}, + {"label": "San Francisco", "value": "SF"}, + ], + multi=True, + close_on_select=False, + ), + html.Div(id="dropdown-value", style={"height": "10px", "width": "10px"}), + ] + ) + + @app.callback( + Output("dropdown-value", "children"), + [Input("multi-dropdown", "value")], + ) + def update_value(val): + return json.dumps([v for v in val]) + + dash_dcc.start_server(app) + dropdown = dash_dcc.find_element("#multi-dropdown") + dropdown.click() + outer_menu = dash_dcc.find_element("#multi-dropdown .Select-menu-outer") + outer_menu.click() + dash_dcc.wait_for_contains_class("#multi-dropdown .Select", "is-open") + outer_menu.click() + dash_dcc.wait_for_contains_class("#multi-dropdown .Select", "is-open") + dash_dcc.find_element("body").send_keys(Keys.ESCAPE) + + dash_dcc.wait_for_text_to_equal("#dropdown-value", '["MTL", "SF"]') + + +def test_ddcos002_multi_close(dash_dcc): + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.Dropdown( + id="multi-dropdown", + options=[ + {"label": "New York City", "value": "NYC"}, + {"label": "Montreal", "value": "MTL"}, + {"label": "San Francisco", "value": "SF"}, + ], + multi=True, + close_on_select=True, + ), + html.Div(id="dropdown-value", style={"height": "10px", "width": "10px"}), + ] + ) + + @app.callback( + Output("dropdown-value", "children"), + [Input("multi-dropdown", "value")], + ) + def update_value(val): + return json.dumps([v for v in val]) + + dash_dcc.start_server(app) + dropdown = dash_dcc.find_element("#multi-dropdown") + dropdown.click() + outer_menu = dash_dcc.find_element("#multi-dropdown .Select-menu-outer") + outer_menu.click() + dash_dcc.wait_for_no_elements("#multi-dropdown .Select-menu-outer") + dash_dcc.find_element("body").send_keys(Keys.ESCAPE) + + dash_dcc.wait_for_text_to_equal("#dropdown-value", '["MTL"]') + + +def test_ddcos003_single_open(dash_dcc): + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.Dropdown( + id="multi-dropdown", + options=[ + {"label": "New York City", "value": "NYC"}, + {"label": "Montreal", "value": "MTL"}, + {"label": "San Francisco", "value": "SF"}, + ], + close_on_select=True, + ), + html.Div(id="dropdown-value", style={"height": "10px", "width": "10px"}), + ] + ) + + @app.callback( + Output("dropdown-value", "children"), + [Input("multi-dropdown", "value")], + ) + def update_value(val): + return json.dumps(val) + + dash_dcc.start_server(app) + dropdown = dash_dcc.find_element("#multi-dropdown") + dropdown.click() + outer_menu = dash_dcc.find_element("#multi-dropdown .Select-menu-outer") + outer_menu.click() + dash_dcc.wait_for_no_elements("#multi-dropdown .Select-menu-outer") + dash_dcc.find_element("body").send_keys(Keys.ESCAPE) + + dash_dcc.wait_for_text_to_equal("#dropdown-value", '"MTL"') From bc1d0d29bd8edefc57a629b4750c31da3bb1763f Mon Sep 17 00:00:00 2001 From: Tyler Baur Date: Fri, 29 Mar 2024 11:28:13 -0500 Subject: [PATCH 2/2] do not save the prop if not multi, just pass in true --- .../dash-core-components/src/fragments/Dropdown.react.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/components/dash-core-components/src/fragments/Dropdown.react.js b/components/dash-core-components/src/fragments/Dropdown.react.js index 5aef204729..ca9f1982f1 100644 --- a/components/dash-core-components/src/fragments/Dropdown.react.js +++ b/components/dash-core-components/src/fragments/Dropdown.react.js @@ -55,9 +55,7 @@ const Dropdown = props => { if (!persistentOptions || !isEqual(options, persistentOptions.current)) { persistentOptions.current = options; } - if (!multi) { - setProps({close_on_select: true}); - } + const [sanitizedOptions, filterOptions] = useMemo(() => { let sanitized = sanitizeOptions(options); @@ -159,7 +157,7 @@ const Dropdown = props => { filterOptions={filterOptions} options={sanitizedOptions} value={value} - closeOnSelect={close_on_select} + closeOnSelect={multi ? close_on_select : true} onChange={onChange} onInputChange={onInputChange} backspaceRemoves={clearable}