diff --git a/examples/chips_on_blur/README.md b/examples/chips_on_blur/README.md new file mode 100644 index 0000000000..638db5f93e --- /dev/null +++ b/examples/chips_on_blur/README.md @@ -0,0 +1,8 @@ +# Input Chips with Blur Support + +Demonstrates how `ui.input_chips` automatically adds typed values as chips when the input field loses focus (blur event), in addition to the Enter key behavior. + +The example shows all three `new_value_mode` options: +- **toggle**: Adds value if absent, removes if present +- **add**: Always adds values (allows duplicates) +- **add-unique**: Only adds if not already present diff --git a/examples/chips_on_blur/main.py b/examples/chips_on_blur/main.py new file mode 100644 index 0000000000..96d6220bf6 --- /dev/null +++ b/examples/chips_on_blur/main.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Demonstrates input_chips blur functionality. + +This example shows how input_chips automatically adds typed values as chips +when the field loses focus (blur event), in addition to the default Enter key behavior. +""" +from nicegui import ui + +ui.markdown('## Input Chips with Blur Support') +ui.markdown(''' +Type a value and press **Tab** or **click elsewhere** to add it as a chip. +The **Enter** key also works (original behavior). +''') + +ui.separator() + +# Toggle mode (default) +with ui.card().classes('w-full'): + ui.label('Toggle Mode (default)').classes('text-h6') + ui.markdown('Adding the same value twice **removes** it.') + chips_toggle = ui.input_chips(label='Try typing "test" twice', new_value_mode='toggle') + ui.label().bind_text_from(chips_toggle, 'value', lambda v: f'Chips: {v}') + +ui.separator() + +# Add mode +with ui.card().classes('w-full'): + ui.label('Add Mode').classes('text-h6') + ui.markdown('**Allows duplicate** values.') + chips_add = ui.input_chips(label='Try typing "test" twice', new_value_mode='add') + ui.label().bind_text_from(chips_add, 'value', lambda v: f'Chips: {v}') + +ui.separator() + +# Add-unique mode +with ui.card().classes('w-full'): + ui.label('Add-Unique Mode').classes('text-h6') + ui.markdown('**Prevents duplicate** values.') + chips_unique = ui.input_chips(label='Try typing "test" twice', new_value_mode='add-unique') + ui.label().bind_text_from(chips_unique, 'value', lambda v: f'Chips: {v}') + +ui.separator() + +ui.button('Click to trigger blur', icon='touch_app') + +ui.run(port=8087) diff --git a/nicegui/elements/input_chips.py b/nicegui/elements/input_chips.py index fda747b5cc..7b167947e9 100644 --- a/nicegui/elements/input_chips.py +++ b/nicegui/elements/input_chips.py @@ -22,7 +22,14 @@ def __init__(self, """Input Chips An input field that manages a collection of values as visual "chips" or tags. - Users can type to add new chips and remove existing ones by clicking or using keyboard shortcuts. + Users can add new chips by pressing Enter or when the input field loses focus (Tab, click away). + Chips can be removed by clicking the "x" icon on each chip. + + The ``new_value_mode`` parameter controls how duplicate values are handled: + + - ``'add'``: Always adds the value (allows duplicates) + - ``'add-unique'``: Only adds if not already present + - ``'toggle'``: Adds if absent, removes if present This element is based on Quasar's `QSelect `_ component. Unlike a traditional dropdown selection, this variant focuses on free-form text input with chips, @@ -55,5 +62,51 @@ def __init__(self, self._props['hide-dropdown-icon'] = True self._props['clearable'] = clearable + # Track the current input value to add on blur + self._current_input_value = '' + self._new_value_mode = new_value_mode + + # Listen to input-value changes to track what user is typing + self.on('input-value', self._handle_input_value_change) + + # Listen to blur event to add the current input value as a chip + self.on('blur', self._handle_blur) + + def _handle_input_value_change(self, e: GenericEventArguments) -> None: + """Track the current input value as user types.""" + self._current_input_value = e.args if e.args else '' + + def _handle_blur(self) -> None: + """Add the current input value as a chip when field loses focus.""" + val = self._current_input_value.strip() if isinstance(self._current_input_value, str) else '' + + if not val: + return + + # Get current chips + current_value = self.value if self.value else [] + + # Apply new-value-mode logic + if self._new_value_mode == 'add': + # Always add the value + new_value = [*current_value, val] + elif self._new_value_mode == 'add-unique': + # Only add if not already present + if val not in current_value: + new_value = [*current_value, val] + else: + return + elif self._new_value_mode == 'toggle': + # Toggle: add if not present, remove if present + if val in current_value: + new_value = [v for v in current_value if v != val] + else: + new_value = [*current_value, val] + else: + return + + # Update the value + self.value = new_value + def _event_args_to_value(self, e: GenericEventArguments) -> Any: return e.args or [] diff --git a/tests/test_input_chips.py b/tests/test_input_chips.py index 7da57c7415..cbf7f37e11 100644 --- a/tests/test_input_chips.py +++ b/tests/test_input_chips.py @@ -46,3 +46,38 @@ def page(): screen.should_contain('Too many') else: screen.should_not_contain('Too many') + + +@pytest.mark.parametrize('new_value_mode', ['add', 'add-unique', 'toggle']) +def test_input_chips_blur_adds_value(screen: Screen, new_value_mode: str): + @ui.page('/') + def page(): + chips = ui.input_chips(new_value_mode=new_value_mode) + ui.label().bind_text_from(chips, 'value', lambda v: f'value = {v}') + + screen.open('/') + screen.should_contain('value = []') + + # Type 'x' and trigger blur + screen.find_by_tag('input').send_keys('x') + screen.wait(0.5) + screen.click('value = []') + screen.wait(0.5) + + # Type 'y' twice with blur each time to test new_value_mode behavior: + # - 'add' keeps both: ['x', 'y', 'y'] + # - 'add-unique' deduplicates: ['x', 'y'] + # - 'toggle' removes second 'y': ['x'] + for _ in range(2): + screen.find_by_tag('input').send_keys('y') + screen.wait(0.5) + screen.click('value = ') # Trigger blur + screen.wait(0.5) + + # Verify behavior matches new_value_mode setting + if new_value_mode == 'add': + screen.should_contain("value = ['x', 'y', 'y']") + elif new_value_mode == 'add-unique': + screen.should_contain("value = ['x', 'y']") + elif new_value_mode == 'toggle': + screen.should_contain("value = ['x']")