Skip to content

feat: DH-19683: multi-select combo box component in deephaven.ui#1347

Draft
mofojed wants to merge 13 commits into
deephaven:mainfrom
mofojed:DH-19683-multi-select-component-in-deephaven-ui
Draft

feat: DH-19683: multi-select combo box component in deephaven.ui#1347
mofojed wants to merge 13 commits into
deephaven:mainfrom
mofojed:DH-19683-multi-select-component-in-deephaven-ui

Conversation

@mofojed
Copy link
Copy Markdown
Member

@mofojed mofojed commented May 6, 2026

Add "multiple" selection_mode option for combo_box. Depends on deephaven/web-client-ui#2671, so won't pass or merge until that is merged.

@mofojed mofojed self-assigned this May 6, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

ui docs preview (Available for 14 days)

@jnumainville jnumainville assigned jnumainville and unassigned mofojed May 14, 2026
@github-actions
Copy link
Copy Markdown

ui docs preview (Available for 14 days)

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds multi-select support to deephaven.ui.combo_box by routing selection_mode="multiple" to a new frontend MultiSelect element and updating docs/tests around selection APIs.

Changes:

  • Adds Python combo_box selection-mode handling, selected_keys props, callback wrapping, and deprecation warnings.
  • Adds frontend MultiSelect element, prop hook, element registration, and tests.
  • Updates combo box docs and generated snapshots for the new selection API and multi-select example.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
plugins/ui/test/deephaven/ui/test_combo_box.py Adds Python tests for selection prop processing, callback wrapping, warnings, and render target selection.
plugins/ui/src/js/src/widget/WidgetUtils.tsx Registers MultiSelect in the element component map.
plugins/ui/src/js/src/elements/MultiSelect.tsx Adds frontend MultiSelect wrapper with object/table-backed rendering support.
plugins/ui/src/js/src/elements/MultiSelect.test.tsx Adds tests for MultiSelect rendering branches.
plugins/ui/src/js/src/elements/model/ElementConstants.ts Adds MultiSelect element name constant.
plugins/ui/src/js/src/elements/index.ts Exports the new MultiSelect element.
plugins/ui/src/js/src/elements/hooks/useMultiSelectProps.ts Adds hook for MultiSelect event/callback prop deserialization.
plugins/ui/src/js/src/elements/hooks/useMultiSelectProps.test.ts Adds hook tests for callback serialization and passthrough props.
plugins/ui/src/deephaven/ui/components/combo_box.py Adds selection mode, multi-select routing, new selection props, warnings, and callback adaptation.
plugins/ui/docs/components/combo_box.md Updates combo box documentation and examples for selected_keys and multi-select mode.
plugins/ui/docs/snapshots/fa4c431cba8e02a50fb02b9cbf6dd6de.json Removes outdated combo box basic snapshot.
plugins/ui/docs/snapshots/f1891462b585b31dabf2e7d96395c968.json Removes outdated control example snapshot.
plugins/ui/docs/snapshots/ec0ed3d95653ca3d2062db0a5ffcbc68.json Removes outdated manual multi-select snapshot.
plugins/ui/docs/snapshots/a3924153476e117f57e563dacb3b31d5.json Adds updated read-only combo box snapshot.
plugins/ui/docs/snapshots/94ab46014a1ec4241a826e7b08b163ef.json Removes outdated read-only combo box snapshot.
plugins/ui/docs/snapshots/87f46b40b82cb2efc19f5bdbab3ac2a3.json Adds new native multi-select combo box snapshot.
plugins/ui/docs/snapshots/639e179ae0580408f236edba3dd1d3c3.json Adds updated control example snapshot.
plugins/ui/docs/snapshots/2747a4e6e572a96106716910e764ee1d.json Adds updated basic combo box snapshot.
plugins/ui/docs/snapshots/09532531c03ea0d5591a4a03b09161a1.json Adds updated selected keys example snapshot.
plugins/ui/docs/snapshots/0761e766ac976e28061e143e034b23bf.json Removes outdated selected key example snapshot.
Comments suppressed due to low confidence (6)

plugins/ui/src/deephaven/ui/components/combo_box.py:125

  • This strips selected_keys and default_selected_keys in the default single-select mode even though the new public API and docs tell users to use those props as the replacement for selected_key/default_selected_key. As a result, combo_box(selected_keys=[...]) renders an uncontrolled ComboBox with no selection prop, so controlled/default selection stops working in single mode.
    # strip props that don't apply to the active mode
    for prop in _SINGLE_ONLY_PROPS if is_multiple else _MULTI_ONLY_PROPS:
        props.pop(prop, None)

plugins/ui/docs/components/combo_box.md:358

  • This note uses invalid admonition syntax because the body is placed on the same quoted line after a second >. Other docs in this repo put > [!NOTE] on its own line and quote the body separately, so this will not render consistently.
> [!NOTE] > `on_change` and `on_selection_change` receive a `Selection` (list of keys) by default. When the deprecated `selected_key` or `default_selected_key` props are used, callbacks receive a single `Key` instead for backwards compatibility. Eventually the single key props will be removed and callbacks will always receive a list of keys.

plugins/ui/src/deephaven/ui/components/combo_box.py:133

  • This wraps callbacks for every single-select ComboBox that does not explicitly pass the deprecated selection props, including existing code that only used on_change/on_selection_change and never used controlled selection. Those handlers previously received a single key, so this turns a common backwards-compatible upgrade into a callback shape change unless users add deprecated props just to keep old behavior.
    # When not using deprecated key props in single-select mode, wrap
    # callbacks so they receive a Selection instead of a single Key.
    if not is_multiple and not uses_deprecated:
        for cb_name in ("on_selection_change", "on_change"):
            cb = props.get(cb_name)
            if cb is not None:
                props[cb_name] = _wrap_callback_as_selection(cb)

plugins/ui/docs/components/combo_box.md:269

  • The statement that callbacks “always receive a list of keys” is inaccurate for cleared selections: _wrap_callback_as_selection passes None through unchanged and the public on_selection_change type allows Selection | None. The docs should mention the None case so handlers do not assume a list unconditionally.
> [!NOTE] > `selected_key` and `default_selected_key` are deprecated. Use `selected_keys` and `default_selected_keys` instead. When the deprecated props are used, `on_selection_change` and `on_change` continue to receive a single key for backwards compatibility. When using the new props, callbacks always receive a list of keys.

plugins/ui/docs/components/combo_box.md:358

  • This says callbacks receive a list by default, but a cleared single-select ComboBox can still deliver None (the wrapper explicitly passes None through and on_selection_change is typed as Selection | None). Please document that nullable case instead of implying handlers can always iterate a list.
> [!NOTE] > `on_change` and `on_selection_change` receive a `Selection` (list of keys) by default. When the deprecated `selected_key` or `default_selected_key` props are used, callbacks receive a single `Key` instead for backwards compatibility. Eventually the single key props will be removed and callbacks will always receive a list of keys.

plugins/ui/src/deephaven/ui/components/combo_box.py:172

  • The callback type hints do not match the runtime behavior. Deprecated selected_key/default_selected_key usage intentionally leaves callbacks receiving a single Key, and the new wrapper passes None through on clear for both callbacks; however the public signature only advertises Selection for on_change and never accepts a Key callback for the deprecated path.
    on_selection_change: Callable[[Selection | None], None] | None = None,
    on_change: Callable[[Selection], None] | None = None,

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread plugins/ui/docs/components/combo_box.md Outdated
Comment thread plugins/ui/test/deephaven/ui/test_combo_box.py Outdated
Comment thread plugins/ui/docs/components/combo_box.md
Comment on lines +76 to +80
def wrapper(value: Any) -> None:
if isinstance(value, (str, int, float, bool)):
callback([value])
else:
callback(value)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworked to call wrap_callable

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 10 comments.

Comments suppressed due to low confidence (3)

plugins/ui/docs/components/combo_box.md:362

  • This note says callbacks always receive a list when using the new props, but the implementation explicitly passes None through when the underlying ComboBox emits no selection. That mismatch will mislead users writing clear-selection handlers; the documentation should either mention None or the implementation should normalize clears to an empty list.
<!-- prettier-ignore -->
> [!NOTE]
> `on_change` and `on_selection_change` receive a `Selection` (list of keys) by default. When the deprecated `selected_key` or `default_selected_key` props are used, callbacks receive a single `Key` instead for backwards compatibility. Eventually the single key props will be removed and callbacks will always receive a list of keys.

plugins/ui/docs/components/combo_box.md:786

  • For multi-select, an empty selection should be represented as an empty list. Initializing selected to [""] sends an empty-string key as a selected item even though none of the options use that key, so readers copying this controlled example can start with invalid selection state.
@ui.component
def ui_combo_box_multi_select_example():
    selected, set_selected = ui.use_state([""])

plugins/ui/docs/components/combo_box.md:374

  • The controlled selection state is initialized and reset to [""], which is a list containing an empty-string key rather than an empty selection. With the new selected_keys API, clearing the ComboBox should use an empty list so the example does not keep a non-existent key selected in state.
    selection_state, set_selection_state = ui.use_state([""])

    def handle_input_change(new_value):
        set_selection_state([""])

Comment on lines +151 to +154
for cb_name in _SELECTION_CALLBACKS:
cb = props.get(cb_name)
if cb is not None:
props[cb_name] = _wrap_callback_as_selection(cb)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switching to only when keys are provided

Comment on lines +146 to +149
if sel_keys is not None:
props["selected_key"] = sel_keys[0] if sel_keys else None
if def_sel_keys is not None:
props["default_selected_key"] = def_sel_keys[0] if def_sel_keys else None
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding warning

wrapped = wrap_callable(callback)

def wrapper(value: Any) -> None:
if isinstance(value, (str, int, float, bool)):
Copy link
Copy Markdown
Collaborator

@jnumainville jnumainville May 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added case for on_change None handling

Comment thread plugins/ui/docs/components/combo_box.md Outdated
Comment thread plugins/ui/docs/components/combo_box.md Outdated
Comment thread plugins/ui/docs/snapshots/09532531c03ea0d5591a4a03b09161a1.json
Comment thread plugins/ui/docs/snapshots/a3924153476e117f57e563dacb3b31d5.json
Comment thread plugins/ui/docs/snapshots/2747a4e6e572a96106716910e764ee1d.json
Comment thread plugins/ui/docs/snapshots/639e179ae0580408f236edba3dd1d3c3.json Outdated
Comment thread plugins/ui/docs/snapshots/87f46b40b82cb2efc19f5bdbab3ac2a3.json
@github-actions
Copy link
Copy Markdown

ui docs preview (Available for 14 days)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants