feat: DH-19683: multi-select combo box component in deephaven.ui#1347
feat: DH-19683: multi-select combo box component in deephaven.ui#1347mofojed wants to merge 13 commits into
Conversation
|
ui docs preview (Available for 14 days) |
|
ui docs preview (Available for 14 days) |
There was a problem hiding this comment.
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_boxselection-mode handling,selected_keysprops, callback wrapping, and deprecation warnings. - Adds frontend
MultiSelectelement, 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_keysanddefault_selected_keysin the default single-select mode even though the new public API and docs tell users to use those props as the replacement forselected_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_changeand 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_selectionpassesNonethrough unchanged and the publicon_selection_changetype allowsSelection | None. The docs should mention theNonecase 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 passesNonethrough andon_selection_changeis typed asSelection | 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_keyusage intentionally leaves callbacks receiving a singleKey, and the new wrapper passesNonethrough on clear for both callbacks; however the public signature only advertisesSelectionforon_changeand never accepts aKeycallback 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.
| def wrapper(value: Any) -> None: | ||
| if isinstance(value, (str, int, float, bool)): | ||
| callback([value]) | ||
| else: | ||
| callback(value) |
There was a problem hiding this comment.
Reworked to call wrap_callable
There was a problem hiding this comment.
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
Nonethrough when the underlying ComboBox emits no selection. That mismatch will mislead users writing clear-selection handlers; the documentation should either mentionNoneor 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
selectedto[""]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 newselected_keysAPI, 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([""])
| for cb_name in _SELECTION_CALLBACKS: | ||
| cb = props.get(cb_name) | ||
| if cb is not None: | ||
| props[cb_name] = _wrap_callback_as_selection(cb) |
There was a problem hiding this comment.
Switching to only when keys are provided
| 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 |
| wrapped = wrap_callable(callback) | ||
|
|
||
| def wrapper(value: Any) -> None: | ||
| if isinstance(value, (str, int, float, bool)): |
There was a problem hiding this comment.
Added case for on_change None handling
|
ui docs preview (Available for 14 days) |
Add
"multiple"selection_modeoption forcombo_box. Depends on deephaven/web-client-ui#2671, so won't pass or merge until that is merged.