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))