diff --git a/src/panel_material_ui/widgets/Autocomplete.jsx b/src/panel_material_ui/widgets/Autocomplete.jsx index 4ac2368..b897098 100644 --- a/src/panel_material_ui/widgets/Autocomplete.jsx +++ b/src/panel_material_ui/widgets/Autocomplete.jsx @@ -6,17 +6,36 @@ export function render({model, el}) { const [value, setValue] = model.useState("value") const [options] = model.useState("options") const [label] = model.useState("label") + const [restrict] = model.useState("restrict") const [variant] = model.useState("variant") const [disabled] = model.useState("disabled") + function CustomPopper(props) { return } + + const filt_func = (options, state) => { + let input = state.inputValue + if (input.length < model.min_characters) { + return [] + } + return options.filter((opt) => { + if (!model.case_sensitive) { + opt = opt.toLowerCase() + input = input.toLowerCase() + } + return model.search_strategy == "includes" ? opt.includes(input) : opt.startsWith(input) + }) + } + return ( setValue(newValue)} options={options} disabled={disabled} + freeSolo={!restrict} + filterOptions={filt_func} variant={variant} PopperComponent={CustomPopper} renderInput={(params) => } diff --git a/src/panel_material_ui/widgets/select.py b/src/panel_material_ui/widgets/select.py index c0e8524..61f9757 100644 --- a/src/panel_material_ui/widgets/select.py +++ b/src/panel_material_ui/widgets/select.py @@ -1,6 +1,8 @@ from __future__ import annotations import param +from panel.util import isIn +from panel.widgets.base import Widget from panel.widgets.select import ( SingleSelectBase as _PnSingleSelectBase, ) @@ -45,6 +47,24 @@ class AutocompleteInput(MaterialSingleSelectBase): ... ) """ + case_sensitive = param.Boolean(default=True, doc=""" + Enable or disable case sensitivity.""") + + min_characters = param.Integer(default=2, doc=""" + The number of characters a user must type before + completions are presented.""") + + restrict = param.Boolean(default=True, doc=""" + Set to False in order to allow users to enter text that is not + present in the list of completion strings.""") + + search_strategy = param.Selector(default='starts_with', + objects=['starts_with', 'includes'], doc=""" + Define how to search the list of completion strings. The default option + `"starts_with"` means that the user's text must match the start of a + completion string. Using `"includes"` means that the user's text can + match any substring of a completion string.""") + variant = param.Selector(objects=["filled", "outlined", "standard"], default="outlined") _allows_none = True @@ -56,8 +76,22 @@ class AutocompleteInput(MaterialSingleSelectBase): def _process_property_change(self, msg): if 'value' in msg and msg['value'] is None: return msg + if not self.restrict and 'value' in msg: + try: + return super()._process_property_change(msg) + except Exception: + return Widget._process_property_change(self, msg) return super()._process_property_change(msg) + def _process_param_change(self, msg): + if 'value' in msg and not self.restrict and not isIn(msg['value'], self.values): + with param.parameterized.discard_events(self): + props = super()._process_param_change(msg) + self.value = props['value'] = msg['value'] + else: + props = super()._process_param_change(msg) + return props + class Select(MaterialSingleSelectBase): """ diff --git a/tests/ui/widgets/test_select.py b/tests/ui/widgets/test_select.py index 91bbb98..5a29f8d 100644 --- a/tests/ui/widgets/test_select.py +++ b/tests/ui/widgets/test_select.py @@ -10,92 +10,195 @@ pytestmark = pytest.mark.ui +def test_autocomplete_input_value_updates(page): + widget = AutocompleteInput(name='Autocomplete Input test', options=["Option 1", "Option 2", "123"]) + serve_component(page, widget) + + expect(page.locator(".autocomplete-input")).to_have_count(1) + + page.locator("input").fill("Option 2") + page.locator(".MuiAutocomplete-option").click() + + wait_until(lambda: widget.value == 'Option 2', page) + +def test_autocomplete_input_value_updates_unrestricted(page): + widget = AutocompleteInput(name='Autocomplete Input test', options=["Option 1", "Option 2", "123"], restrict=False) + serve_component(page, widget) + + expect(page.locator(".autocomplete-input")).to_have_count(1) + + page.locator("input").fill("Option 3") + page.locator("input").press("Enter") + + wait_until(lambda: widget.value == 'Option 3', page) + @pytest.mark.parametrize('variant', ["filled", "outlined", "standard"]) -def test_autocomplete_input_format(page, variant): +def test_autocomplete_input_variant(page, variant): widget = AutocompleteInput(name='Autocomplete Input test', variant=variant, options=["Option 1", "Option 2", "123"]) serve_component(page, widget) - ai = page.locator(".autocomplete-input") - wait_until(lambda: expect(ai).to_have_count(1), page=page, timeout=20000) - ai_format = page.locator(f"div[variant='{variant}']") - wait_until(lambda: expect(ai_format).to_have_count(1), page=page, timeout=20000) + + expect(page.locator(".autocomplete-input")).to_have_count(1) + expect(page.locator(f"div[variant='{variant}']")).to_have_count(1) + +def test_autocomplete_input_search_strategy(page): + widget = AutocompleteInput(name='Autocomplete Input test', options=["Option 1", "Option 2", "123"]) + serve_component(page, widget) + + expect(page.locator(".autocomplete-input")).to_have_count(1) + + page.locator("input").fill("Option") + expect(page.locator(".MuiAutocomplete-option")).to_have_count(2) + + page.locator("input").fill("ti") + expect(page.locator(".MuiAutocomplete-option")).to_have_count(0) + + widget.search_strategy = "includes" + page.locator("input").fill("tion") + expect(page.locator(".MuiAutocomplete-option")).to_have_count(2) + +def test_autocomplete_input_case_sensitive(page): + widget = AutocompleteInput(name='Autocomplete Input test', options=["Option 1", "Option 2", "123"]) + serve_component(page, widget) + + expect(page.locator(".autocomplete-input")).to_have_count(1) + + page.locator("input").fill("opt") + expect(page.locator(".MuiAutocomplete-option")).to_have_count(0) + + widget.case_sensitive = False + + page.locator("input").fill("option") + expect(page.locator(".MuiAutocomplete-option")).to_have_count(2) + +def test_autocomplete_min_characters(page): + widget = AutocompleteInput(name='Autocomplete Input test', options=["Option 1", "Option 2", "123"]) + serve_component(page, widget) + + expect(page.locator(".autocomplete-input")).to_have_count(1) + + page.locator("input").fill("O") + expect(page.locator(".MuiAutocomplete-option")).to_have_count(0) + page.locator("input").fill("") + + widget.min_characters = 1 + + page.locator("input").fill("O") + expect(page.locator(".MuiAutocomplete-option")).to_have_count(2) @pytest.mark.parametrize('variant', ["filled", "outlined", "standard"]) -def test_select_format(page, variant): +def test_select_variant(page, variant): widget = Select(name='Select test', variant=variant, options=["Option 1", "Option 2", "Option 3"]) serve_component(page, widget) - select = page.locator(".select") - wait_until(lambda: expect(select).to_have_count(1), page=page) - select_format = page.locator(f".MuiSelect-{variant}") - expect(select_format).to_have_count(1) + + expect(page.locator(".select")).to_have_count(1) + expect(page.locator(f".MuiSelect-{variant}")).to_have_count(1) @pytest.mark.parametrize('color', ["primary", "secondary", "error", "info", "success", "warning"]) +def test_radio_box_group_color(page, color): + widget = RadioBoxGroup(name='RadioBoxGroup test', options=["Option 1", "Option 2", "Option 3"], color=color) + serve_component(page, widget) + + expect(page.locator(".radio-box-group")).to_have_count(1) + expect(page.locator(f".MuiRadio-color{color.capitalize()}")).to_have_count(len(widget.options)) + + @pytest.mark.parametrize('orientation', ["horizontal", "vertical"]) -def test_radio_box_group_format(page, color, orientation): - widget = RadioBoxGroup(name='RadioBoxGroup test', options=["Option 1", "Option 2", "Option 3"], color=color, orientation=orientation) +def test_radio_box_group_orientation(page, orientation): + widget = RadioBoxGroup(name='RadioBoxGroup test', options=["Option 1", "Option 2", "Option 3"], orientation=orientation) serve_component(page, widget) - rbg = page.locator(".radio-box-group") - wait_until(lambda: expect(rbg).to_have_count(1), page=page) - rbg_color = page.locator(f".MuiRadio-color{color.capitalize()}") - expect(rbg_color).to_have_count(len(widget.options)) + + expect(page.locator(".radio-box-group")).to_have_count(1) if orientation == "horizontal": rbg_orient = page.locator(".MuiRadioGroup-row") expect(rbg_orient).to_have_count(1) @pytest.mark.parametrize('color', ["primary", "secondary", "error", "info", "success", "warning"]) -@pytest.mark.parametrize('orientation', ["horizontal", "vertical"]) -@pytest.mark.parametrize('size', ["small", "medium", "large"]) -def test_radio_button_group_format(page, color, orientation, size): +def test_radio_button_group_color(page, color): widget = RadioButtonGroup( name='RadioButtonGroup test', options=["Option 1", "Option 2", "Option 3"], - color=color, - orientation=orientation, - size=size, + color=color ) serve_component(page, widget) - rbg = page.locator(".radio-button-group") - wait_until(lambda: expect(rbg).to_have_count(1), page=page) - # group level - rbg_orient = page.locator(f".MuiToggleButtonGroup-{orientation}") - expect(rbg_orient).to_have_count(1) - # option level + expect(page.locator(".radio-button-group")).to_have_count(1) if color == "error": option_color = page.locator(f".Mui-{color}") else: option_color = page.locator(f".MuiToggleButton-{color}") - option_size = page.locator(f".MuiToggleButton-size{size.capitalize()}") expect(option_color).to_have_count(len(widget.options)) - expect(option_size).to_have_count(len(widget.options)) -@pytest.mark.parametrize('color', ["primary", "secondary", "error", "info", "success", "warning"]) @pytest.mark.parametrize('orientation', ["horizontal", "vertical"]) +def test_radio_button_group_orientation(page, orientation): + widget = RadioButtonGroup( + name='RadioButtonGroup test', + options=["Option 1", "Option 2", "Option 3"], + orientation=orientation + ) + serve_component(page, widget) + + expect(page.locator(".radio-button-group")).to_have_count(1) + expect(page.locator(f".MuiToggleButtonGroup-{orientation}")).to_have_count(1) + + @pytest.mark.parametrize('size', ["small", "medium", "large"]) -def test_check_button_group_format(page, color, orientation, size): +def test_radio_button_group_size(page, size): + widget = RadioButtonGroup( + name='RadioButtonGroup test', + options=["Option 1", "Option 2", "Option 3"], + size=size + ) + serve_component(page, widget) + + expect(page.locator(".radio-button-group")).to_have_count(1) + expect(page.locator(f".MuiToggleButton-size{size.capitalize()}")).to_have_count(len(widget.options)) + + +@pytest.mark.parametrize('color', ["primary", "secondary", "error", "info", "success", "warning"]) +def test_check_button_group_color(page, color): widget = CheckButtonGroup( name='CheckButtonGroup test', value=[], options=["Option 1", "Option 2", "Option 3"], - color=color, - orientation=orientation, - size=size, + color=color ) serve_component(page, widget) - cbg = page.locator(".check-button-group") - wait_until(lambda: expect(cbg).to_have_count(1), page=page) - # group level - cbg_orient = page.locator(f".MuiToggleButtonGroup-{orientation}") - expect(cbg_orient).to_have_count(1) - # option level + expect(page.locator(".check-button-group")).to_have_count(1) if color == "error": option_color = page.locator(f".Mui-{color}") else: option_color = page.locator(f".MuiToggleButton-{color}") - option_size = page.locator(f".MuiToggleButton-size{size.capitalize()}") expect(option_color).to_have_count(len(widget.options)) - expect(option_size).to_have_count(len(widget.options)) + + +@pytest.mark.parametrize('orientation', ["horizontal", "vertical"]) +def test_check_button_group_orientation(page, orientation): + widget = CheckButtonGroup( + name='CheckButtonGroup test', + value=[], + options=["Option 1", "Option 2", "Option 3"], + orientation=orientation + ) + serve_component(page, widget) + + expect(page.locator(".check-button-group")).to_have_count(1) + expect(page.locator(f".MuiToggleButtonGroup-{orientation}")).to_have_count(1) + + +@pytest.mark.parametrize('size', ["small", "medium", "large"]) +def test_check_button_group_size(page, size): + widget = CheckButtonGroup( + name='CheckButtonGroup test', + value=[], + options=["Option 1", "Option 2", "Option 3"], + size=size + ) + serve_component(page, widget) + + expect(page.locator(".check-button-group")).to_have_count(1) + expect(page.locator(f".MuiToggleButton-size{size.capitalize()}")).to_have_count(len(widget.options))