Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions components/dash-core-components/src/fragments/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const Dropdown = (props: DropdownProps) => {
setProps,
searchable,
search_value,
search_order,
style,
value,
} = props;
Expand Down Expand Up @@ -81,9 +82,9 @@ const Dropdown = (props: DropdownProps) => {
const filteredOptions = useMemo(
() =>
searchable
? filterOptions(sanitized, search_value)
? filterOptions(sanitized, search_value, search_order)
: sanitizedOptions,
[sanitized, searchable, search_value]
[sanitized, searchable, search_value, search_order]
);

const sanitizedValues: OptionValue[] = useMemo(() => {
Expand Down
7 changes: 7 additions & 0 deletions components/dash-core-components/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,13 @@ export interface DropdownProps extends BaseDccProps<DropdownProps> {
* Use with `closeOnSelect=False`
*/
debounce?: boolean;

/**
* The order in which to search results appear. 'index' (the default) means that
* options are presented based on search relevance, while 'original' keeps the
* order of options as they were originally provided.
*/
search_order?: 'index' | 'original';
}

export interface ChecklistProps extends BaseDccProps<ChecklistProps> {
Expand Down
13 changes: 11 additions & 2 deletions components/dash-core-components/src/utils/dropdownSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ export function sanitizeDropdownOptions(

export function filterOptions(
options: SanitizedOptions,
searchValue?: string
searchValue?: string,
search_order?: 'index' | 'original'
): DetailedOption[] {
if (!searchValue) {
return options.options;
Expand All @@ -79,5 +80,13 @@ export function filterOptions(
search.addDocuments(options.options);
}

return (search.search(searchValue) as DetailedOption[]) || [];
const searchResults =
(search.search(searchValue) as DetailedOption[]) || [];

if (search_order === 'original') {
const resultSet = new Set(searchResults);
return options.options.filter(option => resultSet.has(option));
}

return searchResults;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
from dash import Dash, html, dcc, Input, Output
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from time import sleep


def test_ddso001_search_preserves_custom_order(dash_duo):
app = Dash(__name__)

app.layout = html.Div(
[
dcc.Dropdown(
id="dropdown",
options=["11 Text", "12", "23", "112", "111", "110", "22"],
searchable=True,
search_order="original",
),
html.Div(id="output"),
]
)

dash_duo.start_server(app)

dropdown = dash_duo.find_element("#dropdown")
dropdown.click()
dash_duo.wait_for_element(".dash-dropdown-options")

# Search for '11'
search_input = dash_duo.find_element(".dash-dropdown-search")
search_input.send_keys("11")
sleep(0.2)

# Presents matching options in original order
options = dash_duo.find_elements(".dash-dropdown-option")
assert len(options) == 4
assert [opt.text for opt in options] == ["11 Text", "112", "111", "110"]

assert dash_duo.get_logs() == []


def test_ddso002_multi_search_preserves_custom_order(dash_duo):
def send_keys(key):
ActionChains(dash_duo.driver).send_keys(key).perform()

app = Dash(__name__)
app.layout = html.Div(
[
dcc.Dropdown(
id="dropdown",
options=["11 Text", "12", "112", "111", "110"],
multi=True,
searchable=True,
search_order="original",
),
html.Div(id="output"),
]
)

@app.callback(Output("output", "children"), Input("dropdown", "value"))
def update_output(value):
return f"Selected: {value}"

dash_duo.start_server(app)

dropdown = dash_duo.find_element("#dropdown")
dropdown.click()
dash_duo.wait_for_element(".dash-dropdown-options")

# Select '12' (second option)
send_keys(Keys.ARROW_DOWN)
sleep(0.2)
send_keys(Keys.ARROW_DOWN)
sleep(0.2)
send_keys(Keys.SPACE)
dash_duo.wait_for_text_to_equal("#output", "Selected: ['12']")
sleep(0.2)

# Select '111' (fourth option)
send_keys(Keys.ARROW_DOWN)
sleep(0.2)
send_keys(Keys.ARROW_DOWN)
sleep(0.2)
send_keys(Keys.SPACE)
dash_duo.wait_for_text_to_equal("#output", "Selected: ['12', '111']")
sleep(0.2)

# Search for '1'
send_keys(Keys.HOME)
sleep(0.2)
send_keys("1")
sleep(0.2)

# Presents selected options first and rest in original order
options = dash_duo.find_elements(".dash-dropdown-option")
assert len(options) == 5
assert [opt.text for opt in options] == ["12", "111", "11 Text", "112", "110"]

assert dash_duo.get_logs() == []


def test_ddso003_search_preserves_custom_order_full_list(dash_duo):
app = Dash(__name__)

app.layout = html.Div(
[
dcc.Dropdown(
id="dropdown",
options=["A", "Zebra", "Apply", "Apple"],
searchable=True,
search_order="original",
),
html.Div(id="output"),
]
)
dash_duo.start_server(app)

dropdown = dash_duo.find_element("#dropdown")
dropdown.click()

search_input = dash_duo.find_element(".dash-dropdown-search")

# Search for 'A', returns all options
search_input.send_keys("A")
sleep(0.2)

# Presents all options in original order
options = dash_duo.find_elements(".dash-dropdown-option")
assert len(options) == 4
assert [opt.text for opt in options] == ["A", "Zebra", "Apply", "Apple"]

assert dash_duo.get_logs() == []


def test_ddso004_search_no_match(dash_duo):
app = Dash(__name__)

app = Dash(__name__)
app.layout = html.Div(
[
dcc.Dropdown(
id="dropdown",
options=["11 Text", "12", "110", "111", "112"],
searchable=True,
search_order="original",
),
html.Div(id="output"),
]
)
dash_duo.start_server(app)

dropdown = dash_duo.find_element("#dropdown")
dropdown.click()

search_input = dash_duo.find_element(".dash-dropdown-search")

# Search for 'A', returns no options
search_input.send_keys("A")
sleep(0.2)

options = dash_duo.find_elements(".dash-dropdown-option")

assert len(options) == 1
assert [opt.text for opt in options] == ["No options found"]
assert dash_duo.get_logs() == []
Loading