diff --git a/CHANGELOG.md b/CHANGELOG.md index a1324948a..144e36d57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Fix missing session when trying to display an error duing bookmarking. (#1984) +* Fixed `set()` method of `InputSelectize` controller so it clears existing selections before applying new values. (#2024) + ## [1.4.0] - 2025-04-08 diff --git a/shiny/playwright/controller/_input_controls.py b/shiny/playwright/controller/_input_controls.py index a3fde3dfa..de95c6dd0 100644 --- a/shiny/playwright/controller/_input_controls.py +++ b/shiny/playwright/controller/_input_controls.py @@ -1187,21 +1187,71 @@ def set( """ Sets the selected option(s) of the input selectize. + Selected items are altered as follows: + 1. Click on the selectize input to open the dropdown. + 2. Starting from the first selected item, each position in the currently selected list should match `selected`. If the item is not a match, remove it and try again. + 3. Add any remaining items in `selected` that are not currently selected by clicking on them in the dropdown. + 4. Press the `"Escape"` key to close the dropdown. + Parameters ---------- selected - The value(s) of the selected option(s). + The [ordered] value(s) of the selected option(s). timeout The maximum time to wait for the selection to be set. Defaults to `None`. """ if isinstance(selected, str): selected = [selected] + # Open the dropdown self._loc_events.click() - for value in selected: - self._loc_selectize.locator(f"[data-value='{value}']").click( - timeout=timeout - ) + + # Sift through the selected items + # From left to right, we will remove ordered items that are not in the ordered `selected` + # If any selected items are not in the current selection, we will add them at the end + i = 0 + while i < self._loc_events.locator("> .item").count(): + item_loc = self._loc_events.locator("> .item").nth(i) + item_data_value = item_loc.get_attribute("data-value") + + def delete_item(item_loc: Locator) -> None: + """ + Deletes the item by clicking on it and pressing the Delete key. + """ + item_loc.click() + self.page.keyboard.press("Delete") + + # If the item has no data-value, remove it + if item_data_value is None: + delete_item(item_loc) + continue + + # If there are more items than selected, we need to remove the extra ones + if i >= len(selected): + delete_item(item_loc) + continue + + selected_data_value = selected[i] + + # If the item is not the next `selected` value, remove it + if item_data_value != selected_data_value: + delete_item(item_loc) + continue + + # The item is the next `selected` value + # Increment the index! + i += 1 + + # Add the remaining items + if i < len(selected): + for data_value in selected[i:]: + # Click on the item in the dropdown to select it + self._loc_selectize.locator(f"[data-value='{data_value}']").click( + timeout=timeout + ) + + # Close the dropdown self._loc_events.press("Escape") + return def expect_choices( self, diff --git a/tests/playwright/shiny/bugs/2013-selectize-set-does-not-clear/app_selectize.py b/tests/playwright/shiny/bugs/2013-selectize-set-does-not-clear/app_selectize.py new file mode 100644 index 000000000..ed9fb0ff5 --- /dev/null +++ b/tests/playwright/shiny/bugs/2013-selectize-set-does-not-clear/app_selectize.py @@ -0,0 +1,21 @@ +from shiny import App, render, ui +from shiny.session import Inputs, Outputs, Session + +app_ui = ui.page_fluid( + ui.input_selectize( + "test_selectize", + "Select", + ["Choice 1", "Choice 2", "Choice 3", "Choice 4"], + multiple=True, + ), + ui.output_text("test_selectize_output"), +) + + +def server(input: Inputs, output: Outputs, session: Session) -> None: + @render.text + def test_selectize_output(): + return f"Selected: {', '.join(input.test_selectize())}" + + +app = App(app_ui, server) diff --git a/tests/playwright/shiny/bugs/2013-selectize-set-does-not-clear/test_app_selectize.py b/tests/playwright/shiny/bugs/2013-selectize-set-does-not-clear/test_app_selectize.py new file mode 100644 index 000000000..e09cc9323 --- /dev/null +++ b/tests/playwright/shiny/bugs/2013-selectize-set-does-not-clear/test_app_selectize.py @@ -0,0 +1,29 @@ +from playwright.sync_api import Page + +from shiny.playwright import controller +from shiny.pytest import create_app_fixture +from shiny.run import ShinyAppProc + +app = create_app_fixture("app_selectize.py") + + +def test_inputselectize(page: Page, app: ShinyAppProc): + page.goto(app.url) + + input_selectize = controller.InputSelectize(page, "test_selectize") + output_text = controller.OutputText(page, "test_selectize_output") + + input_selectize.set(["Choice 1", "Choice 2", "Choice 3"]) + output_text.expect_value("Selected: Choice 1, Choice 2, Choice 3") + input_selectize.set(["Choice 2", "Choice 3"]) + output_text.expect_value("Selected: Choice 2, Choice 3") + input_selectize.set(["Choice 2"]) + output_text.expect_value("Selected: Choice 2") + input_selectize.set(["Choice 2", "Choice 3"]) + output_text.expect_value("Selected: Choice 2, Choice 3") + input_selectize.set(["Choice 1", "Choice 2"]) + output_text.expect_value("Selected: Choice 1, Choice 2") + input_selectize.set([]) + output_text.expect_value("Selected: ") + input_selectize.set(["Choice 1", "Choice 3"]) + output_text.expect_value("Selected: Choice 1, Choice 3")