diff --git a/demo/select_demo.py b/demo/select_demo.py index 8e23f8140..e67190aff 100644 --- a/demo/select_demo.py +++ b/demo/select_demo.py @@ -3,12 +3,12 @@ @me.stateclass class State: - selected_value: str = "" + selected_values: list[str] def on_selection_change(e: me.SelectSelectionChangeEvent): s = me.state(State) - s.selected_value = e.value + s.selected_values = e.values @me.page( @@ -28,6 +28,7 @@ def app(): ], on_selection_change=on_selection_change, style=me.Style(width=500), + multiple=True, ) s = me.state(State) - me.text(text="Selected value: " + s.selected_value) + me.text(text="Selected values: " + ", ".join(s.selected_values)) diff --git a/mesop/components/select/e2e/__init__.py b/mesop/components/select/e2e/__init__.py index 734ca8341..0bc63fb97 100644 --- a/mesop/components/select/e2e/__init__.py +++ b/mesop/components/select/e2e/__init__.py @@ -1 +1,2 @@ from . import select_app as select_app +from . import select_app_multiple as select_app_multiple diff --git a/mesop/components/select/e2e/select_app_multiple.py b/mesop/components/select/e2e/select_app_multiple.py new file mode 100644 index 000000000..15e62095c --- /dev/null +++ b/mesop/components/select/e2e/select_app_multiple.py @@ -0,0 +1,29 @@ +import mesop as me + + +@me.stateclass +class State: + selected_values: list[str] + + +def on_selection_change(e: me.SelectSelectionChangeEvent): + s = me.state(State) + s.selected_values = e.values + + +@me.page(path="/components/select/e2e/select_app_multiple") +def app(): + me.text(text="Select") + me.select( + label="Select", + options=[ + me.SelectOption(label="label 1", value="value1"), + me.SelectOption(label="label 2", value="value2"), + me.SelectOption(label="label 3", value="value3"), + ], + on_selection_change=on_selection_change, + multiple=True, + style=me.Style(width=500), + ) + s = me.state(State) + me.text(text="Selected values: " + ", ".join(s.selected_values)) diff --git a/mesop/components/select/e2e/select_test.ts b/mesop/components/select/e2e/select_test.ts index 76ff3c0b1..4b50fdebd 100644 --- a/mesop/components/select/e2e/select_test.ts +++ b/mesop/components/select/e2e/select_test.ts @@ -1,11 +1,25 @@ import {test, expect} from '@playwright/test'; -test('test', async ({page}) => { +test('single selection', async ({page}) => { await page.goto('/components/select/e2e/select_app'); + await page.getByRole('combobox').click(); + await page.getByRole('option', {name: 'label 2'}).click(); + await expect(page.getByText('Selected value: value2')).toBeAttached(); + + await page.getByRole('combobox').click(); + await page.getByRole('option', {name: 'label 3'}).click(); + await expect(page.getByText('Selected value: value3')).toBeAttached(); +}); + +test('multiple selection', async ({page}) => { + await page.goto('/components/select/e2e/select_app_multiple'); await page.getByLabel('Select').click(); + await page.getByRole('option', {name: 'label 2'}).click(); + await expect(page.getByText('Selected values: value2')).toBeAttached(); - expect( - await page.getByText('Selected value: value2').textContent(), - ).toContain('Selected value: value2'); + await page.getByRole('option', {name: 'label 1'}).click(); + await expect( + page.getByText('Selected values: value1, value2'), + ).toBeAttached(); }); diff --git a/mesop/components/select/select.ng.html b/mesop/components/select/select.ng.html index f83798d94..6ea1ddcf0 100644 --- a/mesop/components/select/select.ng.html +++ b/mesop/components/select/select.ng.html @@ -8,6 +8,7 @@ [value]="config().getValue()" (openedChange)="onSelectOpenedChangeEvent($event)" (selectionChange)="onSelectSelectionChangeEvent($event)" + [multiple]="config().getMultiple()" > @for(option of config().getOptionsList(); track $index) { {{option.getLabel()}} diff --git a/mesop/components/select/select.proto b/mesop/components/select/select.proto index 73f0d4976..adfb0b7ce 100644 --- a/mesop/components/select/select.proto +++ b/mesop/components/select/select.proto @@ -3,9 +3,14 @@ syntax = "proto2"; package mesop.components.select; +message SelectChangeEvent { + repeated string values = 1; +} + message SelectType { optional bool disabled = 2; optional bool disable_ripple = 3; + optional bool multiple = 18; optional double tab_index = 4; optional string placeholder = 6; optional string value = 9; diff --git a/mesop/components/select/select.py b/mesop/components/select/select.py index df448c8ee..409e7aa6f 100644 --- a/mesop/components/select/select.py +++ b/mesop/components/select/select.py @@ -35,23 +35,33 @@ class SelectOpenedChangeEvent(MesopEvent): @dataclass(kw_only=True) class SelectSelectionChangeEvent(MesopEvent): - """Event representing a change in the select component's value. + """Event representing a change in the select component's value(s). Attributes: - value: The new value of the select component after the change. + values: New values of the select component after the change. key (str): Key of the component that emitted this event. """ - value: str + values: list[str] + @property + def value(self): + """Shortcut for returning a single value.""" + if not self.values: + return "" + return self.values[0] -register_event_mapper( - SelectSelectionChangeEvent, - lambda event, key: SelectSelectionChangeEvent( + +def map_select_change_event(event, key): + select_event = select_pb.SelectChangeEvent() + select_event.ParseFromString(event.bytes_value) + return SelectSelectionChangeEvent( key=key.key, - value=event.string_value, - ), -) + values=list(select_event.values), + ) + + +register_event_mapper(SelectSelectionChangeEvent, map_select_change_event) @dataclass(kw_only=True) @@ -82,6 +92,7 @@ def select( placeholder: str = "", value: str = "", style: Style | None = None, + multiple: bool = False, ): """Creates a Select component. @@ -91,6 +102,7 @@ def select( on_opened_change: Event emitted when the select panel has been toggled. disabled: Whether the select is disabled. disable_ripple: Whether ripples in the select are disabled. + multiple: Whether multiple selections are allowed. tab_index: Tab index of the select. placeholder: Placeholder to be shown if no value has been selected. value: Value of the select control. @@ -109,6 +121,7 @@ def select( label=label, disabled=disabled, disable_ripple=disable_ripple, + multiple=multiple, tab_index=tab_index, placeholder=placeholder, value=value, diff --git a/mesop/components/select/select.ts b/mesop/components/select/select.ts index 7e297c2e1..903c77ad6 100644 --- a/mesop/components/select/select.ts +++ b/mesop/components/select/select.ts @@ -6,7 +6,10 @@ import { Type, Style, } from 'mesop/mesop/protos/ui_jspb_proto_pb/mesop/protos/ui_pb'; -import {SelectType} from 'mesop/mesop/components/select/select_jspb_proto_pb/mesop/components/select/select_pb'; +import { + SelectChangeEvent, + SelectType, +} from 'mesop/mesop/components/select/select_jspb_proto_pb/mesop/components/select/select_pb'; import {Channel} from '../../web/src/services/channel'; import {formatStyle} from '../../web/src/utils/styles'; @@ -45,11 +48,18 @@ export class SelectComponent { onSelectSelectionChangeEvent(event: MatSelectChange): void { const userEvent = new UserEvent(); - userEvent.setHandlerId( this.config().getOnSelectSelectionChangeEventHandlerId()!, ); - userEvent.setStringValue(event.value); + const changeEvent = new SelectChangeEvent(); + if (typeof event.value === 'string') { + changeEvent.addValues(event.value); + } else { + for (const value of event.value) { + changeEvent.addValues(value); + } + } + userEvent.setBytesValue(changeEvent.serializeBinary()); userEvent.setKey(this.key); this.channel.dispatch(userEvent); }