Skip to content

Commit fc48427

Browse files
authored
Merge branch 'dev' into fix-slider-mark-height
2 parents 3dc4abb + 3d6602f commit fc48427

File tree

6 files changed

+871
-1551
lines changed

6 files changed

+871
-1551
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ This project adheres to [Semantic Versioning](https://semver.org/).
44

55
## [UNRELEASED]
66

7+
## Added
8+
- [#3680](https://github.com/plotly/dash/pull/3680) Added `search_order` prop to `Dropdown` to allow users to preserve original option order during search
9+
710
## Added
811
- [#3523](https://github.com/plotly/dash/pull/3523) Fall back to background callback function names if source cannot be found
912

components/dash-core-components/src/fragments/Dropdown.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const Dropdown = (props: DropdownProps) => {
4141
setProps,
4242
searchable,
4343
search_value,
44+
search_order,
4445
style,
4546
value,
4647
} = props;
@@ -81,9 +82,9 @@ const Dropdown = (props: DropdownProps) => {
8182
const filteredOptions = useMemo(
8283
() =>
8384
searchable
84-
? filterOptions(sanitized, search_value)
85+
? filterOptions(sanitized, search_value, search_order)
8586
: sanitizedOptions,
86-
[sanitized, searchable, search_value]
87+
[sanitized, searchable, search_value, search_order]
8788
);
8889

8990
const sanitizedValues: OptionValue[] = useMemo(() => {

components/dash-core-components/src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,13 @@ export interface DropdownProps extends BaseDccProps<DropdownProps> {
746746
* Use with `closeOnSelect=False`
747747
*/
748748
debounce?: boolean;
749+
750+
/**
751+
* The order in which to search results appear. 'index' (the default) means that
752+
* options are presented based on search relevance, while 'original' keeps the
753+
* order of options as they were originally provided.
754+
*/
755+
search_order?: 'index' | 'original';
749756
}
750757

751758
export interface ChecklistProps extends BaseDccProps<ChecklistProps> {

components/dash-core-components/src/utils/dropdownSearch.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ export function sanitizeDropdownOptions(
6060

6161
export function filterOptions(
6262
options: SanitizedOptions,
63-
searchValue?: string
63+
searchValue?: string,
64+
search_order?: 'index' | 'original'
6465
): DetailedOption[] {
6566
if (!searchValue) {
6667
return options.options;
@@ -79,5 +80,13 @@ export function filterOptions(
7980
search.addDocuments(options.options);
8081
}
8182

82-
return (search.search(searchValue) as DetailedOption[]) || [];
83+
const searchResults =
84+
(search.search(searchValue) as DetailedOption[]) || [];
85+
86+
if (search_order === 'original') {
87+
const resultSet = new Set(searchResults);
88+
return options.options.filter(option => resultSet.has(option));
89+
}
90+
91+
return searchResults;
8392
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
from dash import Dash, html, dcc, Input, Output
2+
from selenium.webdriver.common.keys import Keys
3+
from selenium.webdriver.common.action_chains import ActionChains
4+
from time import sleep
5+
6+
7+
def test_ddso001_search_preserves_custom_order(dash_duo):
8+
app = Dash(__name__)
9+
10+
app.layout = html.Div(
11+
[
12+
dcc.Dropdown(
13+
id="dropdown",
14+
options=["11 Text", "12", "23", "112", "111", "110", "22"],
15+
searchable=True,
16+
search_order="original",
17+
),
18+
html.Div(id="output"),
19+
]
20+
)
21+
22+
dash_duo.start_server(app)
23+
24+
dropdown = dash_duo.find_element("#dropdown")
25+
dropdown.click()
26+
dash_duo.wait_for_element(".dash-dropdown-options")
27+
28+
# Search for '11'
29+
search_input = dash_duo.find_element(".dash-dropdown-search")
30+
search_input.send_keys("11")
31+
sleep(0.2)
32+
33+
# Presents matching options in original order
34+
options = dash_duo.find_elements(".dash-dropdown-option")
35+
assert len(options) == 4
36+
assert [opt.text for opt in options] == ["11 Text", "112", "111", "110"]
37+
38+
assert dash_duo.get_logs() == []
39+
40+
41+
def test_ddso002_multi_search_preserves_custom_order(dash_duo):
42+
def send_keys(key):
43+
ActionChains(dash_duo.driver).send_keys(key).perform()
44+
45+
app = Dash(__name__)
46+
app.layout = html.Div(
47+
[
48+
dcc.Dropdown(
49+
id="dropdown",
50+
options=["11 Text", "12", "112", "111", "110"],
51+
multi=True,
52+
searchable=True,
53+
search_order="original",
54+
),
55+
html.Div(id="output"),
56+
]
57+
)
58+
59+
@app.callback(Output("output", "children"), Input("dropdown", "value"))
60+
def update_output(value):
61+
return f"Selected: {value}"
62+
63+
dash_duo.start_server(app)
64+
65+
dropdown = dash_duo.find_element("#dropdown")
66+
dropdown.click()
67+
dash_duo.wait_for_element(".dash-dropdown-options")
68+
69+
# Select '12' (second option)
70+
send_keys(Keys.ARROW_DOWN)
71+
sleep(0.2)
72+
send_keys(Keys.ARROW_DOWN)
73+
sleep(0.2)
74+
send_keys(Keys.SPACE)
75+
dash_duo.wait_for_text_to_equal("#output", "Selected: ['12']")
76+
sleep(0.2)
77+
78+
# Select '111' (fourth option)
79+
send_keys(Keys.ARROW_DOWN)
80+
sleep(0.2)
81+
send_keys(Keys.ARROW_DOWN)
82+
sleep(0.2)
83+
send_keys(Keys.SPACE)
84+
dash_duo.wait_for_text_to_equal("#output", "Selected: ['12', '111']")
85+
sleep(0.2)
86+
87+
# Search for '1'
88+
send_keys(Keys.HOME)
89+
sleep(0.2)
90+
send_keys("1")
91+
sleep(0.2)
92+
93+
# Presents selected options first and rest in original order
94+
options = dash_duo.find_elements(".dash-dropdown-option")
95+
assert len(options) == 5
96+
assert [opt.text for opt in options] == ["12", "111", "11 Text", "112", "110"]
97+
98+
assert dash_duo.get_logs() == []
99+
100+
101+
def test_ddso003_search_preserves_custom_order_full_list(dash_duo):
102+
app = Dash(__name__)
103+
104+
app.layout = html.Div(
105+
[
106+
dcc.Dropdown(
107+
id="dropdown",
108+
options=["A", "Zebra", "Apply", "Apple"],
109+
searchable=True,
110+
search_order="original",
111+
),
112+
html.Div(id="output"),
113+
]
114+
)
115+
dash_duo.start_server(app)
116+
117+
dropdown = dash_duo.find_element("#dropdown")
118+
dropdown.click()
119+
120+
search_input = dash_duo.find_element(".dash-dropdown-search")
121+
122+
# Search for 'A', returns all options
123+
search_input.send_keys("A")
124+
sleep(0.2)
125+
126+
# Presents all options in original order
127+
options = dash_duo.find_elements(".dash-dropdown-option")
128+
assert len(options) == 4
129+
assert [opt.text for opt in options] == ["A", "Zebra", "Apply", "Apple"]
130+
131+
assert dash_duo.get_logs() == []
132+
133+
134+
def test_ddso004_search_no_match(dash_duo):
135+
app = Dash(__name__)
136+
137+
app = Dash(__name__)
138+
app.layout = html.Div(
139+
[
140+
dcc.Dropdown(
141+
id="dropdown",
142+
options=["11 Text", "12", "110", "111", "112"],
143+
searchable=True,
144+
search_order="original",
145+
),
146+
html.Div(id="output"),
147+
]
148+
)
149+
dash_duo.start_server(app)
150+
151+
dropdown = dash_duo.find_element("#dropdown")
152+
dropdown.click()
153+
154+
search_input = dash_duo.find_element(".dash-dropdown-search")
155+
156+
# Search for 'A', returns no options
157+
search_input.send_keys("A")
158+
sleep(0.2)
159+
160+
options = dash_duo.find_elements(".dash-dropdown-option")
161+
162+
assert len(options) == 1
163+
assert [opt.text for opt in options] == ["No options found"]
164+
assert dash_duo.get_logs() == []

0 commit comments

Comments
 (0)