From 9c9c886925a480971fb22e457dc563d879da943d Mon Sep 17 00:00:00 2001 From: gzh2003 Date: Tue, 21 Apr 2026 11:46:54 -0400 Subject: [PATCH 01/20] multiselect plugin wrapper --- .../src/deephaven/ui/components/combo_box.py | 94 +++++++++++++++++-- .../ui/src/js/src/elements/MultiSelect.tsx | 71 ++++++++++++++ .../src/elements/hooks/useMultiSelectProps.ts | 63 +++++++++++++ plugins/ui/src/js/src/elements/index.ts | 1 + .../js/src/elements/model/ElementConstants.ts | 1 + plugins/ui/src/js/src/widget/WidgetUtils.tsx | 2 + 6 files changed, 226 insertions(+), 6 deletions(-) create mode 100644 plugins/ui/src/js/src/elements/MultiSelect.tsx create mode 100644 plugins/ui/src/js/src/elements/hooks/useMultiSelectProps.ts diff --git a/plugins/ui/src/deephaven/ui/components/combo_box.py b/plugins/ui/src/deephaven/ui/components/combo_box.py index 58b3da335..cbb389df8 100644 --- a/plugins/ui/src/deephaven/ui/components/combo_box.py +++ b/plugins/ui/src/deephaven/ui/components/combo_box.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Callable, Any +from typing import Callable, Any, Literal from .types import ( FocusEventCallable, @@ -29,7 +29,7 @@ from .item_table_source import ItemTableSource from ..elements import BaseElement, Element, NodeType from .._internal.utils import create_props, unpack_item_table_source -from ..types import Key, Undefined, UndefinedType +from ..types import Key, Selection, Undefined, UndefinedType from .basic import component_element ComboBoxElement = BaseElement @@ -44,9 +44,56 @@ _NULLABLE_PROPS = ["selected_key"] +# Props that only apply to single-select ComboBox. +_SINGLE_ONLY_PROPS = { + "selected_key", + "default_selected_key", +} + +# Props that only apply to multi-select mode. +_MULTI_ONLY_PROPS = { + "selected_keys", + "default_selected_keys", +} + +# Props that raise a ValueError if explicitly set in the wrong mode. +_SINGLE_ONLY_VALIDATED = { + "selected_key": Undefined, + "default_selected_key": None, +} + +_MULTI_ONLY_VALIDATED = { + "selected_keys": None, + "default_selected_keys": None, +} + + +def _validate_selection_mode(props: dict[str, Any], mode: str) -> None: + """Validate and strip props that conflict with the given selection mode. + + Raises ValueError for props that conflict with the active mode, + and removes props that only apply to the other mode. + + Args: + props: The props to validate. + mode: The active selection mode. + """ + if mode == "multiple": + validated, strip = _SINGLE_ONLY_VALIDATED, _SINGLE_ONLY_PROPS + else: + validated, strip = _MULTI_ONLY_VALIDATED, _MULTI_ONLY_PROPS + + for prop, default in validated.items(): + val = props.get(prop) + if val is not default: + raise ValueError(f"'{prop}' is not supported when selection_mode='{mode}'.") + for prop in strip: + props.pop(prop, None) + def combo_box( *children: Item | SectionElement | Table | PartitionedTable | ItemTableSource, + selection_mode: Literal["single", "multiple"] = "single", menu_trigger: MenuTriggerAction | None = "input", is_quiet: bool | None = None, align: Align | None = "end", @@ -62,6 +109,8 @@ def combo_box( disabled_keys: list[Key] | None = None, selected_key: Key | None | UndefinedType = Undefined, default_selected_key: Key | None = None, + selected_keys: Selection | None = None, + default_selected_keys: Selection | None = None, is_disabled: bool | None = None, is_read_only: bool | None = None, is_required: bool | None = None, @@ -131,7 +180,13 @@ def combo_box( key: str | None = None, ) -> ComboBoxElement: """ - A combo box that can be used to search or select from a list. Children should be one of five types: + A combo box that can be used to search or select from a list. + + When `selection_mode="single"` (default), behaves as a standard ComboBox with a single + selected value. When `selection_mode="multiple"`, displays selected items as tags inside the + input area and presents a filterable dropdown list for multi-selection. + + Children should be one of five types: 1. If children are of type `Item`, they are the dropdown options. 2. If children are of type `SectionElement`, they are the dropdown sections. @@ -148,6 +203,8 @@ def combo_box( Args: *children: The options to render within the combo box. + selection_mode: Whether the combo box allows single or multiple selection. + Defaults to `"single"`. menu_trigger: The interaction required to display the ComboBox menu. is_quiet: Whether the ComboBox should be displayed with a quiet style. align: Alignment of the menu relative to the input target. @@ -157,16 +214,26 @@ def combo_box( should_flip: Whether the menu should automatically flip direction when space is limited. menu_width: Width of the menu. By default, matches width of the combobox. Note that the minimum width of the dropdown is always equal to the combobox's width. - form_value: Whether the text or key of the selected item is submitted as part of an HTML form. - When allowsCustomValue is true, this option does not apply and the text is always submitted. + form_value: Whether the text or key of the selected item(s) is submitted as part of an HTML form. + In single-select mode, when `allows_custom_value` is true, this option does not apply and the + text is always submitted. In multi-select mode, controls whether comma-joined keys or labels + are submitted via the hidden form input. should_focus_wrap: Whether keyboard navigation is circular. input_value: The value of the search input (controlled). default_input_value: The default value of the search input (uncontrolled). allows_custom_value: Whether the ComboBox allows a non-item matching input value to be set. + In multi-select mode, pressing Enter when no item is focused adds the typed text as a custom tag. + If the typed text matches an existing item's label, that item's key is used instead. disabled_keys: The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. selected_key: The currently selected key in the collection (controlled). + Only applies in single-select mode. default_selected_key: The initial selected key in the collection (uncontrolled). + Only applies in single-select mode. + selected_keys: The currently selected keys in the collection (controlled). + Only applies in multi-select mode. + default_selected_keys: The initial selected keys in the collection (uncontrolled). + Only applies in multi-select mode. is_disabled: Whether the input is disabled. is_read_only: Whether the input can be selected but not changed by the user. is_required: Whether user input is required on the input before form submission. @@ -185,6 +252,8 @@ def combo_box( on_open_change: Method that is called when the open state of the menu changes. Returns the new open state and the action that caused the opening of the menu. on_selection_change: Handler that is called when the selection changes. + In single-select mode, receives the selected key. + In multi-select mode, receives the full selection (set of keys). on_change: Alias of `on_selection_change`. Handler that is called when the selection changes. on_input_change: Handler that is called when the ComboBox input value changes. on_focus: Handler that is called when the element receives focus. @@ -237,13 +306,26 @@ def combo_box( UNSAFE_style: A CSS style to apply to the element. key: A unique identifier used by React to render elements in a list. + Raises: + ValueError: If `selected_key` or `default_selected_key` is set when + `selection_mode="multiple"`. + ValueError: If `selected_keys` or `default_selected_keys` + is set when `selection_mode="single"`. + Returns: The rendered ComboBox. """ children, props = create_props(locals()) + is_multiple = props.pop("selection_mode", "single") == "multiple" + + _validate_selection_mode(props, "multiple" if is_multiple else "single") + children, props = unpack_item_table_source(children, props, SUPPORTED_SOURCE_ARGS) return component_element( - "ComboBox", *children, _nullable_props=_NULLABLE_PROPS, **props + "MultiSelect" if is_multiple else "ComboBox", + *children, + _nullable_props=[] if is_multiple else _NULLABLE_PROPS, + **props, ) diff --git a/plugins/ui/src/js/src/elements/MultiSelect.tsx b/plugins/ui/src/js/src/elements/MultiSelect.tsx new file mode 100644 index 000000000..44f136ba9 --- /dev/null +++ b/plugins/ui/src/js/src/elements/MultiSelect.tsx @@ -0,0 +1,71 @@ +import { useSelector } from 'react-redux'; +import { MultiSelect as DHMultiSelect } from '@deephaven/components'; +import { MultiSelect as DHMultiSelectJSApi } from '@deephaven/jsapi-components'; +import { isElementOfType } from '@deephaven/react-hooks'; +import type { dh } from '@deephaven/jsapi-types'; +import { ApiContext } from '@deephaven/jsapi-bootstrap'; +import { getSettings, RootState } from '@deephaven/redux'; +import { + SerializedMultiSelectProps, + useMultiSelectProps, +} from './hooks/useMultiSelectProps'; +import ObjectView from './ObjectView'; +import { useObjectViewObject } from './hooks/useObjectViewObject'; +import UriObjectView from './UriObjectView'; +import { getErrorShortMessage } from '../widget/WidgetErrorUtils'; + +export function MultiSelect( + props: SerializedMultiSelectProps +): JSX.Element | null { + const settings = useSelector(getSettings); + const { children, ...pickerProps } = useMultiSelectProps(props); + + const isObjectView = + isElementOfType(children, ObjectView) || + isElementOfType(children, UriObjectView); + const { + widget: table, + api, + isLoading, + error, + } = useObjectViewObject(children); + + if (isObjectView) { + if (error != null) { + const message = getErrorShortMessage(error); + return ( + + {[]} + + ); + } + if (isLoading || table == null || api == null) { + return ( + // eslint-disable-next-line react/jsx-props-no-spreading + + {[]} + + ); + } + return ( + + + + ); + } + + // eslint-disable-next-line react/jsx-props-no-spreading + return {children}; +} + +export default MultiSelect; diff --git a/plugins/ui/src/js/src/elements/hooks/useMultiSelectProps.ts b/plugins/ui/src/js/src/elements/hooks/useMultiSelectProps.ts new file mode 100644 index 000000000..aeddff9f4 --- /dev/null +++ b/plugins/ui/src/js/src/elements/hooks/useMultiSelectProps.ts @@ -0,0 +1,63 @@ +import { MultiSelectProps as DHMultiSelectProps } from '@deephaven/components'; +import { MultiSelectProps as DHMultiSelectJSApiProps } from '@deephaven/jsapi-components'; +import { + SerializedSelectionProps, + useSelectionProps, +} from './useSelectionProps'; +import { + SerializedPickerEventProps, + WrappedDHPickerJSApiProps, +} from './usePickerProps'; +import { useFocusEventCallback } from './useFocusEventCallback'; +import { useKeyboardEventCallback } from './useKeyboardEventCallback'; + +type WrappedDHMultiSelectJSApiProps = + WrappedDHPickerJSApiProps; + +export type SerializedMultiSelectProps = ( + | DHMultiSelectProps + | WrappedDHMultiSelectJSApiProps +) & + SerializedSelectionProps & + SerializedPickerEventProps; + +/** + * Wrap MultiSelect props with the appropriate serialized event callbacks. + * @param props Props to wrap + * @returns Wrapped props + */ +export function useMultiSelectProps({ + onChange: serializedOnChange, + onSelectionChange: serializedOnSelectionChange, + onFocus, + onBlur, + onKeyDown, + onKeyUp, + ...otherProps +}: SerializedMultiSelectProps): + | DHMultiSelectProps + | WrappedDHMultiSelectJSApiProps { + const { onChange, onSelectionChange } = useSelectionProps({ + onChange: serializedOnChange, + onSelectionChange: serializedOnSelectionChange, + }); + + const deserializedOnFocus = useFocusEventCallback(onFocus); + const deserializedOnBlur = useFocusEventCallback(onBlur); + const deserializedOnKeyDown = useKeyboardEventCallback(onKeyDown); + const deserializedOnKeyUp = useKeyboardEventCallback(onKeyUp); + + return { + onChange, + onSelectionChange, + onFocus: deserializedOnFocus, + onBlur: deserializedOnBlur, + onKeyDown: deserializedOnKeyDown, + onKeyUp: deserializedOnKeyUp, + // The @deephaven/components `MultiSelect` has its own normalization logic + // that handles primitive children types (string, number, boolean). It also + // handles nested children inside of `Item` and `Section` components, so + // we are intentionally not wrapping `otherProps` in `mapSpectrumProps` + ...otherProps, + }; +} diff --git a/plugins/ui/src/js/src/elements/index.ts b/plugins/ui/src/js/src/elements/index.ts index 2ea1c76a0..0fbd38e34 100644 --- a/plugins/ui/src/js/src/elements/index.ts +++ b/plugins/ui/src/js/src/elements/index.ts @@ -28,6 +28,7 @@ export * from './LogicButton'; export * from './Markdown'; export * from './Menu'; export * from './Meter'; +export * from './MultiSelect'; export * from './model'; export * from './ObjectView'; export * from './Picker'; diff --git a/plugins/ui/src/js/src/elements/model/ElementConstants.ts b/plugins/ui/src/js/src/elements/model/ElementConstants.ts index 928da2331..c8447b82b 100644 --- a/plugins/ui/src/js/src/elements/model/ElementConstants.ts +++ b/plugins/ui/src/js/src/elements/model/ElementConstants.ts @@ -71,6 +71,7 @@ export const ELEMENT_NAME = { menu: uiComponentName('Menu'), menuTrigger: uiComponentName('MenuTrigger'), meter: uiComponentName('Meter'), + multiSelect: uiComponentName('MultiSelect'), numberField: uiComponentName('NumberField'), picker: uiComponentName('Picker'), progressBar: uiComponentName('ProgressBar'), diff --git a/plugins/ui/src/js/src/widget/WidgetUtils.tsx b/plugins/ui/src/js/src/widget/WidgetUtils.tsx index 00b00b1d0..a0c1106a7 100644 --- a/plugins/ui/src/js/src/widget/WidgetUtils.tsx +++ b/plugins/ui/src/js/src/widget/WidgetUtils.tsx @@ -88,6 +88,7 @@ import { Markdown, Menu, Meter, + MultiSelect, Picker, ProgressBar, ProgressCircle, @@ -186,6 +187,7 @@ export const elementComponentMap: Record, unknown> = { [ELEMENT_NAME.menu]: Menu, [ELEMENT_NAME.menuTrigger]: MenuTrigger, [ELEMENT_NAME.meter]: Meter, + [ELEMENT_NAME.multiSelect]: MultiSelect, [ELEMENT_NAME.numberField]: NumberField, [ELEMENT_NAME.picker]: Picker, [ELEMENT_NAME.progressBar]: ProgressBar, From db5a635a01a38a02fe2e73e49439d40926636e5d Mon Sep 17 00:00:00 2001 From: gzh2003 Date: Tue, 21 Apr 2026 12:00:53 -0400 Subject: [PATCH 02/20] fix docs error --- plugins/ui/src/deephaven/ui/components/combo_box.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/ui/src/deephaven/ui/components/combo_box.py b/plugins/ui/src/deephaven/ui/components/combo_box.py index cbb389df8..865f2a185 100644 --- a/plugins/ui/src/deephaven/ui/components/combo_box.py +++ b/plugins/ui/src/deephaven/ui/components/combo_box.py @@ -307,10 +307,8 @@ def combo_box( key: A unique identifier used by React to render elements in a list. Raises: - ValueError: If `selected_key` or `default_selected_key` is set when - `selection_mode="multiple"`. - ValueError: If `selected_keys` or `default_selected_keys` - is set when `selection_mode="single"`. + ValueError: If `selected_key` or `default_selected_key` is set when `selection_mode="multiple"`. + ValueError: If `selected_keys` or `default_selected_keys` is set when `selection_mode="single"`. Returns: The rendered ComboBox. From 086748b22215260ab3915843e262972cc453fbd3 Mon Sep 17 00:00:00 2001 From: gzh2003 Date: Tue, 21 Apr 2026 12:07:12 -0400 Subject: [PATCH 03/20] Issue with raise entries in autodoc --- plugins/ui/src/deephaven/ui/components/combo_box.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/plugins/ui/src/deephaven/ui/components/combo_box.py b/plugins/ui/src/deephaven/ui/components/combo_box.py index 865f2a185..ef332539c 100644 --- a/plugins/ui/src/deephaven/ui/components/combo_box.py +++ b/plugins/ui/src/deephaven/ui/components/combo_box.py @@ -204,7 +204,8 @@ def combo_box( Args: *children: The options to render within the combo box. selection_mode: Whether the combo box allows single or multiple selection. - Defaults to `"single"`. + Defaults to `"single"`. When `"multiple"`, use `selected_keys`/`default_selected_keys` + instead of `selected_key`/`default_selected_key`. menu_trigger: The interaction required to display the ComboBox menu. is_quiet: Whether the ComboBox should be displayed with a quiet style. align: Alignment of the menu relative to the input target. @@ -306,10 +307,6 @@ def combo_box( UNSAFE_style: A CSS style to apply to the element. key: A unique identifier used by React to render elements in a list. - Raises: - ValueError: If `selected_key` or `default_selected_key` is set when `selection_mode="multiple"`. - ValueError: If `selected_keys` or `default_selected_keys` is set when `selection_mode="single"`. - Returns: The rendered ComboBox. """ From 83107fe5aaf74749e645eaee70a5e1536216f7af Mon Sep 17 00:00:00 2001 From: gzh2003 Date: Sat, 25 Apr 2026 21:03:25 -0400 Subject: [PATCH 04/20] should be using hand rolled props --- plugins/ui/src/js/src/elements/MultiSelect.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/plugins/ui/src/js/src/elements/MultiSelect.tsx b/plugins/ui/src/js/src/elements/MultiSelect.tsx index 44f136ba9..f94b941ce 100644 --- a/plugins/ui/src/js/src/elements/MultiSelect.tsx +++ b/plugins/ui/src/js/src/elements/MultiSelect.tsx @@ -18,7 +18,7 @@ export function MultiSelect( props: SerializedMultiSelectProps ): JSX.Element | null { const settings = useSelector(getSettings); - const { children, ...pickerProps } = useMultiSelectProps(props); + const { children, ...multiSelectProps } = useMultiSelectProps(props); const isObjectView = isElementOfType(children, ObjectView) || @@ -36,7 +36,7 @@ export function MultiSelect( return ( @@ -47,7 +47,7 @@ export function MultiSelect( if (isLoading || table == null || api == null) { return ( // eslint-disable-next-line react/jsx-props-no-spreading - + {[]} ); @@ -56,7 +56,7 @@ export function MultiSelect( @@ -64,8 +64,10 @@ export function MultiSelect( ); } - // eslint-disable-next-line react/jsx-props-no-spreading - return {children}; + return ( + // eslint-disable-next-line react/jsx-props-no-spreading + {children} + ); } export default MultiSelect; From 23417ed8d76b7b1f847ed616eab80fdb6ff24286 Mon Sep 17 00:00:00 2001 From: gzh2003 Date: Mon, 27 Apr 2026 01:15:38 -0400 Subject: [PATCH 05/20] fix round trip issue --- plugins/ui/src/js/src/elements/MultiSelect.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/plugins/ui/src/js/src/elements/MultiSelect.tsx b/plugins/ui/src/js/src/elements/MultiSelect.tsx index f94b941ce..3f961d25a 100644 --- a/plugins/ui/src/js/src/elements/MultiSelect.tsx +++ b/plugins/ui/src/js/src/elements/MultiSelect.tsx @@ -23,12 +23,7 @@ export function MultiSelect( const isObjectView = isElementOfType(children, ObjectView) || isElementOfType(children, UriObjectView); - const { - widget: table, - api, - isLoading, - error, - } = useObjectViewObject(children); + const { widget: table, api, error } = useObjectViewObject(children); if (isObjectView) { if (error != null) { @@ -44,7 +39,10 @@ export function MultiSelect( ); } - if (isLoading || table == null || api == null) { + // Don't gate on `isLoading` as it flips true on server round-trips and + // would unmount/remount the spectrum MultiSelect, closing any open + // popover. + if (table == null || api == null) { return ( // eslint-disable-next-line react/jsx-props-no-spreading From 343d65136fe771ae60d5076b0b0b5bf394cbf68c Mon Sep 17 00:00:00 2001 From: Joe Numainville Date: Thu, 14 May 2026 10:30:50 -0500 Subject: [PATCH 06/20] deprecation --- .../src/deephaven/ui/components/combo_box.py | 82 ++++++++++++++++--- 1 file changed, 72 insertions(+), 10 deletions(-) diff --git a/plugins/ui/src/deephaven/ui/components/combo_box.py b/plugins/ui/src/deephaven/ui/components/combo_box.py index ef332539c..c36cbfe60 100644 --- a/plugins/ui/src/deephaven/ui/components/combo_box.py +++ b/plugins/ui/src/deephaven/ui/components/combo_box.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings from typing import Callable, Any, Literal from .types import ( @@ -91,9 +92,34 @@ def _validate_selection_mode(props: dict[str, Any], mode: str) -> None: props.pop(prop, None) +def _wrap_callback_as_selection( + callback: Callable[..., None] | None, +) -> Callable[..., None] | None: + """ + Wrap a callback so it always receives a Selection instead of a single Key. + + Args: + callback: The callback to wrap. + + Returns: + A wrapped callback that always receives a Selection. + """ + if callback is None: + return None + + def wrapper(value: Any) -> None: + if isinstance(value, (str, int, float, bool)): + callback([value]) + else: + callback(value) + + return wrapper + + def combo_box( *children: Item | SectionElement | Table | PartitionedTable | ItemTableSource, selection_mode: Literal["single", "multiple"] = "single", + selection_event: bool = False, menu_trigger: MenuTriggerAction | None = "input", is_quiet: bool | None = None, align: Align | None = "end", @@ -126,8 +152,8 @@ def combo_box( necessity_indicator: NecessityIndicator | None = None, contextual_help: Element | None = None, on_open_change: Callable[[bool, MenuTriggerAction], None] | None = None, - on_selection_change: Callable[[Key | None], None] | None = None, - on_change: Callable[[Key], None] | None = None, + on_selection_change: Callable[[Selection | None], None] | None = None, + on_change: Callable[[Selection], None] | None = None, on_input_change: Callable[[str], None] | None = None, on_focus: Callable[[FocusEventCallable], None] | None = None, on_blur: Callable[[FocusEventCallable], None] | None = None, @@ -206,6 +232,11 @@ def combo_box( selection_mode: Whether the combo box allows single or multiple selection. Defaults to `"single"`. When `"multiple"`, use `selected_keys`/`default_selected_keys` instead of `selected_key`/`default_selected_key`. + selection_event: When True, `on_selection_change` and `on_change` receive a + `Selection` (list of keys) instead of a single `Key` in single-select mode. + Defaults to False for backwards compatibility. Set to True to opt in to the + new behavior. In a future version, this will become the default and this + prop will be deprecated. menu_trigger: The interaction required to display the ComboBox menu. is_quiet: Whether the ComboBox should be displayed with a quiet style. align: Alignment of the menu relative to the input target. @@ -227,14 +258,10 @@ def combo_box( If the typed text matches an existing item's label, that item's key is used instead. disabled_keys: The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. - selected_key: The currently selected key in the collection (controlled). - Only applies in single-select mode. - default_selected_key: The initial selected key in the collection (uncontrolled). - Only applies in single-select mode. + selected_key: Deprecated. Use `selected_keys` instead. + default_selected_key: Deprecated. Use `default_selected_keys` instead. selected_keys: The currently selected keys in the collection (controlled). - Only applies in multi-select mode. default_selected_keys: The initial selected keys in the collection (uncontrolled). - Only applies in multi-select mode. is_disabled: Whether the input is disabled. is_read_only: Whether the input can be selected but not changed by the user. is_required: Whether user input is required on the input before form submission. @@ -253,9 +280,11 @@ def combo_box( on_open_change: Method that is called when the open state of the menu changes. Returns the new open state and the action that caused the opening of the menu. on_selection_change: Handler that is called when the selection changes. - In single-select mode, receives the selected key. - In multi-select mode, receives the full selection (set of keys). + When `selection_event=True`, always receives a `Selection` (list of keys). + Otherwise, receives a single `Key` in single-select mode (deprecated). on_change: Alias of `on_selection_change`. Handler that is called when the selection changes. + When `selection_event=True`, always receives a `Selection` (list of keys). + Otherwise, receives a single `Key` in single-select mode (deprecated). on_input_change: Handler that is called when the ComboBox input value changes. on_focus: Handler that is called when the element receives focus. on_blur: Handler that is called when the element loses focus. @@ -312,7 +341,40 @@ def combo_box( """ children, props = create_props(locals()) + if selected_key is not Undefined: + warnings.warn( + "'selected_key' is deprecated. Use 'selected_keys' instead.", + DeprecationWarning, + stacklevel=2, + ) + if default_selected_key is not None: + warnings.warn( + "'default_selected_key' is deprecated. Use 'default_selected_keys' instead.", + DeprecationWarning, + stacklevel=2, + ) + is_multiple = props.pop("selection_mode", "single") == "multiple" + use_selection_event = props.pop("selection_event", False) + + if not is_multiple: + if use_selection_event: + 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) + else: + for cb_name in ("on_selection_change", "on_change"): + if props.get(cb_name) is not None: + warnings.warn( + f"'{cb_name}' currently receives a single Key in " + "single-select mode. In a future version, it will " + "receive a Selection (list of keys). Set " + "selection_event=True to opt in to the new behavior " + "and suppress this warning.", + DeprecationWarning, + stacklevel=2, + ) _validate_selection_mode(props, "multiple" if is_multiple else "single") From 665b17897ce5bb33f1981a5ba85d81a62c53f27b Mon Sep 17 00:00:00 2001 From: Joe Numainville Date: Thu, 14 May 2026 10:33:18 -0500 Subject: [PATCH 07/20] fixed --- plugins/ui/src/deephaven/ui/components/combo_box.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/ui/src/deephaven/ui/components/combo_box.py b/plugins/ui/src/deephaven/ui/components/combo_box.py index c36cbfe60..5a0f9cbb9 100644 --- a/plugins/ui/src/deephaven/ui/components/combo_box.py +++ b/plugins/ui/src/deephaven/ui/components/combo_box.py @@ -230,8 +230,7 @@ def combo_box( Args: *children: The options to render within the combo box. selection_mode: Whether the combo box allows single or multiple selection. - Defaults to `"single"`. When `"multiple"`, use `selected_keys`/`default_selected_keys` - instead of `selected_key`/`default_selected_key`. + Defaults to `"single"`. selection_event: When True, `on_selection_change` and `on_change` receive a `Selection` (list of keys) instead of a single `Key` in single-select mode. Defaults to False for backwards compatibility. Set to True to opt in to the From 29ac4d251eaf6b1f6d936f07b85033d73e1785cd Mon Sep 17 00:00:00 2001 From: Joe Numainville Date: Thu, 14 May 2026 13:24:40 -0500 Subject: [PATCH 08/20] refactor and tests --- plugins/ui/docs/components/combo_box.md | 123 +++------- .../src/deephaven/ui/components/combo_box.py | 140 +++++------ .../src/js/src/elements/MultiSelect.test.tsx | 157 ++++++++++++ .../hooks/useMultiSelectProps.test.ts | 127 ++++++++++ .../ui/test/deephaven/ui/test_combo_box.py | 232 ++++++++++++++++++ 5 files changed, 613 insertions(+), 166 deletions(-) create mode 100644 plugins/ui/src/js/src/elements/MultiSelect.test.tsx create mode 100644 plugins/ui/src/js/src/elements/hooks/useMultiSelectProps.test.ts create mode 100644 plugins/ui/test/deephaven/ui/test_combo_box.py diff --git a/plugins/ui/docs/components/combo_box.md b/plugins/ui/docs/components/combo_box.md index 65e4e9c20..9029035a0 100644 --- a/plugins/ui/docs/components/combo_box.md +++ b/plugins/ui/docs/components/combo_box.md @@ -10,7 +10,7 @@ from deephaven import ui @ui.component def ui_combo_box_basic(): - option, set_option = ui.use_state("") + option, set_option = ui.use_state([]) return ui.combo_box( ui.item("red panda"), @@ -21,7 +21,7 @@ def ui_combo_box_basic(): ui.item("snake"), ui.item("ant"), label="Favorite Animal", - selected_key=option, + selected_keys=option, on_change=set_option, ) @@ -262,9 +262,11 @@ my_combo_box_required_examples = ui_combo_box_required_examples() ## Selection -In a combo box, the `default_selected_key` or `selected_key` props set a selected option. +Use `selected_keys` or `default_selected_keys` to set the selected option(s). -The `default_selected_key` is useful for simpler scenarios where you don't need to control the state externally. The `selected_key` is used for scenarios where the state should be managed by the parent component, providing control and flexibility over the selection of the combo box. +`default_selected_keys` is useful for simpler scenarios where you don't need to control the state externally. `selected_keys` is used for scenarios where the state should be managed by the parent component, providing control and flexibility over the selection of the combo box. + +> [!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. ```python from deephaven import ui @@ -272,7 +274,7 @@ from deephaven import ui @ui.component def ui_combo_box_selected_key_examples(): - option, set_option = ui.use_state("Option 1") + option, set_option = ui.use_state(["Option 1"]) return [ ui.combo_box( ui.item("Option 1"), @@ -284,7 +286,7 @@ def ui_combo_box_selected_key_examples(): ui.item("Option 7"), ui.item("Option 8"), ui.item("Option 9"), - default_selected_key="Option 2", + default_selected_keys=["Option 2"], label="Pick an option (uncontrolled)", ), ui.combo_box( @@ -297,7 +299,7 @@ def ui_combo_box_selected_key_examples(): ui.item("Option 7"), ui.item("Option 8"), ui.item("Option 9"), - selected_key=option, + selected_keys=option, on_change=set_option, label="Pick an option (controlled)", ), @@ -347,12 +349,14 @@ my_combo_box_section_example = ui.combo_box( ## Events -Combo boxes support selection via mouse, keyboard, and touch. You can handle all these via the `on_change` prop, which receives the selected key as an argument. Additionally, combo boxes accept an `on_input_change` prop, which is triggered whenever the search value is edited by the user, whether through typing or option selection. +Combo boxes support selection via mouse, keyboard, and touch. You can handle all these via the `on_change` prop. Additionally, combo boxes accept an `on_input_change` prop, which is triggered whenever the search value is edited by the user, whether through typing or option selection. Each interaction done in the combo box will trigger its associated event handler. For instance, typing in the input field will only trigger the `on_input_change`, not the `on_change`. Note, this is not the case for selections; when a selection is made, both the `on_change` and `on_input_change` are triggered. +> [!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. + ```python from deephaven import ui @@ -360,17 +364,15 @@ from deephaven import ui @ui.component def ui_combo_box_control_example(): input_value, set_input_value = ui.use_state("") - selection_state, set_selection_state = ui.use_state("") + selection_state, set_selection_state = ui.use_state([]) def handle_input_change(new_value): - set_selection_state("") set_input_value(new_value) - print(f"Text changed to {input_value}") + print(f"Text changed to {new_value}") def handle_selection_change(new_value): - set_input_value(new_value) set_selection_state(new_value) - print(f"Selection changed to {selection_state}") + print(f"Selection changed to {new_value}") return [ ui.combo_box( @@ -383,11 +385,10 @@ def ui_combo_box_control_example(): ui.item("Option 7"), ui.item("Option 8"), ui.item("Option 9"), - input_value=input_value, on_input_change=handle_input_change, - selected_key=selection_state, + selected_keys=selection_state, on_change=handle_selection_change, - ) + ), ] @@ -600,7 +601,7 @@ my_combo_box_is_read_only_example = ui.combo_box( ui.item("Option 6", key="Option 6"), ui.item("Option 7", key="Option 7"), ui.item("Option 8", key="Option 8"), - default_selected_key="Option 1", + default_selected_keys=["Option 1"], is_read_only=True, ) ``` @@ -765,84 +766,36 @@ def ui_combo_box_alignment_direction_examples(): my_combo_box_alignment_direction_examples = ui_combo_box_alignment_direction_examples() ``` -## How to create a multi-select component +## Multi-select -By leveraging the `on_change` handler of `ui.combo_box` to dynamically generate items, you can pair it with `ui.tag_group` to build a multi-select component. +Set `selection_mode="multiple"` to allow selecting multiple items. Selected items appear as tags inside the input area, and the dropdown list can be filtered by typing. ```python from deephaven import ui @ui.component -def ui_combo_box_multi_select_example( - options, on_input_change_callback=None, on_selection_change_callback=None -): - input_value, set_input_value = ui.use_state("") - selection_state, set_selection_state = ui.use_state("") - items, set_items = ui.use_state([]) - - def handle_input_change(new_value): - set_selection_state("") - set_input_value(new_value) - print(f"Text changed to {new_value}") - if on_input_change_callback: - on_input_change_callback(new_value) - - def handle_selection_change(new_value): - set_input_value("") - set_selection_state(new_value) - set_items( - lambda prev_items: prev_items + [new_value] - if new_value not in prev_items and new_value is not None - else prev_items - ) - print(f"Selection changed to {items}") - if on_selection_change_callback: - on_selection_change_callback(new_value, items) - - return [ - ui.flex( - ui.flex( - ui.combo_box( - *[ui.item(option) for option in options], - input_value=input_value, - on_input_change=handle_input_change, - selected_key=selection_state, - on_change=handle_selection_change, - ), - ui.tag_group( - *[ui.item(item, key=item.lower()) for item in items], - on_remove=lambda keys: set_items( - [item for item in items if item.lower() not in keys] - ), - ), - direction="row", - align_items="center", - ), - align_items="start", - ) - ] +def ui_combo_box_multi_select_example(): + selected, set_selected = ui.use_state([]) + return ui.combo_box( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + ui.item("Option 9"), + selection_mode="multiple", + selected_keys=selected, + on_change=set_selected, + label="Pick options", + ) -my_options = [ - "Option 1", - "Option 2", - "Option 3", - "Option 4", - "Option 5", - "Option 6", - "Option 7", - "Option 8", - "Option 9", -] -my_combo_box_multi_select_example = ui_combo_box_multi_select_example( - options=my_options, - on_input_change_callback=lambda value: print(f"Custom input handler: {value}"), - on_selection_change_callback=lambda value, items: print( - f"Custom selection handler: {value}, {items}" - ), -) +my_combo_box_multi_select_example = ui_combo_box_multi_select_example() ``` ## API Reference diff --git a/plugins/ui/src/deephaven/ui/components/combo_box.py b/plugins/ui/src/deephaven/ui/components/combo_box.py index 5a0f9cbb9..f99eb358b 100644 --- a/plugins/ui/src/deephaven/ui/components/combo_box.py +++ b/plugins/ui/src/deephaven/ui/components/combo_box.py @@ -45,52 +45,18 @@ _NULLABLE_PROPS = ["selected_key"] -# Props that only apply to single-select ComboBox. +# Props that only apply to single-select ComboBox and are stripped in multi mode. _SINGLE_ONLY_PROPS = { "selected_key", "default_selected_key", } -# Props that only apply to multi-select mode. +# Props that only apply to multi-select mode and are stripped in single mode. _MULTI_ONLY_PROPS = { "selected_keys", "default_selected_keys", } -# Props that raise a ValueError if explicitly set in the wrong mode. -_SINGLE_ONLY_VALIDATED = { - "selected_key": Undefined, - "default_selected_key": None, -} - -_MULTI_ONLY_VALIDATED = { - "selected_keys": None, - "default_selected_keys": None, -} - - -def _validate_selection_mode(props: dict[str, Any], mode: str) -> None: - """Validate and strip props that conflict with the given selection mode. - - Raises ValueError for props that conflict with the active mode, - and removes props that only apply to the other mode. - - Args: - props: The props to validate. - mode: The active selection mode. - """ - if mode == "multiple": - validated, strip = _SINGLE_ONLY_VALIDATED, _SINGLE_ONLY_PROPS - else: - validated, strip = _MULTI_ONLY_VALIDATED, _MULTI_ONLY_PROPS - - for prop, default in validated.items(): - val = props.get(prop) - if val is not default: - raise ValueError(f"'{prop}' is not supported when selection_mode='{mode}'.") - for prop in strip: - props.pop(prop, None) - def _wrap_callback_as_selection( callback: Callable[..., None] | None, @@ -116,10 +82,60 @@ def wrapper(value: Any) -> None: return wrapper +def _process_selection_props( + props: dict[str, Any], + is_multiple: bool, + *, + stacklevel: int = 3, +) -> None: + """Process selection-related props: emit deprecation warnings, strip + inapplicable props, and wrap callbacks when needed. + + When the deprecated ``selected_key`` / ``default_selected_key`` props are + used, callbacks continue to receive a single ``Key``. When only the new + ``selected_keys`` / ``default_selected_keys`` props are used, callbacks in + single-select mode are wrapped so they always receive a ``Selection``. + + Args: + props: Mutable props dict (modified in place). + is_multiple: Whether multi-select mode is active. + stacklevel: Stack level passed to warnings.warn to point to the caller's code. + """ + uses_deprecated = ( + props.get("selected_key") is not Undefined + or props.get("default_selected_key") is not None + ) + + # warn about deprecated single-select props if they are set + if props.get("selected_key") is not Undefined: + warnings.warn( + "'selected_key' is deprecated. Use 'selected_keys' instead.", + DeprecationWarning, + stacklevel=stacklevel, + ) + if props.get("default_selected_key") is not None: + warnings.warn( + "'default_selected_key' is deprecated. Use 'default_selected_keys' instead.", + DeprecationWarning, + stacklevel=stacklevel, + ) + + # 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) + + # 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) + + def combo_box( *children: Item | SectionElement | Table | PartitionedTable | ItemTableSource, selection_mode: Literal["single", "multiple"] = "single", - selection_event: bool = False, menu_trigger: MenuTriggerAction | None = "input", is_quiet: bool | None = None, align: Align | None = "end", @@ -231,11 +247,6 @@ def combo_box( *children: The options to render within the combo box. selection_mode: Whether the combo box allows single or multiple selection. Defaults to `"single"`. - selection_event: When True, `on_selection_change` and `on_change` receive a - `Selection` (list of keys) instead of a single `Key` in single-select mode. - Defaults to False for backwards compatibility. Set to True to opt in to the - new behavior. In a future version, this will become the default and this - prop will be deprecated. menu_trigger: The interaction required to display the ComboBox menu. is_quiet: Whether the ComboBox should be displayed with a quiet style. align: Alignment of the menu relative to the input target. @@ -279,11 +290,11 @@ def combo_box( on_open_change: Method that is called when the open state of the menu changes. Returns the new open state and the action that caused the opening of the menu. on_selection_change: Handler that is called when the selection changes. - When `selection_event=True`, always receives a `Selection` (list of keys). - Otherwise, receives a single `Key` in single-select mode (deprecated). + Receives a `Selection` (list of keys). When the deprecated `selected_key` + or `default_selected_key` props are used, receives a single `Key` instead. on_change: Alias of `on_selection_change`. Handler that is called when the selection changes. - When `selection_event=True`, always receives a `Selection` (list of keys). - Otherwise, receives a single `Key` in single-select mode (deprecated). + Receives a `Selection` (list of keys). When the deprecated `selected_key` + or `default_selected_key` props are used, receives a single `Key` instead. on_input_change: Handler that is called when the ComboBox input value changes. on_focus: Handler that is called when the element receives focus. on_blur: Handler that is called when the element loses focus. @@ -340,42 +351,9 @@ def combo_box( """ children, props = create_props(locals()) - if selected_key is not Undefined: - warnings.warn( - "'selected_key' is deprecated. Use 'selected_keys' instead.", - DeprecationWarning, - stacklevel=2, - ) - if default_selected_key is not None: - warnings.warn( - "'default_selected_key' is deprecated. Use 'default_selected_keys' instead.", - DeprecationWarning, - stacklevel=2, - ) - is_multiple = props.pop("selection_mode", "single") == "multiple" - use_selection_event = props.pop("selection_event", False) - - if not is_multiple: - if use_selection_event: - 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) - else: - for cb_name in ("on_selection_change", "on_change"): - if props.get(cb_name) is not None: - warnings.warn( - f"'{cb_name}' currently receives a single Key in " - "single-select mode. In a future version, it will " - "receive a Selection (list of keys). Set " - "selection_event=True to opt in to the new behavior " - "and suppress this warning.", - DeprecationWarning, - stacklevel=2, - ) - - _validate_selection_mode(props, "multiple" if is_multiple else "single") + + _process_selection_props(props, is_multiple) children, props = unpack_item_table_source(children, props, SUPPORTED_SOURCE_ARGS) diff --git a/plugins/ui/src/js/src/elements/MultiSelect.test.tsx b/plugins/ui/src/js/src/elements/MultiSelect.test.tsx new file mode 100644 index 000000000..b2ec7130a --- /dev/null +++ b/plugins/ui/src/js/src/elements/MultiSelect.test.tsx @@ -0,0 +1,157 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { MultiSelect } from './MultiSelect'; +import type { SerializedMultiSelectProps } from './hooks/useMultiSelectProps'; + +// Mock ObjectView and UriObjectView before they trigger deep dependency chains +jest.mock('./ObjectView', () => jest.fn(() => null)); +jest.mock('./UriObjectView', () => jest.fn(() => null)); +jest.mock('../widget/WidgetErrorUtils', () => ({ + getErrorShortMessage: jest.fn((e: Error) => e.message), +})); + +// Mock all heavy dependencies +jest.mock('react-redux', () => ({ + useSelector: jest.fn(() => ({})), +})); + +jest.mock('./hooks/useMultiSelectProps', () => ({ + useMultiSelectProps: jest.fn((props: Record) => { + const { + onChange, + onSelectionChange, + onFocus, + onBlur, + onKeyDown, + onKeyUp, + ...rest + } = props; + return rest; + }), +})); + +jest.mock('./hooks/useObjectViewObject', () => ({ + useObjectViewObject: jest.fn(() => ({ + widget: null, + api: null, + isLoading: false, + error: null, + })), +})); + +jest.mock('@deephaven/components', () => ({ + MultiSelect: jest.fn( + ({ + children, + ...props + }: { + children?: React.ReactNode; + [key: string]: unknown; + }) => ( +
+ {children} +
+ ) + ), +})); + +jest.mock('@deephaven/jsapi-components', () => ({ + MultiSelect: jest.fn(() =>
), +})); + +jest.mock('@deephaven/react-hooks', () => ({ + isElementOfType: jest.fn(() => false), +})); + +jest.mock('@deephaven/jsapi-bootstrap', () => ({ + ApiContext: { + Provider: ({ children }: { children: React.ReactNode }) => <>{children}, + }, +})); + +jest.mock('@deephaven/redux', () => ({ + getSettings: jest.fn(() => ({})), +})); + +describe('MultiSelect', () => { + it('renders DHMultiSelect with children when not an ObjectView', () => { + const props = { + children: ['Option A', 'Option B'], + label: 'Test', + } as unknown as SerializedMultiSelectProps; + + const { getByTestId } = render(); + expect(getByTestId('dh-multi-select')).toBeTruthy(); + }); + + it('renders loading state when ObjectView with no table', () => { + const { isElementOfType } = jest.requireMock('@deephaven/react-hooks'); + isElementOfType.mockReturnValue(true); + + const { useObjectViewObject } = jest.requireMock( + './hooks/useObjectViewObject' + ); + useObjectViewObject.mockReturnValue({ + widget: null, + api: null, + isLoading: true, + error: null, + }); + + const props = { + children: React.createElement('div'), + label: 'Loading test', + } as unknown as SerializedMultiSelectProps; + + const { getByTestId } = render(); + const el = getByTestId('dh-multi-select'); + expect(el).toBeTruthy(); + }); + + it('renders error state when ObjectView has error', () => { + const { isElementOfType } = jest.requireMock('@deephaven/react-hooks'); + isElementOfType.mockReturnValue(true); + + const { useObjectViewObject } = jest.requireMock( + './hooks/useObjectViewObject' + ); + useObjectViewObject.mockReturnValue({ + widget: null, + api: null, + isLoading: false, + error: new Error('Test error'), + }); + + const props = { + children: React.createElement('div'), + label: 'Error test', + } as unknown as SerializedMultiSelectProps; + + const { getByTestId } = render(); + const el = getByTestId('dh-multi-select'); + expect(el).toBeTruthy(); + }); + + it('renders JSApi MultiSelect when ObjectView has table and api', () => { + const { isElementOfType } = jest.requireMock('@deephaven/react-hooks'); + isElementOfType.mockReturnValue(true); + + const { useObjectViewObject } = jest.requireMock( + './hooks/useObjectViewObject' + ); + useObjectViewObject.mockReturnValue({ + widget: {}, + api: {}, + isLoading: false, + error: null, + }); + + const props = { + children: React.createElement('div'), + label: 'JSApi test', + } as unknown as SerializedMultiSelectProps; + + const { getByTestId } = render(); + expect(getByTestId('dh-multi-select-jsapi')).toBeTruthy(); + }); +}); diff --git a/plugins/ui/src/js/src/elements/hooks/useMultiSelectProps.test.ts b/plugins/ui/src/js/src/elements/hooks/useMultiSelectProps.test.ts new file mode 100644 index 000000000..54c1540b9 --- /dev/null +++ b/plugins/ui/src/js/src/elements/hooks/useMultiSelectProps.test.ts @@ -0,0 +1,127 @@ +import { renderHook, act } from '@testing-library/react'; +import { useMultiSelectProps } from './useMultiSelectProps'; +import type { SerializedMultiSelectProps } from './useMultiSelectProps'; + +describe('useMultiSelectProps', () => { + it('passes through other props unchanged', () => { + const props = { + label: 'Test Label', + isDisabled: true, + selectedKeys: ['a', 'b'], + } as unknown as SerializedMultiSelectProps; + + const { result } = renderHook(() => useMultiSelectProps(props)); + + expect(result.current).toMatchObject({ + label: 'Test Label', + isDisabled: true, + selectedKeys: ['a', 'b'], + }); + }); + + it('deserializes onChange into a function', () => { + const onChange = jest.fn(); + const props = { + onChange, + } as unknown as SerializedMultiSelectProps; + + const { result } = renderHook(() => useMultiSelectProps(props)); + + expect(result.current.onChange).toBeDefined(); + expect(typeof result.current.onChange).toBe('function'); + }); + + it('deserializes onSelectionChange into a function', () => { + const onSelectionChange = jest.fn(); + const props = { + onSelectionChange, + } as unknown as SerializedMultiSelectProps; + + const { result } = renderHook(() => useMultiSelectProps(props)); + + expect(result.current.onSelectionChange).toBeDefined(); + expect(typeof result.current.onSelectionChange).toBe('function'); + }); + + it('serializes Set selection to array when onChange fires', () => { + const onChange = jest.fn(); + const props = { + onChange, + } as unknown as SerializedMultiSelectProps; + + const { result } = renderHook(() => useMultiSelectProps(props)); + + act(() => { + result.current.onChange?.(new Set(['a', 'b'])); + }); + + expect(onChange).toHaveBeenCalledWith(['a', 'b']); + }); + + it('serializes Set selection to array when onSelectionChange fires', () => { + const onSelectionChange = jest.fn(); + const props = { + onSelectionChange, + } as unknown as SerializedMultiSelectProps; + + const { result } = renderHook(() => useMultiSelectProps(props)); + + act(() => { + result.current.onSelectionChange?.(new Set(['x', 'y'])); + }); + + expect(onSelectionChange).toHaveBeenCalledWith(['x', 'y']); + }); + + it('passes "all" selection through unchanged', () => { + const onChange = jest.fn(); + const props = { + onChange, + } as unknown as SerializedMultiSelectProps; + + const { result } = renderHook(() => useMultiSelectProps(props)); + + act(() => { + result.current.onChange?.('all'); + }); + + expect(onChange).toHaveBeenCalledWith('all'); + }); + + it('deserializes focus and blur callbacks', () => { + const onFocus = jest.fn(); + const onBlur = jest.fn(); + const props = { + onFocus, + onBlur, + } as unknown as SerializedMultiSelectProps; + + const { result } = renderHook(() => useMultiSelectProps(props)); + + expect(result.current.onFocus).toBeDefined(); + expect(result.current.onBlur).toBeDefined(); + }); + + it('deserializes keyboard callbacks', () => { + const onKeyDown = jest.fn(); + const onKeyUp = jest.fn(); + const props = { + onKeyDown, + onKeyUp, + } as unknown as SerializedMultiSelectProps; + + const { result } = renderHook(() => useMultiSelectProps(props)); + + expect(result.current.onKeyDown).toBeDefined(); + expect(result.current.onKeyUp).toBeDefined(); + }); + + it('returns undefined for omitted callbacks', () => { + const props = {} as SerializedMultiSelectProps; + + const { result } = renderHook(() => useMultiSelectProps(props)); + + expect(result.current.onChange).toBeUndefined(); + expect(result.current.onSelectionChange).toBeUndefined(); + }); +}); diff --git a/plugins/ui/test/deephaven/ui/test_combo_box.py b/plugins/ui/test/deephaven/ui/test_combo_box.py new file mode 100644 index 000000000..03fb4f8b9 --- /dev/null +++ b/plugins/ui/test/deephaven/ui/test_combo_box.py @@ -0,0 +1,232 @@ +import unittest +import warnings + +from .BaseTest import BaseTestCase + + +class ComboBoxProcessSelectionPropsTest(BaseTestCase): + def setUp(self): + from deephaven.ui.types import Undefined + + self.Undefined = Undefined + + def _process(self, props, is_multiple): + from deephaven.ui.components.combo_box import _process_selection_props + + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + _process_selection_props(props, is_multiple) + + def test_single_mode_strips_multi_props(self): + props = { + "selected_keys": ["a", "b"], + "default_selected_keys": ["c"], + "other": "value", + } + self._process(props, is_multiple=False) + self.assertNotIn("selected_keys", props) + self.assertNotIn("default_selected_keys", props) + self.assertEqual(props["other"], "value") + + def test_multiple_mode_strips_single_props(self): + props = { + "selected_key": self.Undefined, + "default_selected_key": None, + "other": "value", + } + self._process(props, is_multiple=True) + self.assertNotIn("selected_key", props) + self.assertNotIn("default_selected_key", props) + self.assertEqual(props["other"], "value") + + def test_multiple_mode_strips_set_single_props(self): + props = { + "selected_key": "some_key", + "default_selected_key": "other", + } + self._process(props, is_multiple=True) + self.assertNotIn("selected_key", props) + self.assertNotIn("default_selected_key", props) + + +class ComboBoxWrapCallbackTest(BaseTestCase): + def _wrap(self, callback): + from deephaven.ui.components.combo_box import _wrap_callback_as_selection + + return _wrap_callback_as_selection(callback) + + def test_none_returns_none(self): + self.assertIsNone(self._wrap(None)) + + def test_wraps_string_key(self): + received = [] + wrapped = self._wrap(lambda v: received.append(v)) + wrapped("my_key") + self.assertEqual(received, [["my_key"]]) + + def test_wraps_int_key(self): + received = [] + wrapped = self._wrap(lambda v: received.append(v)) + wrapped(42) + self.assertEqual(received, [[42]]) + + def test_wraps_float_key(self): + received = [] + wrapped = self._wrap(lambda v: received.append(v)) + wrapped(3.14) + self.assertEqual(received, [[3.14]]) + + def test_wraps_bool_key(self): + received = [] + wrapped = self._wrap(lambda v: received.append(v)) + wrapped(True) + self.assertEqual(received, [[True]]) + + def test_passes_list_through(self): + received = [] + wrapped = self._wrap(lambda v: received.append(v)) + wrapped(["a", "b"]) + self.assertEqual(received, [["a", "b"]]) + + def test_passes_none_through(self): + received = [] + wrapped = self._wrap(lambda v: received.append(v)) + wrapped(None) + self.assertEqual(received, [None]) + + +class ComboBoxDeprecationTest(BaseTestCase): + def test_selected_key_warns(self): + from deephaven.ui import combo_box + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + combo_box(selected_key="a") + dep_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)] + messages = [str(x.message) for x in dep_warnings] + self.assertTrue( + any("selected_key" in m and "selected_keys" in m for m in messages), + f"Expected selected_key deprecation warning, got: {messages}", + ) + + def test_default_selected_key_warns(self): + from deephaven.ui import combo_box + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + combo_box(default_selected_key="a") + dep_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)] + messages = [str(x.message) for x in dep_warnings] + self.assertTrue( + any( + "default_selected_key" in m and "default_selected_keys" in m + for m in messages + ), + f"Expected default_selected_key deprecation warning, got: {messages}", + ) + + def test_no_warning_when_defaults(self): + from deephaven.ui import combo_box + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + combo_box() + dep_warnings = [ + x + for x in w + if issubclass(x.category, DeprecationWarning) + and ("selected_key" in str(x.message)) + ] + self.assertEqual( + len(dep_warnings), + 0, + f"Unexpected deprecation warning: {[str(x.message) for x in dep_warnings]}", + ) + + +class ComboBoxCallbackWrappingTest(BaseTestCase): + """Callbacks are wrapped to receive Selection when deprecated key props are NOT used.""" + + def _process(self, props, is_multiple): + from deephaven.ui.components.combo_box import _process_selection_props + + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + _process_selection_props(props, is_multiple) + + def test_single_wraps_when_no_deprecated_props(self): + from deephaven.ui.types import Undefined + + received = [] + handler = lambda v: received.append(v) + props = { + "selected_key": Undefined, + "default_selected_key": None, + "on_change": handler, + } + self._process(props, is_multiple=False) + props["on_change"]("my_key") + self.assertEqual(received, [["my_key"]]) + + def test_single_no_wrap_when_selected_key_used(self): + received = [] + handler = lambda v: received.append(v) + props = { + "selected_key": "a", + "default_selected_key": None, + "on_change": handler, + } + self._process(props, is_multiple=False) + props["on_change"]("my_key") + self.assertEqual(received, ["my_key"]) + + def test_single_no_wrap_when_default_selected_key_used(self): + from deephaven.ui.types import Undefined + + received = [] + handler = lambda v: received.append(v) + props = { + "selected_key": Undefined, + "default_selected_key": "b", + "on_change": handler, + } + self._process(props, is_multiple=False) + props["on_change"]("my_key") + self.assertEqual(received, ["my_key"]) + + def test_multiple_no_wrap_regardless(self): + received = [] + handler = lambda v: received.append(v) + props = { + "selected_key": "x", + "default_selected_key": None, + "on_change": handler, + } + self._process(props, is_multiple=True) + # single props are stripped, callback untouched + props["on_change"](["a", "b"]) + self.assertEqual(received, [["a", "b"]]) + + +class ComboBoxSelectionModeTest(BaseTestCase): + def test_single_mode_renders_combo_box(self): + from deephaven.ui import combo_box + + result = combo_box(label="Test") + self.assertEqual(result.name, "deephaven.ui.components.ComboBox") + + def test_multiple_mode_renders_multi_select(self): + from deephaven.ui import combo_box + + result = combo_box(selection_mode="multiple", label="Test") + self.assertEqual(result.name, "deephaven.ui.components.MultiSelect") + + def test_multiple_mode_accepts_selected_keys(self): + from deephaven.ui import combo_box + + result = combo_box(selection_mode="multiple", selected_keys=["a", "b"]) + self.assertEqual(result.name, "deephaven.ui.components.MultiSelect") + + +if __name__ == "__main__": + unittest.main() From ce954faa855c6c4001bceca0ca1890babb47000d Mon Sep 17 00:00:00 2001 From: Joe Numainville Date: Thu, 14 May 2026 14:05:56 -0500 Subject: [PATCH 09/20] fixing issues --- .../src/js/src/elements/MultiSelect.test.tsx | 42 ++++++++++++------- .../ui/src/js/src/elements/MultiSelect.tsx | 4 +- .../src/elements/hooks/useMultiSelectProps.ts | 8 ++-- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/plugins/ui/src/js/src/elements/MultiSelect.test.tsx b/plugins/ui/src/js/src/elements/MultiSelect.test.tsx index b2ec7130a..7018b8a0c 100644 --- a/plugins/ui/src/js/src/elements/MultiSelect.test.tsx +++ b/plugins/ui/src/js/src/elements/MultiSelect.test.tsx @@ -41,16 +41,8 @@ jest.mock('./hooks/useObjectViewObject', () => ({ jest.mock('@deephaven/components', () => ({ MultiSelect: jest.fn( - ({ - children, - ...props - }: { - children?: React.ReactNode; - [key: string]: unknown; - }) => ( -
- {children} -
+ ({ children }: { children?: React.ReactNode; [key: string]: unknown }) => ( +
{children}
) ), })); @@ -65,7 +57,7 @@ jest.mock('@deephaven/react-hooks', () => ({ jest.mock('@deephaven/jsapi-bootstrap', () => ({ ApiContext: { - Provider: ({ children }: { children: React.ReactNode }) => <>{children}, + Provider: ({ children }: { children: React.ReactNode }) => children, }, })); @@ -80,7 +72,12 @@ describe('MultiSelect', () => { label: 'Test', } as unknown as SerializedMultiSelectProps; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); expect(getByTestId('dh-multi-select')).toBeTruthy(); }); @@ -103,7 +100,12 @@ describe('MultiSelect', () => { label: 'Loading test', } as unknown as SerializedMultiSelectProps; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const el = getByTestId('dh-multi-select'); expect(el).toBeTruthy(); }); @@ -127,7 +129,12 @@ describe('MultiSelect', () => { label: 'Error test', } as unknown as SerializedMultiSelectProps; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const el = getByTestId('dh-multi-select'); expect(el).toBeTruthy(); }); @@ -151,7 +158,12 @@ describe('MultiSelect', () => { label: 'JSApi test', } as unknown as SerializedMultiSelectProps; - const { getByTestId } = render(); + const { getByTestId } = render( + + ); expect(getByTestId('dh-multi-select-jsapi')).toBeTruthy(); }); }); diff --git a/plugins/ui/src/js/src/elements/MultiSelect.tsx b/plugins/ui/src/js/src/elements/MultiSelect.tsx index 3f961d25a..ee8cdf2b1 100644 --- a/plugins/ui/src/js/src/elements/MultiSelect.tsx +++ b/plugins/ui/src/js/src/elements/MultiSelect.tsx @@ -4,9 +4,9 @@ import { MultiSelect as DHMultiSelectJSApi } from '@deephaven/jsapi-components'; import { isElementOfType } from '@deephaven/react-hooks'; import type { dh } from '@deephaven/jsapi-types'; import { ApiContext } from '@deephaven/jsapi-bootstrap'; -import { getSettings, RootState } from '@deephaven/redux'; +import { getSettings, type RootState } from '@deephaven/redux'; import { - SerializedMultiSelectProps, + type SerializedMultiSelectProps, useMultiSelectProps, } from './hooks/useMultiSelectProps'; import ObjectView from './ObjectView'; diff --git a/plugins/ui/src/js/src/elements/hooks/useMultiSelectProps.ts b/plugins/ui/src/js/src/elements/hooks/useMultiSelectProps.ts index aeddff9f4..62119083d 100644 --- a/plugins/ui/src/js/src/elements/hooks/useMultiSelectProps.ts +++ b/plugins/ui/src/js/src/elements/hooks/useMultiSelectProps.ts @@ -1,10 +1,10 @@ -import { MultiSelectProps as DHMultiSelectProps } from '@deephaven/components'; -import { MultiSelectProps as DHMultiSelectJSApiProps } from '@deephaven/jsapi-components'; +import type { MultiSelectProps as DHMultiSelectProps } from '@deephaven/components'; +import type { MultiSelectProps as DHMultiSelectJSApiProps } from '@deephaven/jsapi-components'; import { - SerializedSelectionProps, + type SerializedSelectionProps, useSelectionProps, } from './useSelectionProps'; -import { +import type { SerializedPickerEventProps, WrappedDHPickerJSApiProps, } from './usePickerProps'; From 0498ace7d8cc8fae3a206cbee282fa9378634391 Mon Sep 17 00:00:00 2001 From: Joe Numainville Date: Thu, 14 May 2026 14:50:56 -0500 Subject: [PATCH 10/20] fixed screenshots --- plugins/ui/docs/snapshots/0761e766ac976e28061e143e034b23bf.json | 1 - plugins/ui/docs/snapshots/09532531c03ea0d5591a4a03b09161a1.json | 1 + plugins/ui/docs/snapshots/2747a4e6e572a96106716910e764ee1d.json | 1 + plugins/ui/docs/snapshots/639e179ae0580408f236edba3dd1d3c3.json | 1 + plugins/ui/docs/snapshots/87f46b40b82cb2efc19f5bdbab3ac2a3.json | 1 + plugins/ui/docs/snapshots/94ab46014a1ec4241a826e7b08b163ef.json | 1 - plugins/ui/docs/snapshots/a3924153476e117f57e563dacb3b31d5.json | 1 + plugins/ui/docs/snapshots/ec0ed3d95653ca3d2062db0a5ffcbc68.json | 1 - plugins/ui/docs/snapshots/f1891462b585b31dabf2e7d96395c968.json | 1 - plugins/ui/docs/snapshots/fa4c431cba8e02a50fb02b9cbf6dd6de.json | 1 - 10 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 plugins/ui/docs/snapshots/0761e766ac976e28061e143e034b23bf.json create mode 100644 plugins/ui/docs/snapshots/09532531c03ea0d5591a4a03b09161a1.json create mode 100644 plugins/ui/docs/snapshots/2747a4e6e572a96106716910e764ee1d.json create mode 100644 plugins/ui/docs/snapshots/639e179ae0580408f236edba3dd1d3c3.json create mode 100644 plugins/ui/docs/snapshots/87f46b40b82cb2efc19f5bdbab3ac2a3.json delete mode 100644 plugins/ui/docs/snapshots/94ab46014a1ec4241a826e7b08b163ef.json create mode 100644 plugins/ui/docs/snapshots/a3924153476e117f57e563dacb3b31d5.json delete mode 100644 plugins/ui/docs/snapshots/ec0ed3d95653ca3d2062db0a5ffcbc68.json delete mode 100644 plugins/ui/docs/snapshots/f1891462b585b31dabf2e7d96395c968.json delete mode 100644 plugins/ui/docs/snapshots/fa4c431cba8e02a50fb02b9cbf6dd6de.json diff --git a/plugins/ui/docs/snapshots/0761e766ac976e28061e143e034b23bf.json b/plugins/ui/docs/snapshots/0761e766ac976e28061e143e034b23bf.json deleted file mode 100644 index 62b851727..000000000 --- a/plugins/ui/docs/snapshots/0761e766ac976e28061e143e034b23bf.json +++ /dev/null @@ -1 +0,0 @@ -{"file":"components/combo_box.md","objects":{"my_combo_box_selected_key_examples":{"type":"deephaven.ui.Element","data":{"document":{"props":{"children":[{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","defaultSelectedKey":"Option 2","validationBehavior":"aria","label":"Pick an option (uncontrolled)","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}},{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","selectedKey":"Option 1","validationBehavior":"aria","label":"Pick an option (controlled)","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}}]},"__dhElemName":"__main__.ui_combo_box_selected_key_examples"},"state":"{\"state\": {\"0\": \"Option 1\"}}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/09532531c03ea0d5591a4a03b09161a1.json b/plugins/ui/docs/snapshots/09532531c03ea0d5591a4a03b09161a1.json new file mode 100644 index 000000000..6026f6711 --- /dev/null +++ b/plugins/ui/docs/snapshots/09532531c03ea0d5591a4a03b09161a1.json @@ -0,0 +1 @@ +{"file":"components/combo_box.md","objects":{"my_combo_box_selected_key_examples":{"type":"deephaven.ui.Element","data":{"document":{"props":{"children":[{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Pick an option (uncontrolled)","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}},{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Pick an option (controlled)","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}}]},"__dhElemName":"__main__.ui_combo_box_selected_key_examples"},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/2747a4e6e572a96106716910e764ee1d.json b/plugins/ui/docs/snapshots/2747a4e6e572a96106716910e764ee1d.json new file mode 100644 index 000000000..d857abb1f --- /dev/null +++ b/plugins/ui/docs/snapshots/2747a4e6e572a96106716910e764ee1d.json @@ -0,0 +1 @@ +{"file":"components/combo_box.md","objects":{"my_combo_box_basic":{"type":"deephaven.ui.Element","data":{"document":{"props":{"children":{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Favorite Animal","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"red panda"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"cat"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"dog"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"aardvark"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"kangaroo"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"snake"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"ant"}}]}}},"__dhElemName":"__main__.ui_combo_box_basic"},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/639e179ae0580408f236edba3dd1d3c3.json b/plugins/ui/docs/snapshots/639e179ae0580408f236edba3dd1d3c3.json new file mode 100644 index 000000000..0c43e13ce --- /dev/null +++ b/plugins/ui/docs/snapshots/639e179ae0580408f236edba3dd1d3c3.json @@ -0,0 +1 @@ +{"file":"components/combo_box.md","objects":{"my_combo_box_control_example":{"type":"deephaven.ui.Element","data":{"document":{"props":{"children":[{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"onInputChange":{"__dhCbid":"cb1"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}}]},"__dhElemName":"__main__.ui_combo_box_control_example"},"state":"{\"state\": {\"0\": \"\"}}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/87f46b40b82cb2efc19f5bdbab3ac2a3.json b/plugins/ui/docs/snapshots/87f46b40b82cb2efc19f5bdbab3ac2a3.json new file mode 100644 index 000000000..b409f9fe6 --- /dev/null +++ b/plugins/ui/docs/snapshots/87f46b40b82cb2efc19f5bdbab3ac2a3.json @@ -0,0 +1 @@ +{"file":"components/combo_box.md","objects":{"my_combo_box_multi_select_example":{"type":"deephaven.ui.Element","data":{"document":{"props":{"children":{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","selectedKeys":[],"validationBehavior":"aria","label":"Pick options","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}}},"__dhElemName":"__main__.ui_combo_box_multi_select_example"},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/94ab46014a1ec4241a826e7b08b163ef.json b/plugins/ui/docs/snapshots/94ab46014a1ec4241a826e7b08b163ef.json deleted file mode 100644 index cc1802ac2..000000000 --- a/plugins/ui/docs/snapshots/94ab46014a1ec4241a826e7b08b163ef.json +++ /dev/null @@ -1 +0,0 @@ -{"file":"components/combo_box.md","objects":{"my_combo_box_is_read_only_example":{"type":"deephaven.ui.Element","data":{"document":{"props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","defaultSelectedKey":"Option 1","isReadOnly":true,"validationBehavior":"aria","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 1","children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 2","children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 3","children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 4","children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 5","children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 6","children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 7","children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 8","children":"Option 8"}}]},"__dhElemName":"deephaven.ui.components.ComboBox"},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/a3924153476e117f57e563dacb3b31d5.json b/plugins/ui/docs/snapshots/a3924153476e117f57e563dacb3b31d5.json new file mode 100644 index 000000000..c58bff1d2 --- /dev/null +++ b/plugins/ui/docs/snapshots/a3924153476e117f57e563dacb3b31d5.json @@ -0,0 +1 @@ +{"file":"components/combo_box.md","objects":{"my_combo_box_is_read_only_example":{"type":"deephaven.ui.Element","data":{"document":{"props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","isReadOnly":true,"validationBehavior":"aria","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 1","children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 2","children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 3","children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 4","children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 5","children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 6","children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 7","children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 8","children":"Option 8"}}]},"__dhElemName":"deephaven.ui.components.ComboBox"},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/ec0ed3d95653ca3d2062db0a5ffcbc68.json b/plugins/ui/docs/snapshots/ec0ed3d95653ca3d2062db0a5ffcbc68.json deleted file mode 100644 index bffbf7716..000000000 --- a/plugins/ui/docs/snapshots/ec0ed3d95653ca3d2062db0a5ffcbc68.json +++ /dev/null @@ -1 +0,0 @@ -{"file":"components/combo_box.md","objects":{"my_combo_box_multi_select_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_combo_box_multi_select_example","props":{"children":[{"__dhElemName":"deephaven.ui.components.Flex","props":{"alignItems":"start","gap":"size-100","flex":"auto","children":{"__dhElemName":"deephaven.ui.components.Flex","props":{"direction":"row","alignItems":"center","gap":"size-100","flex":"auto","children":[{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","inputValue":"","selectedKey":"","validationBehavior":"aria","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"onInputChange":{"__dhCbid":"cb1"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}},{"__dhElemName":"deephaven.ui.components.TagGroup","props":{"labelPosition":"top","labelAlign":"start","onRemove":{"__dhCbid":"cb2"}}}]}}}}]}},"state":"{\"state\": {\"0\": \"\", \"1\": \"\"}}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/f1891462b585b31dabf2e7d96395c968.json b/plugins/ui/docs/snapshots/f1891462b585b31dabf2e7d96395c968.json deleted file mode 100644 index 861e7edad..000000000 --- a/plugins/ui/docs/snapshots/f1891462b585b31dabf2e7d96395c968.json +++ /dev/null @@ -1 +0,0 @@ -{"file":"components/combo_box.md","objects":{"my_combo_box_control_example":{"type":"deephaven.ui.Element","data":{"document":{"props":{"children":[{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","inputValue":"","selectedKey":"","validationBehavior":"aria","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"onInputChange":{"__dhCbid":"cb1"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}}]},"__dhElemName":"__main__.ui_combo_box_control_example"},"state":"{\"state\": {\"0\": \"\", \"1\": \"\"}}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/fa4c431cba8e02a50fb02b9cbf6dd6de.json b/plugins/ui/docs/snapshots/fa4c431cba8e02a50fb02b9cbf6dd6de.json deleted file mode 100644 index 626eb4f37..000000000 --- a/plugins/ui/docs/snapshots/fa4c431cba8e02a50fb02b9cbf6dd6de.json +++ /dev/null @@ -1 +0,0 @@ -{"file":"components/combo_box.md","objects":{"my_combo_box_basic":{"type":"deephaven.ui.Element","data":{"document":{"props":{"children":{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","selectedKey":"","validationBehavior":"aria","label":"Favorite Animal","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"red panda"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"cat"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"dog"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"aardvark"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"kangaroo"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"snake"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"ant"}}]}}},"__dhElemName":"__main__.ui_combo_box_basic"},"state":"{\"state\": {\"0\": \"\"}}"}}}} \ No newline at end of file From ac827c2f83fb96f453102128dd61a05ae2c10b01 Mon Sep 17 00:00:00 2001 From: Joe Numainville Date: Fri, 15 May 2026 10:34:46 -0500 Subject: [PATCH 11/20] fixed comments --- plugins/ui/docs/components/combo_box.md | 19 +++++-- .../src/deephaven/ui/components/combo_box.py | 55 +++++++++++++------ .../ui/test/deephaven/ui/test_combo_box.py | 46 +++++++++++++++- 3 files changed, 96 insertions(+), 24 deletions(-) diff --git a/plugins/ui/docs/components/combo_box.md b/plugins/ui/docs/components/combo_box.md index 9029035a0..a6c3e7bf6 100644 --- a/plugins/ui/docs/components/combo_box.md +++ b/plugins/ui/docs/components/combo_box.md @@ -10,7 +10,7 @@ from deephaven import ui @ui.component def ui_combo_box_basic(): - option, set_option = ui.use_state([]) + option, set_option = ui.use_state([""]) return ui.combo_box( ui.item("red panda"), @@ -266,7 +266,9 @@ Use `selected_keys` or `default_selected_keys` to set the selected option(s). `default_selected_keys` is useful for simpler scenarios where you don't need to control the state externally. `selected_keys` is used for scenarios where the state should be managed by the parent component, providing control and flexibility over the selection of the combo box. -> [!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. + +> [!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. ```python from deephaven import ui @@ -355,7 +357,9 @@ Each interaction done in the combo box will trigger its associated event handler Note, this is not the case for selections; when a selection is made, both the `on_change` and `on_input_change` are triggered. -> [!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. + +> [!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. ```python from deephaven import ui @@ -364,13 +368,15 @@ from deephaven import ui @ui.component def ui_combo_box_control_example(): input_value, set_input_value = ui.use_state("") - selection_state, set_selection_state = ui.use_state([]) + selection_state, set_selection_state = ui.use_state([""]) def handle_input_change(new_value): + set_selection_state([""]) set_input_value(new_value) print(f"Text changed to {new_value}") def handle_selection_change(new_value): + set_input_value(new_value[0] if new_value else "") set_selection_state(new_value) print(f"Selection changed to {new_value}") @@ -385,10 +391,11 @@ def ui_combo_box_control_example(): ui.item("Option 7"), ui.item("Option 8"), ui.item("Option 9"), + input_value=input_value, on_input_change=handle_input_change, selected_keys=selection_state, on_change=handle_selection_change, - ), + ) ] @@ -776,7 +783,7 @@ from deephaven import ui @ui.component def ui_combo_box_multi_select_example(): - selected, set_selected = ui.use_state([]) + selected, set_selected = ui.use_state([""]) return ui.combo_box( ui.item("Option 1"), diff --git a/plugins/ui/src/deephaven/ui/components/combo_box.py b/plugins/ui/src/deephaven/ui/components/combo_box.py index f99eb358b..23aa335cc 100644 --- a/plugins/ui/src/deephaven/ui/components/combo_box.py +++ b/plugins/ui/src/deephaven/ui/components/combo_box.py @@ -29,7 +29,7 @@ from .item import Item from .item_table_source import ItemTableSource from ..elements import BaseElement, Element, NodeType -from .._internal.utils import create_props, unpack_item_table_source +from .._internal.utils import create_props, unpack_item_table_source, wrap_callable from ..types import Key, Selection, Undefined, UndefinedType from .basic import component_element @@ -57,12 +57,15 @@ "default_selected_keys", } +_SELECTION_CALLBACKS = {"on_selection_change", "on_change"} + def _wrap_callback_as_selection( callback: Callable[..., None] | None, ) -> Callable[..., None] | None: """ Wrap a callback so it always receives a Selection instead of a single Key. + Uses wrap_callable to handle user callbacks with varying argument counts. Args: callback: The callback to wrap. @@ -73,11 +76,13 @@ def _wrap_callback_as_selection( if callback is None: return None + wrapped = wrap_callable(callback) + def wrapper(value: Any) -> None: if isinstance(value, (str, int, float, bool)): - callback([value]) + wrapped([value]) else: - callback(value) + wrapped(value) return wrapper @@ -88,13 +93,15 @@ def _process_selection_props( *, stacklevel: int = 3, ) -> None: - """Process selection-related props: emit deprecation warnings, strip - inapplicable props, and wrap callbacks when needed. + """Process selection-related props: emit deprecation warnings, convert or + strip inapplicable props, and wrap callbacks when needed. - When the deprecated ``selected_key`` / ``default_selected_key`` props are - used, callbacks continue to receive a single ``Key``. When only the new - ``selected_keys`` / ``default_selected_keys`` props are used, callbacks in - single-select mode are wrapped so they always receive a ``Selection``. + In single-select mode with the new selected_keys / default_selected_keys + props, converts them to selected_key / default_selected_key (which the + JS ComboBox understands) and wraps callbacks so they receive a Selection. + + When the deprecated selected_key / default_selected_key props are + used, callbacks continue to receive a single Key. Args: props: Mutable props dict (modified in place). @@ -120,14 +127,28 @@ def _process_selection_props( stacklevel=stacklevel, ) - # 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) - - # 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"): + if is_multiple: + # Multi-select: strip deprecated single-select props + for prop in _SINGLE_ONLY_PROPS: + props.pop(prop, None) + elif uses_deprecated: + # Single-select with deprecated props: strip the new multi props + for prop in _MULTI_ONLY_PROPS: + props.pop(prop, None) + else: + # Single-select but using new multi props + # Convert to single for ComboBox and wrap callbacks + sel_keys = props.pop("selected_keys", None) + def_sel_keys = props.pop("default_selected_keys", None) + props.pop("selected_key", None) + props.pop("default_selected_key", None) + + 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 + + for cb_name in _SELECTION_CALLBACKS: cb = props.get(cb_name) if cb is not None: props[cb_name] = _wrap_callback_as_selection(cb) diff --git a/plugins/ui/test/deephaven/ui/test_combo_box.py b/plugins/ui/test/deephaven/ui/test_combo_box.py index 03fb4f8b9..5bf40ccc0 100644 --- a/plugins/ui/test/deephaven/ui/test_combo_box.py +++ b/plugins/ui/test/deephaven/ui/test_combo_box.py @@ -17,8 +17,10 @@ def _process(self, props, is_multiple): warnings.simplefilter("always") _process_selection_props(props, is_multiple) - def test_single_mode_strips_multi_props(self): + def test_single_mode_converts_selected_keys_to_selected_key(self): props = { + "selected_key": self.Undefined, + "default_selected_key": None, "selected_keys": ["a", "b"], "default_selected_keys": ["c"], "other": "value", @@ -26,8 +28,46 @@ def test_single_mode_strips_multi_props(self): self._process(props, is_multiple=False) self.assertNotIn("selected_keys", props) self.assertNotIn("default_selected_keys", props) + self.assertEqual(props["selected_key"], "a") + self.assertEqual(props["default_selected_key"], "c") self.assertEqual(props["other"], "value") + def test_single_mode_converts_empty_selected_keys_to_none(self): + props = { + "selected_key": self.Undefined, + "default_selected_key": None, + "selected_keys": [], + "default_selected_keys": [], + } + self._process(props, is_multiple=False) + self.assertIsNone(props["selected_key"]) + self.assertIsNone(props["default_selected_key"]) + + def test_single_mode_no_conversion_when_keys_none(self): + props = { + "selected_key": self.Undefined, + "default_selected_key": None, + "selected_keys": None, + "default_selected_keys": None, + } + self._process(props, is_multiple=False) + self.assertNotIn("selected_key", props) + self.assertNotIn("default_selected_key", props) + self.assertNotIn("selected_keys", props) + self.assertNotIn("default_selected_keys", props) + + def test_single_mode_deprecated_strips_multi_props(self): + props = { + "selected_key": "a", + "default_selected_key": None, + "selected_keys": ["x"], + "default_selected_keys": ["y"], + } + self._process(props, is_multiple=False) + self.assertNotIn("selected_keys", props) + self.assertNotIn("default_selected_keys", props) + self.assertEqual(props["selected_key"], "a") + def test_multiple_mode_strips_single_props(self): props = { "selected_key": self.Undefined, @@ -162,9 +202,13 @@ def test_single_wraps_when_no_deprecated_props(self): props = { "selected_key": Undefined, "default_selected_key": None, + "selected_keys": ["a"], "on_change": handler, } self._process(props, is_multiple=False) + # selected_keys converted to selected_key + self.assertEqual(props["selected_key"], "a") + # callback wrapped props["on_change"]("my_key") self.assertEqual(received, [["my_key"]]) From d4a7edce7f4a8c3c899ec0b24f5cd5b13e701be4 Mon Sep 17 00:00:00 2001 From: Joe Numainville Date: Fri, 15 May 2026 12:16:44 -0500 Subject: [PATCH 12/20] a few more comments --- plugins/ui/docs/components/combo_box.md | 10 +++---- .../src/deephaven/ui/components/combo_box.py | 30 ++++++++++++++----- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/plugins/ui/docs/components/combo_box.md b/plugins/ui/docs/components/combo_box.md index a6c3e7bf6..438dbbde6 100644 --- a/plugins/ui/docs/components/combo_box.md +++ b/plugins/ui/docs/components/combo_box.md @@ -10,7 +10,7 @@ from deephaven import ui @ui.component def ui_combo_box_basic(): - option, set_option = ui.use_state([""]) + option, set_option = ui.use_state([]) return ui.combo_box( ui.item("red panda"), @@ -268,7 +268,7 @@ Use `selected_keys` or `default_selected_keys` to set the selected option(s). > [!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. + > `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 generally receive a list of keys (unless `None` in the case of `on_change`). ```python from deephaven import ui @@ -359,7 +359,7 @@ Note, this is not the case for selections; when a selection is made, both the `o > [!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. + > `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 generally receive a list of keys (unless `None` in the case of `on_change`). ```python from deephaven import ui @@ -368,7 +368,7 @@ from deephaven import ui @ui.component def ui_combo_box_control_example(): input_value, set_input_value = ui.use_state("") - selection_state, set_selection_state = ui.use_state([""]) + selection_state, set_selection_state = ui.use_state([]) def handle_input_change(new_value): set_selection_state([""]) @@ -783,7 +783,7 @@ from deephaven import ui @ui.component def ui_combo_box_multi_select_example(): - selected, set_selected = ui.use_state([""]) + selected, set_selected = ui.use_state([]) return ui.combo_box( ui.item("Option 1"), diff --git a/plugins/ui/src/deephaven/ui/components/combo_box.py b/plugins/ui/src/deephaven/ui/components/combo_box.py index 23aa335cc..f91818a0a 100644 --- a/plugins/ui/src/deephaven/ui/components/combo_box.py +++ b/plugins/ui/src/deephaven/ui/components/combo_box.py @@ -62,6 +62,7 @@ def _wrap_callback_as_selection( callback: Callable[..., None] | None, + callback_name: str | None = None, ) -> Callable[..., None] | None: """ Wrap a callback so it always receives a Selection instead of a single Key. @@ -81,6 +82,9 @@ def _wrap_callback_as_selection( def wrapper(value: Any) -> None: if isinstance(value, (str, int, float, bool)): wrapped([value]) + elif value is None and callback_name is "on_change": + # on_change with None means no selection + wrapped([]) else: wrapped(value) @@ -108,9 +112,9 @@ def _process_selection_props( is_multiple: Whether multi-select mode is active. stacklevel: Stack level passed to warnings.warn to point to the caller's code. """ - uses_deprecated = ( - props.get("selected_key") is not Undefined - or props.get("default_selected_key") is not None + uses_keys = ( + props.get("selected_keys") is not Undefined + or props.get("default_selected_keys") is not None ) # warn about deprecated single-select props if they are set @@ -131,8 +135,8 @@ def _process_selection_props( # Multi-select: strip deprecated single-select props for prop in _SINGLE_ONLY_PROPS: props.pop(prop, None) - elif uses_deprecated: - # Single-select with deprecated props: strip the new multi props + elif not uses_keys: + # Doesn't use multi props: strip them for prop in _MULTI_ONLY_PROPS: props.pop(prop, None) else: @@ -144,14 +148,24 @@ def _process_selection_props( props.pop("default_selected_key", None) if sel_keys is not None: + if not isinstance(sel_keys, list) or len(sel_keys) > 1: + warnings.warn( + f"'selected_keys' should be a list with at most one key when 'selection_mode' is 'single'. Got: {sel_keys}", + stacklevel=3, + ) props["selected_key"] = sel_keys[0] if sel_keys else None if def_sel_keys is not None: + if not isinstance(def_sel_keys, list) or len(def_sel_keys) > 1: + warnings.warn( + f"'default_selected_keys' should be a list with at most one key when 'selection_mode' is 'single'. Got: {def_sel_keys}", + stacklevel=3, + ) props["default_selected_key"] = def_sel_keys[0] if def_sel_keys else None for cb_name in _SELECTION_CALLBACKS: cb = props.get(cb_name) if cb is not None: - props[cb_name] = _wrap_callback_as_selection(cb) + props[cb_name] = _wrap_callback_as_selection(cb, cb_name) def combo_box( @@ -189,8 +203,8 @@ def combo_box( necessity_indicator: NecessityIndicator | None = None, contextual_help: Element | None = None, on_open_change: Callable[[bool, MenuTriggerAction], None] | None = None, - on_selection_change: Callable[[Selection | None], None] | None = None, - on_change: Callable[[Selection], None] | None = None, + on_selection_change: Callable[[Key | Selection | None], None] | None = None, + on_change: Callable[[Key | Selection], None] | None = None, on_input_change: Callable[[str], None] | None = None, on_focus: Callable[[FocusEventCallable], None] | None = None, on_blur: Callable[[FocusEventCallable], None] | None = None, From 50db8889c877f4999fa1dca89172b5c18d0fd679 Mon Sep 17 00:00:00 2001 From: Joe Numainville Date: Fri, 15 May 2026 12:45:41 -0500 Subject: [PATCH 13/20] snapshots --- plugins/ui/docs/snapshots/0cf5572d36b4c8bdf11d666c2afd41c5.json | 1 + plugins/ui/docs/snapshots/639e179ae0580408f236edba3dd1d3c3.json | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 plugins/ui/docs/snapshots/0cf5572d36b4c8bdf11d666c2afd41c5.json delete mode 100644 plugins/ui/docs/snapshots/639e179ae0580408f236edba3dd1d3c3.json diff --git a/plugins/ui/docs/snapshots/0cf5572d36b4c8bdf11d666c2afd41c5.json b/plugins/ui/docs/snapshots/0cf5572d36b4c8bdf11d666c2afd41c5.json new file mode 100644 index 000000000..f350b5108 --- /dev/null +++ b/plugins/ui/docs/snapshots/0cf5572d36b4c8bdf11d666c2afd41c5.json @@ -0,0 +1 @@ +{"file":"components/combo_box.md","objects":{"my_combo_box_control_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_combo_box_control_example","props":{"children":[{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","inputValue":"","validationBehavior":"aria","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"onInputChange":{"__dhCbid":"cb1"},"selectedKey":null,"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}}]}},"state":"{\"state\": {\"0\": \"\"}}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/639e179ae0580408f236edba3dd1d3c3.json b/plugins/ui/docs/snapshots/639e179ae0580408f236edba3dd1d3c3.json deleted file mode 100644 index 0c43e13ce..000000000 --- a/plugins/ui/docs/snapshots/639e179ae0580408f236edba3dd1d3c3.json +++ /dev/null @@ -1 +0,0 @@ -{"file":"components/combo_box.md","objects":{"my_combo_box_control_example":{"type":"deephaven.ui.Element","data":{"document":{"props":{"children":[{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"onInputChange":{"__dhCbid":"cb1"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}}]},"__dhElemName":"__main__.ui_combo_box_control_example"},"state":"{\"state\": {\"0\": \"\"}}"}}}} \ No newline at end of file From 933b76022f949e8dfd38fda9f8e0f80325f16b51 Mon Sep 17 00:00:00 2001 From: Joe Numainville Date: Mon, 18 May 2026 10:57:25 -0500 Subject: [PATCH 14/20] comments --- .../src/deephaven/ui/components/combo_box.py | 138 ++++++++++-------- .../ui/test/deephaven/ui/test_combo_box.py | 40 +++-- 2 files changed, 107 insertions(+), 71 deletions(-) diff --git a/plugins/ui/src/deephaven/ui/components/combo_box.py b/plugins/ui/src/deephaven/ui/components/combo_box.py index f91818a0a..40a6c409f 100644 --- a/plugins/ui/src/deephaven/ui/components/combo_box.py +++ b/plugins/ui/src/deephaven/ui/components/combo_box.py @@ -1,7 +1,7 @@ from __future__ import annotations import warnings -from typing import Callable, Any, Literal +from typing import Callable, Any, Literal, Sequence from .types import ( FocusEventCallable, @@ -65,8 +65,7 @@ def _wrap_callback_as_selection( callback_name: str | None = None, ) -> Callable[..., None] | None: """ - Wrap a callback so it always receives a Selection instead of a single Key. - Uses wrap_callable to handle user callbacks with varying argument counts. + Wrap a callback so it always receives a Selection instead of (possibly) a single Key. Args: callback: The callback to wrap. @@ -82,8 +81,8 @@ def _wrap_callback_as_selection( def wrapper(value: Any) -> None: if isinstance(value, (str, int, float, bool)): wrapped([value]) - elif value is None and callback_name is "on_change": - # on_change with None means no selection + elif value is None and callback_name == "on_change": + # on_change with None means an empty list to be consistent with the typing wrapped([]) else: wrapped(value) @@ -91,77 +90,98 @@ def wrapper(value: Any) -> None: return wrapper +def _convert_selection_prop( + props: dict[str, Any], + multi_prop: str, + single_prop: str, + is_multiple: bool, + default_val: Any, +) -> bool: + """ + Convert between single and multi select props based on the selection mode, emitting warnings as needed. + + Args: + props: The props dict to modify in place. + multi_prop: The name of the multi-select prop (e.g. "selected_keys"). + single_prop: The name of the single-select prop (e.g. "selected_key"). + is_multiple: Whether multi-select mode is active. + default_val: The default value to use (for the single prop only). + + Returns: + True if callbacks should always return a Key + """ + multi_val = props.pop(multi_prop) + single_val = props.pop(single_prop) + + if single_val is not default_val: + if is_multiple: + # Throw an error if the user is trying to use the single prop in multi-select mode since it shouldn't work + raise ValueError( + f"'{single_prop}' cannot be used when 'selection_mode' is 'multiple'. Use '{multi_prop}' instead." + ) + # Otherwise use the single prop value + # Warn and don't convert callbacks since the user is using the deprecated single prop which expects a single Key + warnings.warn( + f"'{single_prop}' is deprecated. Use '{multi_prop}' instead.", + FutureWarning, + stacklevel=2, + ) + props[single_prop] = single_val + return True + + if is_multiple: + # In multi-select mode, multi_prop is expected + props[multi_prop] = multi_val + return False + + if multi_val is not None: + # multi_prop is provided in single-select mode, so we need to convert it to the single prop + if not isinstance(multi_val, list) or len(multi_val) > 1: + warnings.warn( + f"'{multi_prop}' should be a list with at most one key when 'selection_mode' is 'single'. Got: {multi_val}", + stacklevel=2, + ) + # Use the single prop for the ComboBox + props[single_prop] = multi_val[0] if multi_val else None + # In single-select mode but using the multi prop, so the callbacks receive a Selection + return False + + # No value provided for either prop, so keep the callback as is, expecting a Selection + # This is technically an ambiguous case and may conflict with deprecated usage + # but we have no way to know if the user intends to use the Key or Selection callbacks + # without a hacky check of some sort. + props[single_prop] = single_val + return False + + def _process_selection_props( props: dict[str, Any], is_multiple: bool, - *, - stacklevel: int = 3, ) -> None: - """Process selection-related props: emit deprecation warnings, convert or + """ + Process selection-related props: emit deprecation warnings, convert or strip inapplicable props, and wrap callbacks when needed. In single-select mode with the new selected_keys / default_selected_keys props, converts them to selected_key / default_selected_key (which the - JS ComboBox understands) and wraps callbacks so they receive a Selection. + ComboBox understands) and wraps callbacks so they receive a Selection. When the deprecated selected_key / default_selected_key props are used, callbacks continue to receive a single Key. Args: - props: Mutable props dict (modified in place). + props: Mutable props dict is_multiple: Whether multi-select mode is active. - stacklevel: Stack level passed to warnings.warn to point to the caller's code. """ - uses_keys = ( - props.get("selected_keys") is not Undefined - or props.get("default_selected_keys") is not None + selected_takes_key = _convert_selection_prop( + props, "selected_keys", "selected_key", is_multiple, Undefined + ) + default_takes_key = _convert_selection_prop( + props, "default_selected_keys", "default_selected_key", is_multiple, None ) - # warn about deprecated single-select props if they are set - if props.get("selected_key") is not Undefined: - warnings.warn( - "'selected_key' is deprecated. Use 'selected_keys' instead.", - DeprecationWarning, - stacklevel=stacklevel, - ) - if props.get("default_selected_key") is not None: - warnings.warn( - "'default_selected_key' is deprecated. Use 'default_selected_keys' instead.", - DeprecationWarning, - stacklevel=stacklevel, - ) - - if is_multiple: - # Multi-select: strip deprecated single-select props - for prop in _SINGLE_ONLY_PROPS: - props.pop(prop, None) - elif not uses_keys: - # Doesn't use multi props: strip them - for prop in _MULTI_ONLY_PROPS: - props.pop(prop, None) - else: - # Single-select but using new multi props - # Convert to single for ComboBox and wrap callbacks - sel_keys = props.pop("selected_keys", None) - def_sel_keys = props.pop("default_selected_keys", None) - props.pop("selected_key", None) - props.pop("default_selected_key", None) - - if sel_keys is not None: - if not isinstance(sel_keys, list) or len(sel_keys) > 1: - warnings.warn( - f"'selected_keys' should be a list with at most one key when 'selection_mode' is 'single'. Got: {sel_keys}", - stacklevel=3, - ) - props["selected_key"] = sel_keys[0] if sel_keys else None - if def_sel_keys is not None: - if not isinstance(def_sel_keys, list) or len(def_sel_keys) > 1: - warnings.warn( - f"'default_selected_keys' should be a list with at most one key when 'selection_mode' is 'single'. Got: {def_sel_keys}", - stacklevel=3, - ) - props["default_selected_key"] = def_sel_keys[0] if def_sel_keys else None - + if not (selected_takes_key or default_takes_key): + # We aren't in the deprecated single prop case, so we need to convert callbacks to always receive a Selection for cb_name in _SELECTION_CALLBACKS: cb = props.get(cb_name) if cb is not None: diff --git a/plugins/ui/test/deephaven/ui/test_combo_box.py b/plugins/ui/test/deephaven/ui/test_combo_box.py index 5bf40ccc0..9336350e2 100644 --- a/plugins/ui/test/deephaven/ui/test_combo_box.py +++ b/plugins/ui/test/deephaven/ui/test_combo_box.py @@ -51,8 +51,9 @@ def test_single_mode_no_conversion_when_keys_none(self): "default_selected_keys": None, } self._process(props, is_multiple=False) - self.assertNotIn("selected_key", props) - self.assertNotIn("default_selected_key", props) + # When selected_keys is None, falls through and sets single prop to its default + self.assertEqual(props["selected_key"], self.Undefined) + self.assertEqual(props["default_selected_key"], None) self.assertNotIn("selected_keys", props) self.assertNotIn("default_selected_keys", props) @@ -72,6 +73,8 @@ def test_multiple_mode_strips_single_props(self): props = { "selected_key": self.Undefined, "default_selected_key": None, + "selected_keys": None, + "default_selected_keys": None, "other": "value", } self._process(props, is_multiple=True) @@ -79,14 +82,15 @@ def test_multiple_mode_strips_single_props(self): self.assertNotIn("default_selected_key", props) self.assertEqual(props["other"], "value") - def test_multiple_mode_strips_set_single_props(self): + def test_multiple_mode_raises_when_single_props_set(self): props = { "selected_key": "some_key", "default_selected_key": "other", + "selected_keys": None, + "default_selected_keys": None, } - self._process(props, is_multiple=True) - self.assertNotIn("selected_key", props) - self.assertNotIn("default_selected_key", props) + with self.assertRaises(ValueError): + self._process(props, is_multiple=True) class ComboBoxWrapCallbackTest(BaseTestCase): @@ -142,7 +146,7 @@ def test_selected_key_warns(self): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") combo_box(selected_key="a") - dep_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)] + dep_warnings = [x for x in w if issubclass(x.category, FutureWarning)] messages = [str(x.message) for x in dep_warnings] self.assertTrue( any("selected_key" in m and "selected_keys" in m for m in messages), @@ -155,7 +159,7 @@ def test_default_selected_key_warns(self): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") combo_box(default_selected_key="a") - dep_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)] + dep_warnings = [x for x in w if issubclass(x.category, FutureWarning)] messages = [str(x.message) for x in dep_warnings] self.assertTrue( any( @@ -174,7 +178,7 @@ def test_no_warning_when_defaults(self): dep_warnings = [ x for x in w - if issubclass(x.category, DeprecationWarning) + if issubclass(x.category, FutureWarning) and ("selected_key" in str(x.message)) ] self.assertEqual( @@ -203,6 +207,7 @@ def test_single_wraps_when_no_deprecated_props(self): "selected_key": Undefined, "default_selected_key": None, "selected_keys": ["a"], + "default_selected_keys": None, "on_change": handler, } self._process(props, is_multiple=False) @@ -218,6 +223,8 @@ def test_single_no_wrap_when_selected_key_used(self): props = { "selected_key": "a", "default_selected_key": None, + "selected_keys": None, + "default_selected_keys": None, "on_change": handler, } self._process(props, is_multiple=False) @@ -232,22 +239,31 @@ def test_single_no_wrap_when_default_selected_key_used(self): props = { "selected_key": Undefined, "default_selected_key": "b", + "selected_keys": None, + "default_selected_keys": None, "on_change": handler, } self._process(props, is_multiple=False) props["on_change"]("my_key") self.assertEqual(received, ["my_key"]) - def test_multiple_no_wrap_regardless(self): + def test_multiple_wraps_callbacks(self): + from deephaven.ui.types import Undefined + received = [] handler = lambda v: received.append(v) props = { - "selected_key": "x", + "selected_key": Undefined, "default_selected_key": None, + "selected_keys": ["x"], + "default_selected_keys": None, "on_change": handler, } self._process(props, is_multiple=True) - # single props are stripped, callback untouched + # single props are stripped + self.assertNotIn("selected_key", props) + self.assertNotIn("default_selected_key", props) + # callback wrapped but lists pass through unchanged props["on_change"](["a", "b"]) self.assertEqual(received, [["a", "b"]]) From db4871079ccdaa0adcb50f32c50e683ff63ba042 Mon Sep 17 00:00:00 2001 From: Joe Numainville Date: Mon, 18 May 2026 13:15:56 -0500 Subject: [PATCH 15/20] better warns --- .../ui/src/deephaven/ui/components/combo_box.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/plugins/ui/src/deephaven/ui/components/combo_box.py b/plugins/ui/src/deephaven/ui/components/combo_box.py index 40a6c409f..c4b46a079 100644 --- a/plugins/ui/src/deephaven/ui/components/combo_box.py +++ b/plugins/ui/src/deephaven/ui/components/combo_box.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging import warnings from typing import Callable, Any, Literal, Sequence @@ -33,6 +34,9 @@ from ..types import Key, Selection, Undefined, UndefinedType from .basic import component_element + +logger = logging.getLogger(__name__) + ComboBoxElement = BaseElement SUPPORTED_SOURCE_ARGS = { @@ -136,9 +140,13 @@ def _convert_selection_prop( if multi_val is not None: # multi_prop is provided in single-select mode, so we need to convert it to the single prop - if not isinstance(multi_val, list) or len(multi_val) > 1: - warnings.warn( - f"'{multi_prop}' should be a list with at most one key when 'selection_mode' is 'single'. Got: {multi_val}", + if not isinstance(multi_val, Sequence) or isinstance(multi_val, str): + raise ValueError( + f"'{multi_prop}' should be a Sequence when 'selection_mode' is 'single'. Got type: {type(multi_val)}" + ) + if len(multi_val) > 1: + logger.warning( + f"'{multi_prop}' should be a Sequence with at most one key when 'selection_mode' is 'single'. Got: {multi_val}. Only the first value will be used.", stacklevel=2, ) # Use the single prop for the ComboBox From 574fbb595320cd942fabda7dde7cf5af90f6c6e7 Mon Sep 17 00:00:00 2001 From: Joe Numainville Date: Thu, 21 May 2026 17:18:44 -0500 Subject: [PATCH 16/20] split out --- plugins/ui/docs/components/combo_box.md | 143 ++-- plugins/ui/docs/components/multi_select.md | 761 ++++++++++++++++++ .../src/deephaven/ui/components/__init__.py | 2 + .../src/deephaven/ui/components/combo_box.py | 201 +---- .../deephaven/ui/components/multi_select.py | 257 ++++++ .../ui/test/deephaven/ui/test_combo_box.py | 285 +------ 6 files changed, 1137 insertions(+), 512 deletions(-) create mode 100644 plugins/ui/docs/components/multi_select.md create mode 100644 plugins/ui/src/deephaven/ui/components/multi_select.py diff --git a/plugins/ui/docs/components/combo_box.md b/plugins/ui/docs/components/combo_box.md index 438dbbde6..f7e463ffe 100644 --- a/plugins/ui/docs/components/combo_box.md +++ b/plugins/ui/docs/components/combo_box.md @@ -10,7 +10,7 @@ from deephaven import ui @ui.component def ui_combo_box_basic(): - option, set_option = ui.use_state([]) + option, set_option = ui.use_state("") return ui.combo_box( ui.item("red panda"), @@ -21,7 +21,7 @@ def ui_combo_box_basic(): ui.item("snake"), ui.item("ant"), label="Favorite Animal", - selected_keys=option, + selected_key=option, on_change=set_option, ) @@ -36,14 +36,15 @@ my_combo_box_basic = ui_combo_box_basic() Recommendations for creating clear and effective combo boxes: 1. The combo box's text input simplifies searching through large lists. For lists with fewer than 6 items, use radio buttons. For lists with more than 6 items, assess if the list is complex enough to need searching and filtering, and if not, use a picker instead. -2. Every combo box should have a label specified. Without one, the combo box is ambiguous and not accessible. -3. Options in the combo box should be kept short and concise; multiple lines are strongly discouraged. If more than one line is needed, consider using a description to add context to the option. -4. Choose a `width` for your combo boxes that can accommodate most of the available options. -5. The field labels, menu items, and placeholder text should all be in sentence case. -6. Identify which combo boxes are required or optional, and use the `is_required` field or the `necessity_indicator` to mark them accordingly. -7. A combo box's help text should provide actionable guidance on what to select and how to select it, offering additional context without repeating the placeholder text. -8. When an error occurs, the help text specified in a combo box is replaced by error text; thus, ensure both help and error text convey the same essential information to maintain consistent messaging and prevent loss of critical details. -9. Write error messages in a clear, concise, and helpful manner, guiding users to resolve the issue without ambiguity; ideally, they should be 1-2 short, complete sentences. +2. For selecting multiple options, use a [multi-select](multi_select.md) instead. +3. Every combo box should have a label specified. Without one, the combo box is ambiguous and not accessible. +4. Options in the combo box should be kept short and concise; multiple lines are strongly discouraged. If more than one line is needed, consider using a description to add context to the option. +5. Choose a `width` for your combo boxes that can accommodate most of the available options. +6. The field labels, menu items, and placeholder text should all be in sentence case. +7. Identify which combo boxes are required or optional, and use the `is_required` field or the `necessity_indicator` to mark them accordingly. +8. A combo box's help text should provide actionable guidance on what to select and how to select it, offering additional context without repeating the placeholder text. +9. When an error occurs, the help text specified in a combo box is replaced by error text; thus, ensure both help and error text convey the same essential information to maintain consistent messaging and prevent loss of critical details. +10. Write error messages in a clear, concise, and helpful manner, guiding users to resolve the issue without ambiguity; ideally, they should be 1-2 short, complete sentences. ## Data sources @@ -262,13 +263,9 @@ my_combo_box_required_examples = ui_combo_box_required_examples() ## Selection -Use `selected_keys` or `default_selected_keys` to set the selected option(s). +In a combo box, the `default_selected_key` or `selected_key` props set a selected option. -`default_selected_keys` is useful for simpler scenarios where you don't need to control the state externally. `selected_keys` is used for scenarios where the state should be managed by the parent component, providing control and flexibility over the selection of the combo box. - - -> [!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 generally receive a list of keys (unless `None` in the case of `on_change`). +The `default_selected_key` is useful for simpler scenarios where you don't need to control the state externally. The `selected_key` is used for scenarios where the state should be managed by the parent component, providing control and flexibility over the selection of the combo box. ```python from deephaven import ui @@ -276,7 +273,7 @@ from deephaven import ui @ui.component def ui_combo_box_selected_key_examples(): - option, set_option = ui.use_state(["Option 1"]) + option, set_option = ui.use_state("Option 1") return [ ui.combo_box( ui.item("Option 1"), @@ -288,7 +285,7 @@ def ui_combo_box_selected_key_examples(): ui.item("Option 7"), ui.item("Option 8"), ui.item("Option 9"), - default_selected_keys=["Option 2"], + default_selected_key="Option 2", label="Pick an option (uncontrolled)", ), ui.combo_box( @@ -301,7 +298,7 @@ def ui_combo_box_selected_key_examples(): ui.item("Option 7"), ui.item("Option 8"), ui.item("Option 9"), - selected_keys=option, + selected_key=option, on_change=set_option, label="Pick an option (controlled)", ), @@ -351,16 +348,12 @@ my_combo_box_section_example = ui.combo_box( ## Events -Combo boxes support selection via mouse, keyboard, and touch. You can handle all these via the `on_change` prop. Additionally, combo boxes accept an `on_input_change` prop, which is triggered whenever the search value is edited by the user, whether through typing or option selection. +Combo boxes support selection via mouse, keyboard, and touch. You can handle all these via the `on_change` prop, which receives the selected key as an argument. Additionally, combo boxes accept an `on_input_change` prop, which is triggered whenever the search value is edited by the user, whether through typing or option selection. Each interaction done in the combo box will trigger its associated event handler. For instance, typing in the input field will only trigger the `on_input_change`, not the `on_change`. Note, this is not the case for selections; when a selection is made, both the `on_change` and `on_input_change` are triggered. - -> [!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 generally receive a list of keys (unless `None` in the case of `on_change`). - ```python from deephaven import ui @@ -368,17 +361,17 @@ from deephaven import ui @ui.component def ui_combo_box_control_example(): input_value, set_input_value = ui.use_state("") - selection_state, set_selection_state = ui.use_state([]) + selection_state, set_selection_state = ui.use_state("") def handle_input_change(new_value): - set_selection_state([""]) + set_selection_state("") set_input_value(new_value) - print(f"Text changed to {new_value}") + print(f"Text changed to {input_value}") def handle_selection_change(new_value): - set_input_value(new_value[0] if new_value else "") + set_input_value(new_value) set_selection_state(new_value) - print(f"Selection changed to {new_value}") + print(f"Selection changed to {selection_state}") return [ ui.combo_box( @@ -393,7 +386,7 @@ def ui_combo_box_control_example(): ui.item("Option 9"), input_value=input_value, on_input_change=handle_input_change, - selected_keys=selection_state, + selected_key=selection_state, on_change=handle_selection_change, ) ] @@ -608,7 +601,7 @@ my_combo_box_is_read_only_example = ui.combo_box( ui.item("Option 6", key="Option 6"), ui.item("Option 7", key="Option 7"), ui.item("Option 8", key="Option 8"), - default_selected_keys=["Option 1"], + default_selected_key="Option 1", is_read_only=True, ) ``` @@ -773,36 +766,84 @@ def ui_combo_box_alignment_direction_examples(): my_combo_box_alignment_direction_examples = ui_combo_box_alignment_direction_examples() ``` -## Multi-select +## How to create a multi-select component -Set `selection_mode="multiple"` to allow selecting multiple items. Selected items appear as tags inside the input area, and the dropdown list can be filtered by typing. +It's recommended to use [`multi_select`](multi_select.md) for `multi-select` use cases, but if you want the `combo_box` separate from the tags you can also use a `tag_group` to show selected items, and use the `on_input_change` and `on_change` events to manage the state between them. ```python from deephaven import ui @ui.component -def ui_combo_box_multi_select_example(): - selected, set_selected = ui.use_state([]) +def ui_combo_box_multi_select_example( + options, on_input_change_callback=None, on_selection_change_callback=None +): + input_value, set_input_value = ui.use_state("") + selection_state, set_selection_state = ui.use_state("") + items, set_items = ui.use_state([]) - return ui.combo_box( - ui.item("Option 1"), - ui.item("Option 2"), - ui.item("Option 3"), - ui.item("Option 4"), - ui.item("Option 5"), - ui.item("Option 6"), - ui.item("Option 7"), - ui.item("Option 8"), - ui.item("Option 9"), - selection_mode="multiple", - selected_keys=selected, - on_change=set_selected, - label="Pick options", - ) + def handle_input_change(new_value): + set_selection_state("") + set_input_value(new_value) + print(f"Text changed to {new_value}") + if on_input_change_callback: + on_input_change_callback(new_value) + + def handle_selection_change(new_value): + set_input_value("") + set_selection_state(new_value) + set_items( + lambda prev_items: prev_items + [new_value] + if new_value not in prev_items and new_value is not None + else prev_items + ) + print(f"Selection changed to {items}") + if on_selection_change_callback: + on_selection_change_callback(new_value, items) + + return [ + ui.flex( + ui.flex( + ui.combo_box( + *[ui.item(option) for option in options], + input_value=input_value, + on_input_change=handle_input_change, + selected_key=selection_state, + on_change=handle_selection_change, + ), + ui.tag_group( + *[ui.item(item, key=item.lower()) for item in items], + on_remove=lambda keys: set_items( + [item for item in items if item.lower() not in keys] + ), + ), + direction="row", + align_items="center", + ), + align_items="start", + ) + ] -my_combo_box_multi_select_example = ui_combo_box_multi_select_example() +my_options = [ + "Option 1", + "Option 2", + "Option 3", + "Option 4", + "Option 5", + "Option 6", + "Option 7", + "Option 8", + "Option 9", +] + +my_combo_box_multi_select_example = ui_combo_box_multi_select_example( + options=my_options, + on_input_change_callback=lambda value: print(f"Custom input handler: {value}"), + on_selection_change_callback=lambda value, items: print( + f"Custom selection handler: {value}, {items}" + ), +) ``` ## API Reference diff --git a/plugins/ui/docs/components/multi_select.md b/plugins/ui/docs/components/multi_select.md new file mode 100644 index 000000000..6df6396d5 --- /dev/null +++ b/plugins/ui/docs/components/multi_select.md @@ -0,0 +1,761 @@ +# Multi Select + +Multi select displays selected items as tags inside the input area and presents a filterable dropdown list for multi-selection. + +## Example + +```python +from deephaven import ui + + +@ui.component +def ui_multi_select_basic(): + selected, set_selected = ui.use_state([]) + + return ui.multi_select( + ui.item("red panda"), + ui.item("cat"), + ui.item("dog"), + ui.item("aardvark"), + ui.item("kangaroo"), + ui.item("snake"), + ui.item("ant"), + label="Favorite Animals", + selected_keys=selected, + on_change=set_selected, + ) + + +my_multi_select_basic = ui_multi_select_basic() +``` + +## UI Recommendations + +Recommendations for creating clear and effective multi selects: + +1. The multi select's text input simplifies searching through large lists. For lists with fewer than 6 items, use a checkbox group. +2. For selecting only one option, use a [`combo_box`](combo_box.md) instead. +3. Every multi select should have a label specified. Without one, the multi select is ambiguous and not accessible. +4. Options in the multi select should be kept short and concise; multiple lines are strongly discouraged. If more than one line is needed, consider using a description to add context to the option. +5. Choose a `width` for your multi selects that can accommodate most of the available options. +6. The field labels, menu items, and placeholder text should all be in sentence case. +7. Identify which multi selects are required or optional, and use the `is_required` field or the `necessity_indicator` to mark them accordingly. +8. A multi select's help text should provide actionable guidance on what to select and how to select it, offering additional context without repeating the placeholder text. +9. When an error occurs, the help text specified in a multi select is replaced by error text; thus, ensure both help and error text convey the same essential information to maintain consistent messaging and prevent loss of critical details. +10. Write error messages in a clear, concise, and helpful manner, guiding users to resolve the issue without ambiguity; ideally, they should be 1-2 short, complete sentences. + +## Data sources + +For multi selects, we can use a Deephaven table or [URI](uri.md) as a data source to populate the options. When using a table, it automatically uses the first column as both the key and label. If there are any duplicate keys, an error will be thrown; to avoid this, a `select_distinct` can be used on the table prior to using it as a multi select data source. + +```python order=my_multi_select_table_source_example,countries +from deephaven import ui +from deephaven.plot import express as dx + + +countries = dx.data.gapminder().select_distinct("Country") + + +my_multi_select_table_source_example = ui.multi_select(countries, label="Sample Multi Select") +``` + +## Item table sources + +If you wish to manually specify the keys and labels, use a `ui.item_table_source` to dynamically derive the options from a table. + +```python order=my_multi_select_item_table_source_example,column_types +from deephaven import ui, empty_table + +account_icon = "vsAccount" +columns = [ + "Key=new Integer(i)", + "Label=new String(`Display `+i)", + "Icon=(String) account_icon", +] +column_types = empty_table(20).update(columns) + + +item_table_source = ui.item_table_source( + column_types, + key_column="Key", + label_column="Label", + icon_column="Icon", +) + + +my_multi_select_item_table_source_example = ui.multi_select( + item_table_source, label="User Multi Select" +) +``` + +## Custom Value + +By default, when a multi select loses focus, it resets its input value. To allow users to enter custom values as tags, use the `allows_custom_value` prop. Pressing Enter when no item is focused adds the typed text as a custom tag. If the typed text matches an existing item's label, that item's key is used instead. + +```python +from deephaven import ui + + +@ui.component +def ui_multi_select_custom_value_example(): + selected, set_selected = ui.use_state([]) + return ui.multi_select( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + allows_custom_value=True, + selected_keys=selected, + on_change=set_selected, + label="Select or type options", + ) + + +my_multi_select_custom_value_example = ui_multi_select_custom_value_example() +``` + +## HTML Forms + +Multi selects can support a `name` prop for integration with HTML forms, allowing for easy identification of a value on form submission. The `form_value` prop determines whether comma-joined keys or labels of the selected items are submitted via the hidden form input. + +```python +from deephaven import ui + + +@ui.component +def ui_multi_select_form_examples(): + return [ + ui.form( + ui.multi_select( + ui.item("Chocolate"), + ui.item("Mint"), + ui.item("Vanilla"), + ui.item("Strawberry"), + ui.item("Cookies and Cream"), + ui.item("Coffee"), + ui.item("Mango"), + label="Ice cream flavors", + name="flavors", + ), + ui.button("Submit", type="submit"), + on_submit=lambda event: print(event), + ) + ] + + +my_multi_select_form_examples = ui_multi_select_form_examples() +``` + +## Labeling + +The multi select can be labeled using the `label` prop, and if no label is provided, an `aria_label` must be provided to identify the control for accessibility purposes. + +```python +from deephaven import ui + + +@ui.component +def ui_multi_select_label_examples(): + return [ + ui.multi_select( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + label="Pick options", + ), + ui.multi_select( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + aria_label="Pick options", + ), + ] + + +my_multi_select_label_examples = ui_multi_select_label_examples() +``` + +The `is_required` prop and the `necessity_indicator` props can be used to show whether selecting an option in the multi select is required or optional. + +When the `necessity_indicator` prop is set to "label", a localized string will be generated for "(required)" or "(optional)" automatically. + +```python +from deephaven import ui + + +@ui.component +def ui_multi_select_required_examples(): + return [ + ui.multi_select( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + label="Pick options", + is_required=True, + ), + ui.multi_select( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + label="Pick options", + is_required=True, + necessity_indicator="label", + ), + ui.multi_select( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + ui.item("Option 9"), + label="Pick options", + necessity_indicator="label", + ), + ] + + +my_multi_select_required_examples = ui_multi_select_required_examples() +``` + +## Selection + +Use `selected_keys` or `default_selected_keys` to set the selected options. + +`default_selected_keys` is useful for simpler scenarios where you don't need to control the state externally. `selected_keys` is used for scenarios where the state should be managed by the parent component, providing control and flexibility over the selection of the multi select. + +```python +from deephaven import ui + + +@ui.component +def ui_multi_select_selected_keys_examples(): + options, set_options = ui.use_state(["Option 1", "Option 3"]) + return [ + ui.multi_select( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + ui.item("Option 9"), + default_selected_keys=["Option 2", "Option 4"], + label="Pick options (uncontrolled)", + ), + ui.multi_select( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + ui.item("Option 9"), + selected_keys=options, + on_change=set_options, + label="Pick options (controlled)", + ), + ] + + +my_multi_select_selected_keys_examples = ui_multi_select_selected_keys_examples() +``` + +## Sections + +Multi selects support sections to group options. Sections can be used by wrapping groups of items in a Section element. Each Section takes a title and key prop. + +Note that, when searching for options, searching by section will not result in the respective options within that section appearing. + +Also, sections can only be used directly, not from a table data source. + +```python +from deephaven import ui + + +my_multi_select_section_example = ui.multi_select( + ui.section( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + title="Section 1", + ), + ui.section( + ui.item("Option 9"), + ui.item("Option 10"), + ui.item("Option 11"), + ui.item("Option 12"), + ui.item("Option 13"), + ui.item("Option 14"), + ui.item("Option 15"), + ui.item("Option 16"), + title="Section 2", + ), + label="Pick options", +) +``` + +## Events + +Multi selects support selection via mouse, keyboard, and touch. You can handle all these via the `on_change` prop. Additionally, multi selects accept an `on_input_change` prop, which is triggered whenever the search value is edited by the user, whether through typing or option selection. + +Each interaction done in the multi select will trigger its associated event handler. For instance, typing in the input field will only trigger the `on_input_change`, not the `on_change`. + +Note, this is not the case for selections; when a selection is made, both the `on_change` and `on_input_change` are triggered. + +```python +from deephaven import ui + + +@ui.component +def ui_multi_select_events_example(): + input_value, set_input_value = ui.use_state("") + selection_state, set_selection_state = ui.use_state([]) + + def handle_input_change(new_value): + set_input_value(new_value) + print(f"Text changed to {new_value}") + + def handle_selection_change(new_value): + set_selection_state(new_value) + print(f"Selection changed to {new_value}") + + return [ + ui.multi_select( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + ui.item("Option 9"), + input_value=input_value, + on_input_change=handle_input_change, + selected_keys=selection_state, + on_change=handle_selection_change, + label="Pick options", + ) + ] + + +my_multi_select_events_example = ui_multi_select_events_example() +``` + +## Complex items + +Items within a multi select can include additional content to better convey options. You can add icons, avatars, and descriptions to the children of an `ui.item`. When adding a description, set the `slot` prop to "description" to differentiate between the text elements. + +```python +from deephaven import ui + + +my_multi_select_complex_items_example = ui.multi_select( + ui.item( + ui.icon("vsGithubAlt"), + ui.text("Github"), + ui.text("Github Option", slot="description"), + text_value="Github", + ), + ui.item( + ui.icon("vsAzureDevops"), + ui.text("Azure"), + ui.text("Azure Option", slot="description"), + text_value="Azure", + ), + label="Pick services", +) +``` + +## Validation + +The `is_required` prop ensures that the user selects an option. The related `validation_behaviour` prop allows the user to specify aria or native verification. + +When the prop is set to "native", the validation errors block form submission and are displayed as help text automatically. + +```python +from deephaven import ui + + +@ui.component +def ui_multi_select_validation_behaviour_example(): + return ui.form( + ui.multi_select( + ui.section(ui.item("Option 1"), ui.item("Option 2"), title="Section 1"), + validation_behavior="aria", + is_required=True, + label="Pick options", + ) + ) + + +my_multi_select_validation_behaviour_example = ( + ui_multi_select_validation_behaviour_example() +) +``` + +## Trigger Options + +By default, the multi select's menu opens when the user types into the input field ("input"). This behavior can be changed to open on focus ("focus") or only when the field button is clicked ("manual") using the `menu_trigger` prop. + +```python +from deephaven import ui + + +@ui.component +def ui_multi_select_trigger_option_examples(): + return [ + ui.multi_select( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + ui.item("Option 9"), + label="Select Options", + ), + ui.multi_select( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + ui.item("Option 9"), + label="Select Options", + menu_trigger="focus", + ), + ui.multi_select( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + ui.item("Option 9"), + label="Select Options", + menu_trigger="manual", + ), + ] + + +my_multi_select_trigger_option_examples = ui_multi_select_trigger_option_examples() +``` + +## Label position + +By default, the position of a multi select's label is above the multi select, but it can be moved to the side using the `label_position` prop. + +```python +from deephaven import ui + + +@ui.component +def ui_multi_select_label_position_examples(): + return [ + ui.multi_select( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + ui.item("Option 9"), + label="Test Label", + ), + ui.multi_select( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + ui.item("Option 9"), + label="Test Label", + label_position="side", + ), + ] + + +my_multi_select_label_position_examples = ui_multi_select_label_position_examples() +``` + +## Quiet State + +The `is_quiet` prop makes a multi select "quiet". This can be useful when the multi select and its corresponding styling should not distract users from surrounding content. + +```python +from deephaven import ui + + +my_multi_select_is_quiet_example = ui.multi_select( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + ui.item("Option 9"), + is_quiet=True, + label="Pick options", +) +``` + +## Disabled State + +The `is_disabled` prop disables a multi select to prevent user interaction. This is useful when the multi select should be visible but unavailable for selection. + +```python +from deephaven import ui + + +my_multi_select_is_disabled_example = ui.multi_select( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + ui.item("Option 9"), + is_disabled=True, + label="Pick options", +) +``` + +## Read-only State + +The `is_read_only` prop prevents user input in a multi select, but the selected options should be visible. + +```python +from deephaven import ui + + +my_multi_select_is_read_only_example = ui.multi_select( + ui.item("Option 1", key="Option 1"), + ui.item("Option 2", key="Option 2"), + ui.item("Option 3", key="Option 3"), + ui.item("Option 4", key="Option 4"), + ui.item("Option 5", key="Option 5"), + ui.item("Option 6", key="Option 6"), + ui.item("Option 7", key="Option 7"), + ui.item("Option 8", key="Option 8"), + default_selected_keys=["Option 1", "Option 3"], + is_read_only=True, + label="Pick options", +) +``` + +## Help text + +A multi select can have both a `description` and an `error_message`. The description remains visible at all times. Use the error message to offer specific guidance on how to correct the input. + +```python +from deephaven import ui + + +@ui.component +def ui_multi_select_help_text_examples(): + return [ + ui.multi_select( + ui.section( + ui.item("Option 1", key="Option 1"), + ui.item("Option 2", key="Option 2"), + ui.item("Option 3", key="Option 3"), + ui.item("Option 4", key="Option 4"), + ui.item("Option 5", key="Option 5"), + ui.item("Option 6", key="Option 6"), + ui.item("Option 7", key="Option 7"), + ui.item("Option 8", key="Option 8"), + title="Section 1", + ), + label="Sample Label", + description="Select one or more options.", + ), + ui.multi_select( + ui.section( + ui.item("Option 1", key="Option 1"), + ui.item("Option 2", key="Option 2"), + ui.item("Option 3", key="Option 3"), + ui.item("Option 4", key="Option 4"), + ui.item("Option 5", key="Option 5"), + ui.item("Option 6", key="Option 6"), + ui.item("Option 7", key="Option 7"), + ui.item("Option 8", key="Option 8"), + title="Section 1", + ), + label="Sample Label", + validation_state="invalid", + error_message="Sample invalid error message.", + ), + ] + + +my_multi_select_help_text_examples = ui_multi_select_help_text_examples() +``` + +## Contextual Help + +Using the `contextual_help` prop, a `ui.contextual_help` can be placed next to the label to provide additional information about the multi select. + +```python +from deephaven import ui + + +my_multi_select_contextual_help_example = ui.multi_select( + ui.section( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + title="Section 1", + ), + label="Sample Label", + contextual_help=ui.contextual_help( + ui.heading("Content tips"), ui.content("Tips for the content.") + ), +) +``` + +## Custom width + +The `width` prop adjusts the width of a multi select, and the `max_width` prop enforces a maximum width. + +```python +from deephaven import ui + + +@ui.component +def ui_multi_select_width_examples(): + return [ + ui.multi_select( + ui.item("Option 1", key="Option 1"), + ui.item("Option 2", key="Option 2"), + ui.item("Option 3", key="Option 3"), + ui.item("Option 4", key="Option 4"), + ui.item("Option 5", key="Option 5"), + ui.item("Option 6", key="Option 6"), + ui.item("Option 7", key="Option 7"), + ui.item("Option 8", key="Option 8"), + width="size-3600", + ), + ui.multi_select( + ui.item("Option 1", key="Option 1"), + ui.item("Option 2", key="Option 2"), + ui.item("Option 3", key="Option 3"), + ui.item("Option 4", key="Option 4"), + ui.item("Option 5", key="Option 5"), + ui.item("Option 6", key="Option 6"), + ui.item("Option 7", key="Option 7"), + ui.item("Option 8", key="Option 8"), + width="size-3600", + max_width="100%", + ), + ] + + +my_multi_select_width_examples = ui_multi_select_width_examples() +``` + +## Align and Direction + +The `align` prop sets the text alignment of the options in the multi select, while the `direction` prop specifies which direction the menu will open. + +```python +from deephaven import ui + + +@ui.component +def ui_multi_select_alignment_direction_examples(): + return ui.view( + ui.flex( + ui.multi_select( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + align="end", + menu_width="size-3000", + ), + ui.multi_select( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + direction="top", + ), + gap="size-150", + direction="column", + ), + padding=40, + ) + + +my_multi_select_alignment_direction_examples = ( + ui_multi_select_alignment_direction_examples() +) +``` + +## API Reference + +```{eval-rst} +.. dhautofunction:: deephaven.ui.multi_select +``` diff --git a/plugins/ui/src/deephaven/ui/components/__init__.py b/plugins/ui/src/deephaven/ui/components/__init__.py index 7f9e456f6..66b8b78bf 100644 --- a/plugins/ui/src/deephaven/ui/components/__init__.py +++ b/plugins/ui/src/deephaven/ui/components/__init__.py @@ -53,6 +53,7 @@ from .menu import menu from .menu_trigger import menu_trigger from .meter import meter +from .multi_select import multi_select from .number_field import number_field from .panel import panel from .picker import picker @@ -141,6 +142,7 @@ "menu", "menu_trigger", "meter", + "multi_select", "number_field", "panel", "picker", diff --git a/plugins/ui/src/deephaven/ui/components/combo_box.py b/plugins/ui/src/deephaven/ui/components/combo_box.py index c4b46a079..58b3da335 100644 --- a/plugins/ui/src/deephaven/ui/components/combo_box.py +++ b/plugins/ui/src/deephaven/ui/components/combo_box.py @@ -1,8 +1,6 @@ from __future__ import annotations -import logging -import warnings -from typing import Callable, Any, Literal, Sequence +from typing import Callable, Any from .types import ( FocusEventCallable, @@ -30,13 +28,10 @@ from .item import Item from .item_table_source import ItemTableSource from ..elements import BaseElement, Element, NodeType -from .._internal.utils import create_props, unpack_item_table_source, wrap_callable -from ..types import Key, Selection, Undefined, UndefinedType +from .._internal.utils import create_props, unpack_item_table_source +from ..types import Key, Undefined, UndefinedType from .basic import component_element - -logger = logging.getLogger(__name__) - ComboBoxElement = BaseElement SUPPORTED_SOURCE_ARGS = { @@ -49,156 +44,9 @@ _NULLABLE_PROPS = ["selected_key"] -# Props that only apply to single-select ComboBox and are stripped in multi mode. -_SINGLE_ONLY_PROPS = { - "selected_key", - "default_selected_key", -} - -# Props that only apply to multi-select mode and are stripped in single mode. -_MULTI_ONLY_PROPS = { - "selected_keys", - "default_selected_keys", -} - -_SELECTION_CALLBACKS = {"on_selection_change", "on_change"} - - -def _wrap_callback_as_selection( - callback: Callable[..., None] | None, - callback_name: str | None = None, -) -> Callable[..., None] | None: - """ - Wrap a callback so it always receives a Selection instead of (possibly) a single Key. - - Args: - callback: The callback to wrap. - - Returns: - A wrapped callback that always receives a Selection. - """ - if callback is None: - return None - - wrapped = wrap_callable(callback) - - def wrapper(value: Any) -> None: - if isinstance(value, (str, int, float, bool)): - wrapped([value]) - elif value is None and callback_name == "on_change": - # on_change with None means an empty list to be consistent with the typing - wrapped([]) - else: - wrapped(value) - - return wrapper - - -def _convert_selection_prop( - props: dict[str, Any], - multi_prop: str, - single_prop: str, - is_multiple: bool, - default_val: Any, -) -> bool: - """ - Convert between single and multi select props based on the selection mode, emitting warnings as needed. - - Args: - props: The props dict to modify in place. - multi_prop: The name of the multi-select prop (e.g. "selected_keys"). - single_prop: The name of the single-select prop (e.g. "selected_key"). - is_multiple: Whether multi-select mode is active. - default_val: The default value to use (for the single prop only). - - Returns: - True if callbacks should always return a Key - """ - multi_val = props.pop(multi_prop) - single_val = props.pop(single_prop) - - if single_val is not default_val: - if is_multiple: - # Throw an error if the user is trying to use the single prop in multi-select mode since it shouldn't work - raise ValueError( - f"'{single_prop}' cannot be used when 'selection_mode' is 'multiple'. Use '{multi_prop}' instead." - ) - # Otherwise use the single prop value - # Warn and don't convert callbacks since the user is using the deprecated single prop which expects a single Key - warnings.warn( - f"'{single_prop}' is deprecated. Use '{multi_prop}' instead.", - FutureWarning, - stacklevel=2, - ) - props[single_prop] = single_val - return True - - if is_multiple: - # In multi-select mode, multi_prop is expected - props[multi_prop] = multi_val - return False - - if multi_val is not None: - # multi_prop is provided in single-select mode, so we need to convert it to the single prop - if not isinstance(multi_val, Sequence) or isinstance(multi_val, str): - raise ValueError( - f"'{multi_prop}' should be a Sequence when 'selection_mode' is 'single'. Got type: {type(multi_val)}" - ) - if len(multi_val) > 1: - logger.warning( - f"'{multi_prop}' should be a Sequence with at most one key when 'selection_mode' is 'single'. Got: {multi_val}. Only the first value will be used.", - stacklevel=2, - ) - # Use the single prop for the ComboBox - props[single_prop] = multi_val[0] if multi_val else None - # In single-select mode but using the multi prop, so the callbacks receive a Selection - return False - - # No value provided for either prop, so keep the callback as is, expecting a Selection - # This is technically an ambiguous case and may conflict with deprecated usage - # but we have no way to know if the user intends to use the Key or Selection callbacks - # without a hacky check of some sort. - props[single_prop] = single_val - return False - - -def _process_selection_props( - props: dict[str, Any], - is_multiple: bool, -) -> None: - """ - Process selection-related props: emit deprecation warnings, convert or - strip inapplicable props, and wrap callbacks when needed. - - In single-select mode with the new selected_keys / default_selected_keys - props, converts them to selected_key / default_selected_key (which the - ComboBox understands) and wraps callbacks so they receive a Selection. - - When the deprecated selected_key / default_selected_key props are - used, callbacks continue to receive a single Key. - - Args: - props: Mutable props dict - is_multiple: Whether multi-select mode is active. - """ - selected_takes_key = _convert_selection_prop( - props, "selected_keys", "selected_key", is_multiple, Undefined - ) - default_takes_key = _convert_selection_prop( - props, "default_selected_keys", "default_selected_key", is_multiple, None - ) - - if not (selected_takes_key or default_takes_key): - # We aren't in the deprecated single prop case, so we need to convert callbacks to always receive a Selection - for cb_name in _SELECTION_CALLBACKS: - cb = props.get(cb_name) - if cb is not None: - props[cb_name] = _wrap_callback_as_selection(cb, cb_name) - def combo_box( *children: Item | SectionElement | Table | PartitionedTable | ItemTableSource, - selection_mode: Literal["single", "multiple"] = "single", menu_trigger: MenuTriggerAction | None = "input", is_quiet: bool | None = None, align: Align | None = "end", @@ -214,8 +62,6 @@ def combo_box( disabled_keys: list[Key] | None = None, selected_key: Key | None | UndefinedType = Undefined, default_selected_key: Key | None = None, - selected_keys: Selection | None = None, - default_selected_keys: Selection | None = None, is_disabled: bool | None = None, is_read_only: bool | None = None, is_required: bool | None = None, @@ -231,8 +77,8 @@ def combo_box( necessity_indicator: NecessityIndicator | None = None, contextual_help: Element | None = None, on_open_change: Callable[[bool, MenuTriggerAction], None] | None = None, - on_selection_change: Callable[[Key | Selection | None], None] | None = None, - on_change: Callable[[Key | Selection], None] | None = None, + on_selection_change: Callable[[Key | None], None] | None = None, + on_change: Callable[[Key], None] | None = None, on_input_change: Callable[[str], None] | None = None, on_focus: Callable[[FocusEventCallable], None] | None = None, on_blur: Callable[[FocusEventCallable], None] | None = None, @@ -285,13 +131,7 @@ def combo_box( key: str | None = None, ) -> ComboBoxElement: """ - A combo box that can be used to search or select from a list. - - When `selection_mode="single"` (default), behaves as a standard ComboBox with a single - selected value. When `selection_mode="multiple"`, displays selected items as tags inside the - input area and presents a filterable dropdown list for multi-selection. - - Children should be one of five types: + A combo box that can be used to search or select from a list. Children should be one of five types: 1. If children are of type `Item`, they are the dropdown options. 2. If children are of type `SectionElement`, they are the dropdown sections. @@ -308,8 +148,6 @@ def combo_box( Args: *children: The options to render within the combo box. - selection_mode: Whether the combo box allows single or multiple selection. - Defaults to `"single"`. menu_trigger: The interaction required to display the ComboBox menu. is_quiet: Whether the ComboBox should be displayed with a quiet style. align: Alignment of the menu relative to the input target. @@ -319,22 +157,16 @@ def combo_box( should_flip: Whether the menu should automatically flip direction when space is limited. menu_width: Width of the menu. By default, matches width of the combobox. Note that the minimum width of the dropdown is always equal to the combobox's width. - form_value: Whether the text or key of the selected item(s) is submitted as part of an HTML form. - In single-select mode, when `allows_custom_value` is true, this option does not apply and the - text is always submitted. In multi-select mode, controls whether comma-joined keys or labels - are submitted via the hidden form input. + form_value: Whether the text or key of the selected item is submitted as part of an HTML form. + When allowsCustomValue is true, this option does not apply and the text is always submitted. should_focus_wrap: Whether keyboard navigation is circular. input_value: The value of the search input (controlled). default_input_value: The default value of the search input (uncontrolled). allows_custom_value: Whether the ComboBox allows a non-item matching input value to be set. - In multi-select mode, pressing Enter when no item is focused adds the typed text as a custom tag. - If the typed text matches an existing item's label, that item's key is used instead. disabled_keys: The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. - selected_key: Deprecated. Use `selected_keys` instead. - default_selected_key: Deprecated. Use `default_selected_keys` instead. - selected_keys: The currently selected keys in the collection (controlled). - default_selected_keys: The initial selected keys in the collection (uncontrolled). + selected_key: The currently selected key in the collection (controlled). + default_selected_key: The initial selected key in the collection (uncontrolled). is_disabled: Whether the input is disabled. is_read_only: Whether the input can be selected but not changed by the user. is_required: Whether user input is required on the input before form submission. @@ -353,11 +185,7 @@ def combo_box( on_open_change: Method that is called when the open state of the menu changes. Returns the new open state and the action that caused the opening of the menu. on_selection_change: Handler that is called when the selection changes. - Receives a `Selection` (list of keys). When the deprecated `selected_key` - or `default_selected_key` props are used, receives a single `Key` instead. on_change: Alias of `on_selection_change`. Handler that is called when the selection changes. - Receives a `Selection` (list of keys). When the deprecated `selected_key` - or `default_selected_key` props are used, receives a single `Key` instead. on_input_change: Handler that is called when the ComboBox input value changes. on_focus: Handler that is called when the element receives focus. on_blur: Handler that is called when the element loses focus. @@ -414,15 +242,8 @@ def combo_box( """ children, props = create_props(locals()) - is_multiple = props.pop("selection_mode", "single") == "multiple" - - _process_selection_props(props, is_multiple) - children, props = unpack_item_table_source(children, props, SUPPORTED_SOURCE_ARGS) return component_element( - "MultiSelect" if is_multiple else "ComboBox", - *children, - _nullable_props=[] if is_multiple else _NULLABLE_PROPS, - **props, + "ComboBox", *children, _nullable_props=_NULLABLE_PROPS, **props ) diff --git a/plugins/ui/src/deephaven/ui/components/multi_select.py b/plugins/ui/src/deephaven/ui/components/multi_select.py new file mode 100644 index 000000000..af494decc --- /dev/null +++ b/plugins/ui/src/deephaven/ui/components/multi_select.py @@ -0,0 +1,257 @@ +from __future__ import annotations + +from typing import Callable, Any, Sequence + +from .types import ( + FocusEventCallable, + KeyboardEventCallable, + LayoutFlex, + DimensionValue, + AlignSelf, + JustifySelf, + Position, + CSSProperties, + LabelPosition, + ValidationBehavior, + NecessityIndicator, + ValidationState, + MenuTriggerAction, + Align, + MenuDirection, + LoadingState, + FormValue, + Alignment, +) + +from deephaven.table import Table, PartitionedTable +from .section import SectionElement +from .item import Item +from .item_table_source import ItemTableSource +from ..elements import BaseElement, Element, NodeType +from .._internal.utils import create_props, unpack_item_table_source +from ..types import Key, Selection +from .basic import component_element + + +MultiSelectElement = BaseElement + +SUPPORTED_SOURCE_ARGS = { + "key_column", + "label_column", + "description_column", + "icon_column", + "title_column", +} + + +def multi_select( + *children: Item | SectionElement | Table | PartitionedTable | ItemTableSource, + menu_trigger: MenuTriggerAction | None = "input", + is_quiet: bool | None = None, + align: Align | None = "end", + direction: MenuDirection | None = "bottom", + loading_state: LoadingState | None = None, + should_flip: bool = True, + menu_width: DimensionValue | None = None, + form_value: FormValue | None = "text", + should_focus_wrap: bool | None = None, + input_value: str | None = None, + default_input_value: str | None = None, + allows_custom_value: bool | None = None, + disabled_keys: list[Key] | None = None, + selected_keys: Selection | None = None, + default_selected_keys: Selection | None = None, + is_disabled: bool | None = None, + is_read_only: bool | None = None, + is_required: bool | None = None, + validation_behavior: ValidationBehavior = "aria", + auto_focus: bool | None = None, + label: NodeType = None, + description: Element | None = None, + error_message: Element | None = None, + name: str | None = None, + validation_state: ValidationState | None = None, + label_position: LabelPosition = "top", + label_align: Alignment | None = None, + necessity_indicator: NecessityIndicator | None = None, + contextual_help: Element | None = None, + on_open_change: Callable[[bool, MenuTriggerAction], None] | None = None, + on_selection_change: Callable[[Selection], None] | None = None, + on_change: Callable[[Selection], None] | None = None, + on_input_change: Callable[[str], None] | None = None, + on_focus: Callable[[FocusEventCallable], None] | None = None, + on_blur: Callable[[FocusEventCallable], None] | None = None, + on_focus_change: Callable[[bool], None] | None = None, + on_key_down: Callable[[KeyboardEventCallable], None] | None = None, + on_key_up: Callable[[KeyboardEventCallable], None] | None = None, + flex: LayoutFlex | None = None, + flex_grow: float | None = None, + flex_shrink: float | None = None, + flex_basis: DimensionValue | None = None, + align_self: AlignSelf | None = None, + justify_self: JustifySelf | None = None, + order: int | None = None, + grid_area: str | None = None, + grid_row: str | None = None, + grid_row_start: str | None = None, + grid_row_end: str | None = None, + grid_column: str | None = None, + grid_column_start: str | None = None, + grid_column_end: str | None = None, + margin: DimensionValue | None = None, + margin_top: DimensionValue | None = None, + margin_bottom: DimensionValue | None = None, + margin_start: DimensionValue | None = None, + margin_end: DimensionValue | None = None, + margin_x: DimensionValue | None = None, + margin_y: DimensionValue | None = None, + width: DimensionValue | None = None, + height: DimensionValue | None = None, + min_width: DimensionValue | None = None, + min_height: DimensionValue | None = None, + max_width: DimensionValue | None = None, + max_height: DimensionValue | None = None, + position: Position | None = None, + top: DimensionValue | None = None, + bottom: DimensionValue | None = None, + start: DimensionValue | None = None, + end: DimensionValue | None = None, + left: DimensionValue | None = None, + right: DimensionValue | None = None, + z_index: int | None = None, + is_hidden: bool | None = None, + id: str | None = None, + aria_label: str | None = None, + aria_labelledby: str | None = None, + aria_describedby: str | None = None, + aria_details: str | None = None, + UNSAFE_class_name: str | None = None, + UNSAFE_style: CSSProperties | None = None, + key: str | None = None, +) -> MultiSelectElement: + """ + A multi-select component that displays selected items as tags inside the input area + and presents a filterable dropdown list for multi-selection. + + Children should be one of five types: + + 1. If children are of type `Item`, they are the dropdown options. + 2. If children are of type `SectionElement`, they are the dropdown sections. + 3. If children are of type `Table`, the values in the table are the dropdown options. + There can only be one child, the `Table`. + The first column is used as the key and label by default. + 4. If children are of type `PartitionedTable`, the values in the table are the dropdown options + and the partitions create multiple sections. There can only be one child, the `PartitionedTable`. + The first column is used as the key and label by default. + 5. If children are of type `ItemTableSource`, complex items are created from the source. + There can only be one child, the `ItemTableSource`. + Supported ItemTableSource arguments are `key_column`, `label_column`, `description_column`, + `icon_column`, and `title_column`. + + Args: + *children: The options to render within the multi-select. + menu_trigger: The interaction required to display the menu. + is_quiet: Whether the component should be displayed with a quiet style. + align: Alignment of the menu relative to the input target. + direction: Direction the menu will render relative to the component. + loading_state: The current loading state. + Determines whether or not the progress circle should be shown. + should_flip: Whether the menu should automatically flip direction when space is limited. + menu_width: Width of the menu. By default, matches width of the component. + Note that the minimum width of the dropdown is always equal to the component's width. + form_value: Whether the text or key of the selected items is submitted as part of an HTML form. + Controls whether comma-joined keys or labels are submitted via the hidden form input. + should_focus_wrap: Whether keyboard navigation is circular. + input_value: The value of the search input (controlled). + default_input_value: The default value of the search input (uncontrolled). + allows_custom_value: Whether the component allows a non-item matching input value to be set. + Pressing Enter when no item is focused adds the typed text as a custom tag. + If the typed text matches an existing item's label, that item's key is used instead. + disabled_keys: The item keys that are disabled. + These items cannot be selected, focused, or otherwise interacted with. + selected_keys: The currently selected keys in the collection (controlled). + default_selected_keys: The initial selected keys in the collection (uncontrolled). + is_disabled: Whether the input is disabled. + is_read_only: Whether the input can be selected but not changed by the user. + is_required: Whether user input is required on the input before form submission. + validation_behavior: Whether to use native HTML form validation to prevent + form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA. + auto_focus: Whether the element should receive focus on render. + label: The content to display as the label. + description: A description for the field. Provides a hint such as specific requirements for what to choose. + error_message: An error message for the field. + name: The name of the input element, used when submitting an HTML form. + validation_state: Whether the input should display its "valid" or "invalid" visual styling. + label_position: The label's overall position relative to the element it is labeling. + label_align: The label's horizontal alignment relative to the element it is labeling. + necessity_indicator: Whether the required state should be shown as an icon or text. + contextual_help: A ContextualHelp element to place next to the label. + on_open_change: Method that is called when the open state of the menu changes. + Returns the new open state and the action that caused the opening of the menu. + on_selection_change: Handler that is called when the selection changes. + Receives a `Selection` (list of keys). + on_change: Alias of `on_selection_change`. Handler that is called when the selection changes. + Receives a `Selection` (list of keys). + on_input_change: Handler that is called when the input value changes. + on_focus: Handler that is called when the element receives focus. + on_blur: Handler that is called when the element loses focus. + on_focus_change: Handler that is called when the element's focus status changes. + on_key_down: Handler that is called when a key is pressed. + on_key_up: Handler that is called when a key is released. + flex: When used in a flex layout, specifies how the element will grow or shrink to fit the space available. + flex_grow: When used in a flex layout, specifies how much the element will grow to fit the space available. + flex_shrink: When used in a flex layout, specifies how much the element will shrink to fit the space available. + flex_basis: When used in a flex layout, specifies the initial size of the element. + align_self: Overrides the align_items property of a flex or grid container. + justify_self: Specifies how the element is justified inside a flex or grid container. + order: The layout order for the element within a flex or grid container. + grid_area: The name of the grid area to place the element in. + grid_row: The name of the grid row to place the element in. + grid_row_start: The name of the grid row to start the element in. + grid_row_end: The name of the grid row to end the element in. + grid_column: The name of the grid column to place the element in. + grid_column_start: The name of the grid column to start the element in. + grid_column_end: The name of the grid column to end the element in. + margin: The margin to apply around the element. + margin_top: The margin to apply above the element. + margin_bottom: The margin to apply below the element. + margin_start: The margin to apply before the element. + margin_end: The margin to apply after the element. + margin_x: The margin to apply to the left and right of the element. + margin_y: The margin to apply to the top and bottom of the element. + width: The width of the element. + height: The height of the element. + min_width: The minimum width of the element. + min_height: The minimum height of the element. + max_width: The maximum width of the element. + max_height: The maximum height of the element. + position: Specifies how the element is positioned. + top: The distance from the top of the containing element. + bottom: The distance from the bottom of the containing element. + start: The distance from the start of the containing element. + end: The distance from the end of the containing element. + left: The distance from the left of the containing element. + right: The distance from the right of the containing element. + z_index: The stack order of the element. + is_hidden: Whether the element is hidden. + id: A unique identifier for the element. + aria_label: The label for the element. + aria_labelledby: The id of the element that labels the element. + aria_describedby: The id of the element that describes the element. + aria_details: The details for the element. + UNSAFE_class_name: A CSS class to apply to the element. + UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. + + Returns: + The rendered MultiSelect. + """ + children, props = create_props(locals()) + + children, props = unpack_item_table_source(children, props, SUPPORTED_SOURCE_ARGS) + + return component_element( + "MultiSelect", + *children, + **props, + ) diff --git a/plugins/ui/test/deephaven/ui/test_combo_box.py b/plugins/ui/test/deephaven/ui/test_combo_box.py index 9336350e2..9efa6db91 100644 --- a/plugins/ui/test/deephaven/ui/test_combo_box.py +++ b/plugins/ui/test/deephaven/ui/test_combo_box.py @@ -1,290 +1,33 @@ import unittest -import warnings from .BaseTest import BaseTestCase -class ComboBoxProcessSelectionPropsTest(BaseTestCase): - def setUp(self): - from deephaven.ui.types import Undefined - - self.Undefined = Undefined - - def _process(self, props, is_multiple): - from deephaven.ui.components.combo_box import _process_selection_props - - with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - _process_selection_props(props, is_multiple) - - def test_single_mode_converts_selected_keys_to_selected_key(self): - props = { - "selected_key": self.Undefined, - "default_selected_key": None, - "selected_keys": ["a", "b"], - "default_selected_keys": ["c"], - "other": "value", - } - self._process(props, is_multiple=False) - self.assertNotIn("selected_keys", props) - self.assertNotIn("default_selected_keys", props) - self.assertEqual(props["selected_key"], "a") - self.assertEqual(props["default_selected_key"], "c") - self.assertEqual(props["other"], "value") - - def test_single_mode_converts_empty_selected_keys_to_none(self): - props = { - "selected_key": self.Undefined, - "default_selected_key": None, - "selected_keys": [], - "default_selected_keys": [], - } - self._process(props, is_multiple=False) - self.assertIsNone(props["selected_key"]) - self.assertIsNone(props["default_selected_key"]) - - def test_single_mode_no_conversion_when_keys_none(self): - props = { - "selected_key": self.Undefined, - "default_selected_key": None, - "selected_keys": None, - "default_selected_keys": None, - } - self._process(props, is_multiple=False) - # When selected_keys is None, falls through and sets single prop to its default - self.assertEqual(props["selected_key"], self.Undefined) - self.assertEqual(props["default_selected_key"], None) - self.assertNotIn("selected_keys", props) - self.assertNotIn("default_selected_keys", props) - - def test_single_mode_deprecated_strips_multi_props(self): - props = { - "selected_key": "a", - "default_selected_key": None, - "selected_keys": ["x"], - "default_selected_keys": ["y"], - } - self._process(props, is_multiple=False) - self.assertNotIn("selected_keys", props) - self.assertNotIn("default_selected_keys", props) - self.assertEqual(props["selected_key"], "a") - - def test_multiple_mode_strips_single_props(self): - props = { - "selected_key": self.Undefined, - "default_selected_key": None, - "selected_keys": None, - "default_selected_keys": None, - "other": "value", - } - self._process(props, is_multiple=True) - self.assertNotIn("selected_key", props) - self.assertNotIn("default_selected_key", props) - self.assertEqual(props["other"], "value") - - def test_multiple_mode_raises_when_single_props_set(self): - props = { - "selected_key": "some_key", - "default_selected_key": "other", - "selected_keys": None, - "default_selected_keys": None, - } - with self.assertRaises(ValueError): - self._process(props, is_multiple=True) - - -class ComboBoxWrapCallbackTest(BaseTestCase): - def _wrap(self, callback): - from deephaven.ui.components.combo_box import _wrap_callback_as_selection - - return _wrap_callback_as_selection(callback) - - def test_none_returns_none(self): - self.assertIsNone(self._wrap(None)) - - def test_wraps_string_key(self): - received = [] - wrapped = self._wrap(lambda v: received.append(v)) - wrapped("my_key") - self.assertEqual(received, [["my_key"]]) - - def test_wraps_int_key(self): - received = [] - wrapped = self._wrap(lambda v: received.append(v)) - wrapped(42) - self.assertEqual(received, [[42]]) - - def test_wraps_float_key(self): - received = [] - wrapped = self._wrap(lambda v: received.append(v)) - wrapped(3.14) - self.assertEqual(received, [[3.14]]) - - def test_wraps_bool_key(self): - received = [] - wrapped = self._wrap(lambda v: received.append(v)) - wrapped(True) - self.assertEqual(received, [[True]]) - - def test_passes_list_through(self): - received = [] - wrapped = self._wrap(lambda v: received.append(v)) - wrapped(["a", "b"]) - self.assertEqual(received, [["a", "b"]]) - - def test_passes_none_through(self): - received = [] - wrapped = self._wrap(lambda v: received.append(v)) - wrapped(None) - self.assertEqual(received, [None]) - - -class ComboBoxDeprecationTest(BaseTestCase): - def test_selected_key_warns(self): +class ComboBoxTest(BaseTestCase): + def test_renders_combo_box(self): from deephaven.ui import combo_box - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - combo_box(selected_key="a") - dep_warnings = [x for x in w if issubclass(x.category, FutureWarning)] - messages = [str(x.message) for x in dep_warnings] - self.assertTrue( - any("selected_key" in m and "selected_keys" in m for m in messages), - f"Expected selected_key deprecation warning, got: {messages}", - ) - - def test_default_selected_key_warns(self): - from deephaven.ui import combo_box - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - combo_box(default_selected_key="a") - dep_warnings = [x for x in w if issubclass(x.category, FutureWarning)] - messages = [str(x.message) for x in dep_warnings] - self.assertTrue( - any( - "default_selected_key" in m and "default_selected_keys" in m - for m in messages - ), - f"Expected default_selected_key deprecation warning, got: {messages}", - ) - - def test_no_warning_when_defaults(self): - from deephaven.ui import combo_box - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - combo_box() - dep_warnings = [ - x - for x in w - if issubclass(x.category, FutureWarning) - and ("selected_key" in str(x.message)) - ] - self.assertEqual( - len(dep_warnings), - 0, - f"Unexpected deprecation warning: {[str(x.message) for x in dep_warnings]}", - ) - - -class ComboBoxCallbackWrappingTest(BaseTestCase): - """Callbacks are wrapped to receive Selection when deprecated key props are NOT used.""" - - def _process(self, props, is_multiple): - from deephaven.ui.components.combo_box import _process_selection_props - - with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - _process_selection_props(props, is_multiple) - - def test_single_wraps_when_no_deprecated_props(self): - from deephaven.ui.types import Undefined - - received = [] - handler = lambda v: received.append(v) - props = { - "selected_key": Undefined, - "default_selected_key": None, - "selected_keys": ["a"], - "default_selected_keys": None, - "on_change": handler, - } - self._process(props, is_multiple=False) - # selected_keys converted to selected_key - self.assertEqual(props["selected_key"], "a") - # callback wrapped - props["on_change"]("my_key") - self.assertEqual(received, [["my_key"]]) - - def test_single_no_wrap_when_selected_key_used(self): - received = [] - handler = lambda v: received.append(v) - props = { - "selected_key": "a", - "default_selected_key": None, - "selected_keys": None, - "default_selected_keys": None, - "on_change": handler, - } - self._process(props, is_multiple=False) - props["on_change"]("my_key") - self.assertEqual(received, ["my_key"]) - - def test_single_no_wrap_when_default_selected_key_used(self): - from deephaven.ui.types import Undefined - - received = [] - handler = lambda v: received.append(v) - props = { - "selected_key": Undefined, - "default_selected_key": "b", - "selected_keys": None, - "default_selected_keys": None, - "on_change": handler, - } - self._process(props, is_multiple=False) - props["on_change"]("my_key") - self.assertEqual(received, ["my_key"]) - - def test_multiple_wraps_callbacks(self): - from deephaven.ui.types import Undefined - - received = [] - handler = lambda v: received.append(v) - props = { - "selected_key": Undefined, - "default_selected_key": None, - "selected_keys": ["x"], - "default_selected_keys": None, - "on_change": handler, - } - self._process(props, is_multiple=True) - # single props are stripped - self.assertNotIn("selected_key", props) - self.assertNotIn("default_selected_key", props) - # callback wrapped but lists pass through unchanged - props["on_change"](["a", "b"]) - self.assertEqual(received, [["a", "b"]]) - + result = combo_box(label="Test") + self.assertEqual(result.name, "deephaven.ui.components.ComboBox") -class ComboBoxSelectionModeTest(BaseTestCase): - def test_single_mode_renders_combo_box(self): + def test_selected_key(self): from deephaven.ui import combo_box - result = combo_box(label="Test") + result = combo_box(selected_key="a", label="Test") self.assertEqual(result.name, "deephaven.ui.components.ComboBox") - def test_multiple_mode_renders_multi_select(self): - from deephaven.ui import combo_box - result = combo_box(selection_mode="multiple", label="Test") +class MultiSelectTest(BaseTestCase): + def test_renders_multi_select(self): + from deephaven.ui import multi_select + + result = multi_select(label="Test") self.assertEqual(result.name, "deephaven.ui.components.MultiSelect") - def test_multiple_mode_accepts_selected_keys(self): - from deephaven.ui import combo_box + def test_accepts_selected_keys(self): + from deephaven.ui import multi_select - result = combo_box(selection_mode="multiple", selected_keys=["a", "b"]) + result = multi_select(selected_keys=["a", "b"], label="Test") self.assertEqual(result.name, "deephaven.ui.components.MultiSelect") From b662249d70d62ad34a64fdd680104594b8dee8da Mon Sep 17 00:00:00 2001 From: Joe Numainville Date: Fri, 22 May 2026 12:06:04 -0500 Subject: [PATCH 17/20] comments --- plugins/ui/docs/components/multi_select.md | 70 +++++++++---------- .../0761e766ac976e28061e143e034b23bf.json | 1 + .../09532531c03ea0d5591a4a03b09161a1.json | 1 - .../0cf5572d36b4c8bdf11d666c2afd41c5.json | 1 - .../16d416b14e0b60ca58840768ebb3377f.json | 1 + .../2747a4e6e572a96106716910e764ee1d.json | 1 - .../2de457dcb8fd02dae37ae4d35d28a023.json | 1 + .../388471963fdcbabcc15e590d509e0263.json | 1 + .../3e1db3f10151d0f7fddf9f4259d62447.json | 1 + .../415bf47be017adfd4bc2f5c6cba8a5fe.json | 1 + .../41b900591efc757b23440b1380150047.json | 1 + .../4d5923035666817d4436260b412c39bf.json | 1 + .../52d601a9bcb4a2a8f8be03c3992b8d63.json | 1 + .../560d89953fdb8d1b95ca2898778b9519.json | 1 + .../5d3ed3fecda2d0bd4ae4d8825d6fbea9.json | 1 + .../607ba14453021687f50ec3ebd1fd4c77.json | 1 + .../6b92d2a00b4d446f687c171939f4549c.json | 1 + .../6d5a83c0b867d4737036ba0c7df3d06a.json | 1 + .../87f46b40b82cb2efc19f5bdbab3ac2a3.json | 1 - .../8c0650bf02ecad7705b427d2ba3dcae9.json | 1 + ... => 8f831cd9142e5f4d10792383db92ffb8.json} | 2 +- .../94ab46014a1ec4241a826e7b08b163ef.json | 1 + .../9bf473151e912a23a38ae067fbcd5d60.json | 1 + .../b895d93116f94b87b7b2d8a4ced584ef.json | 1 + .../ba962573b1353e4b0230301ecf71f981.json | 1 + .../cef9fe521d17201b470cbf62cf48b92a.json | 1 + .../ec0ed3d95653ca3d2062db0a5ffcbc68.json | 1 + .../f1891462b585b31dabf2e7d96395c968.json | 1 + .../f51c43058b3f9e3badc91be0005f516b.json | 1 + .../fa4c431cba8e02a50fb02b9cbf6dd6de.json | 1 + .../fe8c8adb28d909d50fb4fef51e2ab1c9.json | 1 + .../ui/src/js/src/elements/MultiSelect.tsx | 5 +- 32 files changed, 62 insertions(+), 44 deletions(-) create mode 100644 plugins/ui/docs/snapshots/0761e766ac976e28061e143e034b23bf.json delete mode 100644 plugins/ui/docs/snapshots/09532531c03ea0d5591a4a03b09161a1.json delete mode 100644 plugins/ui/docs/snapshots/0cf5572d36b4c8bdf11d666c2afd41c5.json create mode 100644 plugins/ui/docs/snapshots/16d416b14e0b60ca58840768ebb3377f.json delete mode 100644 plugins/ui/docs/snapshots/2747a4e6e572a96106716910e764ee1d.json create mode 100644 plugins/ui/docs/snapshots/2de457dcb8fd02dae37ae4d35d28a023.json create mode 100644 plugins/ui/docs/snapshots/388471963fdcbabcc15e590d509e0263.json create mode 100644 plugins/ui/docs/snapshots/3e1db3f10151d0f7fddf9f4259d62447.json create mode 100644 plugins/ui/docs/snapshots/415bf47be017adfd4bc2f5c6cba8a5fe.json create mode 100644 plugins/ui/docs/snapshots/41b900591efc757b23440b1380150047.json create mode 100644 plugins/ui/docs/snapshots/4d5923035666817d4436260b412c39bf.json create mode 100644 plugins/ui/docs/snapshots/52d601a9bcb4a2a8f8be03c3992b8d63.json create mode 100644 plugins/ui/docs/snapshots/560d89953fdb8d1b95ca2898778b9519.json create mode 100644 plugins/ui/docs/snapshots/5d3ed3fecda2d0bd4ae4d8825d6fbea9.json create mode 100644 plugins/ui/docs/snapshots/607ba14453021687f50ec3ebd1fd4c77.json create mode 100644 plugins/ui/docs/snapshots/6b92d2a00b4d446f687c171939f4549c.json create mode 100644 plugins/ui/docs/snapshots/6d5a83c0b867d4737036ba0c7df3d06a.json delete mode 100644 plugins/ui/docs/snapshots/87f46b40b82cb2efc19f5bdbab3ac2a3.json create mode 100644 plugins/ui/docs/snapshots/8c0650bf02ecad7705b427d2ba3dcae9.json rename plugins/ui/docs/snapshots/{a3924153476e117f57e563dacb3b31d5.json => 8f831cd9142e5f4d10792383db92ffb8.json} (62%) create mode 100644 plugins/ui/docs/snapshots/94ab46014a1ec4241a826e7b08b163ef.json create mode 100644 plugins/ui/docs/snapshots/9bf473151e912a23a38ae067fbcd5d60.json create mode 100644 plugins/ui/docs/snapshots/b895d93116f94b87b7b2d8a4ced584ef.json create mode 100644 plugins/ui/docs/snapshots/ba962573b1353e4b0230301ecf71f981.json create mode 100644 plugins/ui/docs/snapshots/cef9fe521d17201b470cbf62cf48b92a.json create mode 100644 plugins/ui/docs/snapshots/ec0ed3d95653ca3d2062db0a5ffcbc68.json create mode 100644 plugins/ui/docs/snapshots/f1891462b585b31dabf2e7d96395c968.json create mode 100644 plugins/ui/docs/snapshots/f51c43058b3f9e3badc91be0005f516b.json create mode 100644 plugins/ui/docs/snapshots/fa4c431cba8e02a50fb02b9cbf6dd6de.json create mode 100644 plugins/ui/docs/snapshots/fe8c8adb28d909d50fb4fef51e2ab1c9.json diff --git a/plugins/ui/docs/components/multi_select.md b/plugins/ui/docs/components/multi_select.md index 6df6396d5..3be857332 100644 --- a/plugins/ui/docs/components/multi_select.md +++ b/plugins/ui/docs/components/multi_select.md @@ -124,27 +124,25 @@ from deephaven import ui @ui.component -def ui_multi_select_form_examples(): - return [ - ui.form( - ui.multi_select( - ui.item("Chocolate"), - ui.item("Mint"), - ui.item("Vanilla"), - ui.item("Strawberry"), - ui.item("Cookies and Cream"), - ui.item("Coffee"), - ui.item("Mango"), - label="Ice cream flavors", - name="flavors", - ), - ui.button("Submit", type="submit"), - on_submit=lambda event: print(event), - ) - ] +def ui_multi_select_form_example(): + return ui.form( + ui.multi_select( + ui.item("Chocolate"), + ui.item("Mint"), + ui.item("Vanilla"), + ui.item("Strawberry"), + ui.item("Cookies and Cream"), + ui.item("Coffee"), + ui.item("Mango"), + label="Ice cream flavors", + name="flavors", + ), + ui.button("Submit", type="submit"), + on_submit=lambda event: print(event), + ) -my_multi_select_form_examples = ui_multi_select_form_examples() +my_multi_select_form_example = ui_multi_select_form_example() ``` ## Labeling @@ -351,24 +349,22 @@ def ui_multi_select_events_example(): set_selection_state(new_value) print(f"Selection changed to {new_value}") - return [ - ui.multi_select( - ui.item("Option 1"), - ui.item("Option 2"), - ui.item("Option 3"), - ui.item("Option 4"), - ui.item("Option 5"), - ui.item("Option 6"), - ui.item("Option 7"), - ui.item("Option 8"), - ui.item("Option 9"), - input_value=input_value, - on_input_change=handle_input_change, - selected_keys=selection_state, - on_change=handle_selection_change, - label="Pick options", - ) - ] + return ui.multi_select( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + ui.item("Option 5"), + ui.item("Option 6"), + ui.item("Option 7"), + ui.item("Option 8"), + ui.item("Option 9"), + input_value=input_value, + on_input_change=handle_input_change, + selected_keys=selection_state, + on_change=handle_selection_change, + label="Pick options", + ) my_multi_select_events_example = ui_multi_select_events_example() diff --git a/plugins/ui/docs/snapshots/0761e766ac976e28061e143e034b23bf.json b/plugins/ui/docs/snapshots/0761e766ac976e28061e143e034b23bf.json new file mode 100644 index 000000000..ef787579e --- /dev/null +++ b/plugins/ui/docs/snapshots/0761e766ac976e28061e143e034b23bf.json @@ -0,0 +1 @@ +{"file":"components/combo_box.md","objects":{"my_combo_box_selected_key_examples":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_combo_box_selected_key_examples","props":{"children":[{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","defaultSelectedKey":"Option 2","validationBehavior":"aria","label":"Pick an option (uncontrolled)","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}},{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","selectedKey":"Option 1","validationBehavior":"aria","label":"Pick an option (controlled)","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}}]}},"state":"{\"state\": {\"0\": \"Option 1\"}}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/09532531c03ea0d5591a4a03b09161a1.json b/plugins/ui/docs/snapshots/09532531c03ea0d5591a4a03b09161a1.json deleted file mode 100644 index 6026f6711..000000000 --- a/plugins/ui/docs/snapshots/09532531c03ea0d5591a4a03b09161a1.json +++ /dev/null @@ -1 +0,0 @@ -{"file":"components/combo_box.md","objects":{"my_combo_box_selected_key_examples":{"type":"deephaven.ui.Element","data":{"document":{"props":{"children":[{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Pick an option (uncontrolled)","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}},{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Pick an option (controlled)","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}}]},"__dhElemName":"__main__.ui_combo_box_selected_key_examples"},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/0cf5572d36b4c8bdf11d666c2afd41c5.json b/plugins/ui/docs/snapshots/0cf5572d36b4c8bdf11d666c2afd41c5.json deleted file mode 100644 index f350b5108..000000000 --- a/plugins/ui/docs/snapshots/0cf5572d36b4c8bdf11d666c2afd41c5.json +++ /dev/null @@ -1 +0,0 @@ -{"file":"components/combo_box.md","objects":{"my_combo_box_control_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_combo_box_control_example","props":{"children":[{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","inputValue":"","validationBehavior":"aria","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"onInputChange":{"__dhCbid":"cb1"},"selectedKey":null,"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}}]}},"state":"{\"state\": {\"0\": \"\"}}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/16d416b14e0b60ca58840768ebb3377f.json b/plugins/ui/docs/snapshots/16d416b14e0b60ca58840768ebb3377f.json new file mode 100644 index 000000000..8b043ad0e --- /dev/null +++ b/plugins/ui/docs/snapshots/16d416b14e0b60ca58840768ebb3377f.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"my_multi_select_contextual_help_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Sample Label","labelPosition":"top","contextualHelp":{"__dhElemName":"deephaven.ui.components.ContextualHelp","props":{"heading":{"__dhElemName":"deephaven.ui.components.Heading","props":{"children":["Content tips"],"level":3}},"content":{"__dhElemName":"deephaven.ui.components.Content","props":{"children":["Tips for the content."]}},"variant":"help","placement":"bottom start"}},"children":{"__dhElemName":"deephaven.ui.components.Section","props":{"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}}]}}}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/2747a4e6e572a96106716910e764ee1d.json b/plugins/ui/docs/snapshots/2747a4e6e572a96106716910e764ee1d.json deleted file mode 100644 index d857abb1f..000000000 --- a/plugins/ui/docs/snapshots/2747a4e6e572a96106716910e764ee1d.json +++ /dev/null @@ -1 +0,0 @@ -{"file":"components/combo_box.md","objects":{"my_combo_box_basic":{"type":"deephaven.ui.Element","data":{"document":{"props":{"children":{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Favorite Animal","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"red panda"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"cat"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"dog"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"aardvark"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"kangaroo"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"snake"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"ant"}}]}}},"__dhElemName":"__main__.ui_combo_box_basic"},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/2de457dcb8fd02dae37ae4d35d28a023.json b/plugins/ui/docs/snapshots/2de457dcb8fd02dae37ae4d35d28a023.json new file mode 100644 index 000000000..0395334b8 --- /dev/null +++ b/plugins/ui/docs/snapshots/2de457dcb8fd02dae37ae4d35d28a023.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"column_types":{"type":"Table","data":{"columns":[{"name":"Key","type":"int"},{"name":"Label","type":"java.lang.String"},{"name":"Icon","type":"java.lang.String"}],"rows":[[{"value":"0"},{"value":"Display 0"},{"value":"vsAccount"}],[{"value":"1"},{"value":"Display 1"},{"value":"vsAccount"}],[{"value":"2"},{"value":"Display 2"},{"value":"vsAccount"}],[{"value":"3"},{"value":"Display 3"},{"value":"vsAccount"}],[{"value":"4"},{"value":"Display 4"},{"value":"vsAccount"}],[{"value":"5"},{"value":"Display 5"},{"value":"vsAccount"}],[{"value":"6"},{"value":"Display 6"},{"value":"vsAccount"}],[{"value":"7"},{"value":"Display 7"},{"value":"vsAccount"}],[{"value":"8"},{"value":"Display 8"},{"value":"vsAccount"}],[{"value":"9"},{"value":"Display 9"},{"value":"vsAccount"}],[{"value":"10"},{"value":"Display 10"},{"value":"vsAccount"}],[{"value":"11"},{"value":"Display 11"},{"value":"vsAccount"}],[{"value":"12"},{"value":"Display 12"},{"value":"vsAccount"}],[{"value":"13"},{"value":"Display 13"},{"value":"vsAccount"}],[{"value":"14"},{"value":"Display 14"},{"value":"vsAccount"}],[{"value":"15"},{"value":"Display 15"},{"value":"vsAccount"}],[{"value":"16"},{"value":"Display 16"},{"value":"vsAccount"}],[{"value":"17"},{"value":"Display 17"},{"value":"vsAccount"}],[{"value":"18"},{"value":"Display 18"},{"value":"vsAccount"}],[{"value":"19"},{"value":"Display 19"},{"value":"vsAccount"}]]}},"my_multi_select_item_table_source_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"User Multi Select","labelPosition":"top","labelColumn":"Label","iconColumn":"Icon","keyColumn":"Key","children":{"__dhObid":0}}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/388471963fdcbabcc15e590d509e0263.json b/plugins/ui/docs/snapshots/388471963fdcbabcc15e590d509e0263.json new file mode 100644 index 000000000..2d952a883 --- /dev/null +++ b/plugins/ui/docs/snapshots/388471963fdcbabcc15e590d509e0263.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"my_multi_select_complex_items_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Pick services","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"textValue":"Github","children":[{"__dhElemName":"deephaven.ui.icons.vsGithubAlt","props":{"name":"vsGithubAlt","slot":"icon"}},{"__dhElemName":"deephaven.ui.components.Text","props":{"children":["Github"],"slot":"text"}},{"__dhElemName":"deephaven.ui.components.Text","props":{"children":["Github Option"],"slot":"description"}}]}},{"__dhElemName":"deephaven.ui.components.Item","props":{"textValue":"Azure","children":[{"__dhElemName":"deephaven.ui.icons.vsAzureDevops","props":{"name":"vsAzureDevops","slot":"icon"}},{"__dhElemName":"deephaven.ui.components.Text","props":{"children":["Azure"],"slot":"text"}},{"__dhElemName":"deephaven.ui.components.Text","props":{"children":["Azure Option"],"slot":"description"}}]}}]}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/3e1db3f10151d0f7fddf9f4259d62447.json b/plugins/ui/docs/snapshots/3e1db3f10151d0f7fddf9f4259d62447.json new file mode 100644 index 000000000..f7a7998ec --- /dev/null +++ b/plugins/ui/docs/snapshots/3e1db3f10151d0f7fddf9f4259d62447.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"my_multi_select_selected_keys_examples":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_multi_select_selected_keys_examples","props":{"children":[{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","defaultSelectedKeys":["Option 2","Option 4"],"validationBehavior":"aria","label":"Pick options (uncontrolled)","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}},{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","selectedKeys":["Option 1","Option 3"],"validationBehavior":"aria","label":"Pick options (controlled)","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}}]}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/415bf47be017adfd4bc2f5c6cba8a5fe.json b/plugins/ui/docs/snapshots/415bf47be017adfd4bc2f5c6cba8a5fe.json new file mode 100644 index 000000000..e8d1af4ca --- /dev/null +++ b/plugins/ui/docs/snapshots/415bf47be017adfd4bc2f5c6cba8a5fe.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"my_multi_select_is_disabled_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","isDisabled":true,"validationBehavior":"aria","label":"Pick options","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/41b900591efc757b23440b1380150047.json b/plugins/ui/docs/snapshots/41b900591efc757b23440b1380150047.json new file mode 100644 index 000000000..4bd0a5d1a --- /dev/null +++ b/plugins/ui/docs/snapshots/41b900591efc757b23440b1380150047.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"my_multi_select_label_examples":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_multi_select_label_examples","props":{"children":[{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Pick options","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}}]}},{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","labelPosition":"top","aria-label":"Pick options","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}}]}}]}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/4d5923035666817d4436260b412c39bf.json b/plugins/ui/docs/snapshots/4d5923035666817d4436260b412c39bf.json new file mode 100644 index 000000000..b09d3cd00 --- /dev/null +++ b/plugins/ui/docs/snapshots/4d5923035666817d4436260b412c39bf.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"my_multi_select_label_position_examples":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_multi_select_label_position_examples","props":{"children":[{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Test Label","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}},{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Test Label","labelPosition":"side","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}}]}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/52d601a9bcb4a2a8f8be03c3992b8d63.json b/plugins/ui/docs/snapshots/52d601a9bcb4a2a8f8be03c3992b8d63.json new file mode 100644 index 000000000..df416ae26 --- /dev/null +++ b/plugins/ui/docs/snapshots/52d601a9bcb4a2a8f8be03c3992b8d63.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"my_multi_select_alignment_direction_examples":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_multi_select_alignment_direction_examples","props":{"children":{"__dhElemName":"deephaven.ui.components.View","props":{"children":[{"__dhElemName":"deephaven.ui.components.Flex","props":{"direction":"column","gap":"size-150","flex":"auto","children":[{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"menuWidth":"size-3000","formValue":"text","validationBehavior":"aria","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}}]}},{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"top","shouldFlip":true,"formValue":"text","validationBehavior":"aria","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}}]}}]}}],"padding":40}}}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/560d89953fdb8d1b95ca2898778b9519.json b/plugins/ui/docs/snapshots/560d89953fdb8d1b95ca2898778b9519.json new file mode 100644 index 000000000..06e22fe45 --- /dev/null +++ b/plugins/ui/docs/snapshots/560d89953fdb8d1b95ca2898778b9519.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"my_multi_select_help_text_examples":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_multi_select_help_text_examples","props":{"children":[{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Sample Label","description":"Select one or more options.","labelPosition":"top","children":{"__dhElemName":"deephaven.ui.components.Section","props":{"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 1","children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 2","children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 3","children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 4","children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 5","children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 6","children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 7","children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 8","children":"Option 8"}}]}}}},{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Sample Label","errorMessage":"Sample invalid error message.","validationState":"invalid","labelPosition":"top","children":{"__dhElemName":"deephaven.ui.components.Section","props":{"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 1","children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 2","children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 3","children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 4","children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 5","children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 6","children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 7","children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 8","children":"Option 8"}}]}}}}]}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/5d3ed3fecda2d0bd4ae4d8825d6fbea9.json b/plugins/ui/docs/snapshots/5d3ed3fecda2d0bd4ae4d8825d6fbea9.json new file mode 100644 index 000000000..abf8a23dc --- /dev/null +++ b/plugins/ui/docs/snapshots/5d3ed3fecda2d0bd4ae4d8825d6fbea9.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"my_multi_select_width_examples":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_multi_select_width_examples","props":{"children":[{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","labelPosition":"top","width":"size-3600","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 1","children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 2","children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 3","children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 4","children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 5","children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 6","children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 7","children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 8","children":"Option 8"}}]}},{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","labelPosition":"top","width":"size-3600","maxWidth":"100%","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 1","children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 2","children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 3","children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 4","children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 5","children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 6","children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 7","children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 8","children":"Option 8"}}]}}]}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/607ba14453021687f50ec3ebd1fd4c77.json b/plugins/ui/docs/snapshots/607ba14453021687f50ec3ebd1fd4c77.json new file mode 100644 index 000000000..fd56b06ce --- /dev/null +++ b/plugins/ui/docs/snapshots/607ba14453021687f50ec3ebd1fd4c77.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"countries":{"type":"Table","data":{"columns":[{"name":"Country","type":"java.lang.String"}],"rows":[[{"value":"Afghanistan"}],[{"value":"Albania"}],[{"value":"Algeria"}],[{"value":"Angola"}],[{"value":"Argentina"}],[{"value":"Australia"}],[{"value":"Austria"}],[{"value":"Bahrain"}],[{"value":"Bangladesh"}],[{"value":"Belgium"}],[{"value":"Benin"}],[{"value":"Bolivia"}],[{"value":"Bosnia and Herzegovina"}],[{"value":"Botswana"}],[{"value":"Brazil"}],[{"value":"Bulgaria"}],[{"value":"Burkina Faso"}],[{"value":"Burundi"}],[{"value":"Cambodia"}],[{"value":"Cameroon"}],[{"value":"Canada"}],[{"value":"Central African Republic"}],[{"value":"Chad"}],[{"value":"Chile"}],[{"value":"China"}],[{"value":"Colombia"}],[{"value":"Comoros"}],[{"value":"Congo, Dem. Rep."}],[{"value":"Congo, Rep."}],[{"value":"Costa Rica"}],[{"value":"Cote d'Ivoire"}],[{"value":"Croatia"}],[{"value":"Cuba"}],[{"value":"Czech Republic"}],[{"value":"Denmark"}],[{"value":"Djibouti"}],[{"value":"Dominican Republic"}],[{"value":"Ecuador"}],[{"value":"Egypt"}],[{"value":"El Salvador"}],[{"value":"Equatorial Guinea"}],[{"value":"Eritrea"}],[{"value":"Ethiopia"}],[{"value":"Finland"}],[{"value":"France"}],[{"value":"Gabon"}],[{"value":"Gambia"}],[{"value":"Germany"}],[{"value":"Ghana"}],[{"value":"Greece"}],[{"value":"Guatemala"}],[{"value":"Guinea"}],[{"value":"Guinea-Bissau"}],[{"value":"Haiti"}],[{"value":"Honduras"}],[{"value":"Hong Kong, China"}],[{"value":"Hungary"}],[{"value":"Iceland"}],[{"value":"India"}],[{"value":"Indonesia"}],[{"value":"Iran"}],[{"value":"Iraq"}],[{"value":"Ireland"}],[{"value":"Israel"}],[{"value":"Italy"}],[{"value":"Jamaica"}],[{"value":"Japan"}],[{"value":"Jordan"}],[{"value":"Kenya"}],[{"value":"Korea, Dem. Rep."}],[{"value":"Korea, Rep."}],[{"value":"Kuwait"}],[{"value":"Lebanon"}],[{"value":"Lesotho"}],[{"value":"Liberia"}],[{"value":"Libya"}],[{"value":"Madagascar"}],[{"value":"Malawi"}],[{"value":"Malaysia"}],[{"value":"Mali"}],[{"value":"Mauritania"}],[{"value":"Mauritius"}],[{"value":"Mexico"}],[{"value":"Mongolia"}],[{"value":"Montenegro"}],[{"value":"Morocco"}],[{"value":"Mozambique"}],[{"value":"Myanmar"}],[{"value":"Namibia"}],[{"value":"Nepal"}],[{"value":"Netherlands"}],[{"value":"New Zealand"}],[{"value":"Nicaragua"}],[{"value":"Niger"}],[{"value":"Nigeria"}],[{"value":"Norway"}],[{"value":"Oman"}],[{"value":"Pakistan"}],[{"value":"Panama"}],[{"value":"Paraguay"}]]}},"my_multi_select_table_source_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Sample Multi Select","labelPosition":"top","children":{"__dhObid":0}}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/6b92d2a00b4d446f687c171939f4549c.json b/plugins/ui/docs/snapshots/6b92d2a00b4d446f687c171939f4549c.json new file mode 100644 index 000000000..ebc28d478 --- /dev/null +++ b/plugins/ui/docs/snapshots/6b92d2a00b4d446f687c171939f4549c.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"my_multi_select_section_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Pick options","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Section","props":{"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}}]}},{"__dhElemName":"deephaven.ui.components.Section","props":{"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 10"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 11"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 12"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 13"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 14"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 15"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 16"}}]}}]}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/6d5a83c0b867d4737036ba0c7df3d06a.json b/plugins/ui/docs/snapshots/6d5a83c0b867d4737036ba0c7df3d06a.json new file mode 100644 index 000000000..462654c41 --- /dev/null +++ b/plugins/ui/docs/snapshots/6d5a83c0b867d4737036ba0c7df3d06a.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"my_multi_select_validation_behaviour_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_multi_select_validation_behaviour_example","props":{"children":{"__dhElemName":"deephaven.ui.components.Form","props":{"children":[{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","isRequired":true,"validationBehavior":"aria","label":"Pick options","labelPosition":"top","children":{"__dhElemName":"deephaven.ui.components.Section","props":{"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}}]}}}}],"validationBehavior":"aria","labelPosition":"top"}}}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/87f46b40b82cb2efc19f5bdbab3ac2a3.json b/plugins/ui/docs/snapshots/87f46b40b82cb2efc19f5bdbab3ac2a3.json deleted file mode 100644 index b409f9fe6..000000000 --- a/plugins/ui/docs/snapshots/87f46b40b82cb2efc19f5bdbab3ac2a3.json +++ /dev/null @@ -1 +0,0 @@ -{"file":"components/combo_box.md","objects":{"my_combo_box_multi_select_example":{"type":"deephaven.ui.Element","data":{"document":{"props":{"children":{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","selectedKeys":[],"validationBehavior":"aria","label":"Pick options","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}}},"__dhElemName":"__main__.ui_combo_box_multi_select_example"},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/8c0650bf02ecad7705b427d2ba3dcae9.json b/plugins/ui/docs/snapshots/8c0650bf02ecad7705b427d2ba3dcae9.json new file mode 100644 index 000000000..83a6df562 --- /dev/null +++ b/plugins/ui/docs/snapshots/8c0650bf02ecad7705b427d2ba3dcae9.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"my_multi_select_events_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_multi_select_events_example","props":{"children":{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","inputValue":"","selectedKeys":[],"validationBehavior":"aria","label":"Pick options","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"onInputChange":{"__dhCbid":"cb1"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}}}},"state":"{\"state\": {\"0\": \"\"}}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/a3924153476e117f57e563dacb3b31d5.json b/plugins/ui/docs/snapshots/8f831cd9142e5f4d10792383db92ffb8.json similarity index 62% rename from plugins/ui/docs/snapshots/a3924153476e117f57e563dacb3b31d5.json rename to plugins/ui/docs/snapshots/8f831cd9142e5f4d10792383db92ffb8.json index c58bff1d2..fec855f76 100644 --- a/plugins/ui/docs/snapshots/a3924153476e117f57e563dacb3b31d5.json +++ b/plugins/ui/docs/snapshots/8f831cd9142e5f4d10792383db92ffb8.json @@ -1 +1 @@ -{"file":"components/combo_box.md","objects":{"my_combo_box_is_read_only_example":{"type":"deephaven.ui.Element","data":{"document":{"props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","isReadOnly":true,"validationBehavior":"aria","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 1","children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 2","children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 3","children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 4","children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 5","children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 6","children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 7","children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 8","children":"Option 8"}}]},"__dhElemName":"deephaven.ui.components.ComboBox"},"state":"{}"}}}} \ No newline at end of file +{"file":"components/multi_select.md","objects":{"my_multi_select_is_read_only_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","defaultSelectedKeys":["Option 1","Option 3"],"isReadOnly":true,"validationBehavior":"aria","label":"Pick options","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 1","children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 2","children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 3","children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 4","children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 5","children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 6","children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 7","children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 8","children":"Option 8"}}]}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/94ab46014a1ec4241a826e7b08b163ef.json b/plugins/ui/docs/snapshots/94ab46014a1ec4241a826e7b08b163ef.json new file mode 100644 index 000000000..243247a7b --- /dev/null +++ b/plugins/ui/docs/snapshots/94ab46014a1ec4241a826e7b08b163ef.json @@ -0,0 +1 @@ +{"file":"components/combo_box.md","objects":{"my_combo_box_is_read_only_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","defaultSelectedKey":"Option 1","isReadOnly":true,"validationBehavior":"aria","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 1","children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 2","children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 3","children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 4","children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 5","children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 6","children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 7","children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"key":"Option 8","children":"Option 8"}}]}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/9bf473151e912a23a38ae067fbcd5d60.json b/plugins/ui/docs/snapshots/9bf473151e912a23a38ae067fbcd5d60.json new file mode 100644 index 000000000..88b320ef3 --- /dev/null +++ b/plugins/ui/docs/snapshots/9bf473151e912a23a38ae067fbcd5d60.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"my_multi_select_basic":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_multi_select_basic","props":{"children":{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","selectedKeys":[],"validationBehavior":"aria","label":"Favorite Animals","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"red panda"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"cat"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"dog"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"aardvark"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"kangaroo"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"snake"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"ant"}}]}}}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/b895d93116f94b87b7b2d8a4ced584ef.json b/plugins/ui/docs/snapshots/b895d93116f94b87b7b2d8a4ced584ef.json new file mode 100644 index 000000000..2c47abe8c --- /dev/null +++ b/plugins/ui/docs/snapshots/b895d93116f94b87b7b2d8a4ced584ef.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"my_multi_select_required_examples":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_multi_select_required_examples","props":{"children":[{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","isRequired":true,"validationBehavior":"aria","label":"Pick options","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}}]}},{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","isRequired":true,"validationBehavior":"aria","label":"Pick options","labelPosition":"top","necessityIndicator":"label","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}}]}},{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Pick options","labelPosition":"top","necessityIndicator":"label","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}}]}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/ba962573b1353e4b0230301ecf71f981.json b/plugins/ui/docs/snapshots/ba962573b1353e4b0230301ecf71f981.json new file mode 100644 index 000000000..662385143 --- /dev/null +++ b/plugins/ui/docs/snapshots/ba962573b1353e4b0230301ecf71f981.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"my_multi_select_custom_value_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_multi_select_custom_value_example","props":{"children":{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","allowsCustomValue":true,"selectedKeys":[],"validationBehavior":"aria","label":"Select or type options","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}}]}}}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/cef9fe521d17201b470cbf62cf48b92a.json b/plugins/ui/docs/snapshots/cef9fe521d17201b470cbf62cf48b92a.json new file mode 100644 index 000000000..cb3f90984 --- /dev/null +++ b/plugins/ui/docs/snapshots/cef9fe521d17201b470cbf62cf48b92a.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"my_multi_select_form_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_multi_select_form_example","props":{"children":{"__dhElemName":"deephaven.ui.components.Form","props":{"children":[{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Ice cream flavors","name":"flavors","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Chocolate"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Mint"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Vanilla"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Strawberry"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Cookies and Cream"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Coffee"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Mango"}}]}},{"__dhElemName":"deephaven.ui.components.Button","props":{"variant":"accent","style":"fill","type":"submit","children":"Submit"}}],"validationBehavior":"aria","labelPosition":"top","onSubmit":{"__dhCbid":"cb0"}}}}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/ec0ed3d95653ca3d2062db0a5ffcbc68.json b/plugins/ui/docs/snapshots/ec0ed3d95653ca3d2062db0a5ffcbc68.json new file mode 100644 index 000000000..bffbf7716 --- /dev/null +++ b/plugins/ui/docs/snapshots/ec0ed3d95653ca3d2062db0a5ffcbc68.json @@ -0,0 +1 @@ +{"file":"components/combo_box.md","objects":{"my_combo_box_multi_select_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_combo_box_multi_select_example","props":{"children":[{"__dhElemName":"deephaven.ui.components.Flex","props":{"alignItems":"start","gap":"size-100","flex":"auto","children":{"__dhElemName":"deephaven.ui.components.Flex","props":{"direction":"row","alignItems":"center","gap":"size-100","flex":"auto","children":[{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","inputValue":"","selectedKey":"","validationBehavior":"aria","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"onInputChange":{"__dhCbid":"cb1"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}},{"__dhElemName":"deephaven.ui.components.TagGroup","props":{"labelPosition":"top","labelAlign":"start","onRemove":{"__dhCbid":"cb2"}}}]}}}}]}},"state":"{\"state\": {\"0\": \"\", \"1\": \"\"}}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/f1891462b585b31dabf2e7d96395c968.json b/plugins/ui/docs/snapshots/f1891462b585b31dabf2e7d96395c968.json new file mode 100644 index 000000000..b9d3df24e --- /dev/null +++ b/plugins/ui/docs/snapshots/f1891462b585b31dabf2e7d96395c968.json @@ -0,0 +1 @@ +{"file":"components/combo_box.md","objects":{"my_combo_box_control_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_combo_box_control_example","props":{"children":[{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","inputValue":"","selectedKey":"","validationBehavior":"aria","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"onInputChange":{"__dhCbid":"cb1"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}}]}},"state":"{\"state\": {\"0\": \"\", \"1\": \"\"}}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/f51c43058b3f9e3badc91be0005f516b.json b/plugins/ui/docs/snapshots/f51c43058b3f9e3badc91be0005f516b.json new file mode 100644 index 000000000..cb697b537 --- /dev/null +++ b/plugins/ui/docs/snapshots/f51c43058b3f9e3badc91be0005f516b.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"my_multi_select_trigger_option_examples":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_multi_select_trigger_option_examples","props":{"children":[{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Select Options","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}},{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"focus","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Select Options","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}},{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"manual","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Select Options","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}}]}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/fa4c431cba8e02a50fb02b9cbf6dd6de.json b/plugins/ui/docs/snapshots/fa4c431cba8e02a50fb02b9cbf6dd6de.json new file mode 100644 index 000000000..f640bfcb1 --- /dev/null +++ b/plugins/ui/docs/snapshots/fa4c431cba8e02a50fb02b9cbf6dd6de.json @@ -0,0 +1 @@ +{"file":"components/combo_box.md","objects":{"my_combo_box_basic":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_combo_box_basic","props":{"children":{"__dhElemName":"deephaven.ui.components.ComboBox","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","selectedKey":"","validationBehavior":"aria","label":"Favorite Animal","labelPosition":"top","onChange":{"__dhCbid":"cb0"},"children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"red panda"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"cat"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"dog"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"aardvark"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"kangaroo"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"snake"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"ant"}}]}}}},"state":"{\"state\": {\"0\": \"\"}}"}}}} \ No newline at end of file diff --git a/plugins/ui/docs/snapshots/fe8c8adb28d909d50fb4fef51e2ab1c9.json b/plugins/ui/docs/snapshots/fe8c8adb28d909d50fb4fef51e2ab1c9.json new file mode 100644 index 000000000..af3e3b6bd --- /dev/null +++ b/plugins/ui/docs/snapshots/fe8c8adb28d909d50fb4fef51e2ab1c9.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"my_multi_select_is_quiet_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","isQuiet":true,"align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Pick options","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 1"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 2"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 3"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 4"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 5"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 6"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 7"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 8"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Option 9"}}]}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/src/js/src/elements/MultiSelect.tsx b/plugins/ui/src/js/src/elements/MultiSelect.tsx index ee8cdf2b1..b192d6781 100644 --- a/plugins/ui/src/js/src/elements/MultiSelect.tsx +++ b/plugins/ui/src/js/src/elements/MultiSelect.tsx @@ -5,6 +5,7 @@ import { isElementOfType } from '@deephaven/react-hooks'; import type { dh } from '@deephaven/jsapi-types'; import { ApiContext } from '@deephaven/jsapi-bootstrap'; import { getSettings, type RootState } from '@deephaven/redux'; +import { EMPTY_ARRAY } from '@deephaven/utils'; import { type SerializedMultiSelectProps, useMultiSelectProps, @@ -35,7 +36,7 @@ export function MultiSelect( errorMessage={message} validationState="invalid" > - {[]} + {EMPTY_ARRAY} ); } @@ -46,7 +47,7 @@ export function MultiSelect( return ( // eslint-disable-next-line react/jsx-props-no-spreading - {[]} + {EMPTY_ARRAY} ); } From 90dc770bae6ee33c6dbd79641857a4a465c030fb Mon Sep 17 00:00:00 2001 From: Joe Numainville Date: Fri, 22 May 2026 14:43:06 -0500 Subject: [PATCH 18/20] work children --- plugins/ui/src/js/src/elements/ComboBox.tsx | 8 ++------ plugins/ui/src/js/src/elements/ListView.tsx | 8 ++------ plugins/ui/src/js/src/elements/MultiSelect.tsx | 9 ++------- plugins/ui/src/js/src/elements/Picker.tsx | 8 ++------ 4 files changed, 8 insertions(+), 25 deletions(-) diff --git a/plugins/ui/src/js/src/elements/ComboBox.tsx b/plugins/ui/src/js/src/elements/ComboBox.tsx index a322f6fa0..0db613082 100644 --- a/plugins/ui/src/js/src/elements/ComboBox.tsx +++ b/plugins/ui/src/js/src/elements/ComboBox.tsx @@ -48,17 +48,13 @@ export function ComboBox( {...pickerProps} errorMessage={message} validationState="invalid" - > - {[]} - + /> ); } if (isLoading || table == null || api == null) { return ( // eslint-disable-next-line react/jsx-props-no-spreading - - {[]} - + ); } return ( diff --git a/plugins/ui/src/js/src/elements/ListView.tsx b/plugins/ui/src/js/src/elements/ListView.tsx index 44741c92f..68e39f471 100644 --- a/plugins/ui/src/js/src/elements/ListView.tsx +++ b/plugins/ui/src/js/src/elements/ListView.tsx @@ -38,9 +38,7 @@ export function ListView(props: SerializedListViewProps): JSX.Element | null { // eslint-disable-next-line react/jsx-props-no-spreading {...listViewProps} renderEmptyState={() => } - > - {[]} - + /> ); } if (isLoading || table == null || api == null) { @@ -49,9 +47,7 @@ export function ListView(props: SerializedListViewProps): JSX.Element | null { // eslint-disable-next-line react/jsx-props-no-spreading {...listViewProps} loadingState="loading" - > - {[]} - + /> ); } return ( diff --git a/plugins/ui/src/js/src/elements/MultiSelect.tsx b/plugins/ui/src/js/src/elements/MultiSelect.tsx index b192d6781..b89bdec42 100644 --- a/plugins/ui/src/js/src/elements/MultiSelect.tsx +++ b/plugins/ui/src/js/src/elements/MultiSelect.tsx @@ -5,7 +5,6 @@ import { isElementOfType } from '@deephaven/react-hooks'; import type { dh } from '@deephaven/jsapi-types'; import { ApiContext } from '@deephaven/jsapi-bootstrap'; import { getSettings, type RootState } from '@deephaven/redux'; -import { EMPTY_ARRAY } from '@deephaven/utils'; import { type SerializedMultiSelectProps, useMultiSelectProps, @@ -35,9 +34,7 @@ export function MultiSelect( {...multiSelectProps} errorMessage={message} validationState="invalid" - > - {EMPTY_ARRAY} - + /> ); } // Don't gate on `isLoading` as it flips true on server round-trips and @@ -46,9 +43,7 @@ export function MultiSelect( if (table == null || api == null) { return ( // eslint-disable-next-line react/jsx-props-no-spreading - - {EMPTY_ARRAY} - + ); } return ( diff --git a/plugins/ui/src/js/src/elements/Picker.tsx b/plugins/ui/src/js/src/elements/Picker.tsx index 59e5ce501..9998c07d0 100644 --- a/plugins/ui/src/js/src/elements/Picker.tsx +++ b/plugins/ui/src/js/src/elements/Picker.tsx @@ -44,17 +44,13 @@ export function Picker( const message = getErrorShortMessage(error); return ( // eslint-disable-next-line react/jsx-props-no-spreading - - {[]} - + ); } if (isLoading || table == null || api == null) { return ( // eslint-disable-next-line react/jsx-props-no-spreading - - {[]} - + ); } return ( From 00a586190fbfcf43ad77aca0754bed3adacbbec1 Mon Sep 17 00:00:00 2001 From: Joe Numainville Date: Fri, 22 May 2026 16:45:49 -0500 Subject: [PATCH 19/20] update and e2e --- .../cef9fe521d17201b470cbf62cf48b92a.json | 1 - plugins/ui/src/js/package.json | 30 ++++++------- tests/app.d/tests.app | 2 + tests/app.d/ui_combo_box.py | 38 +++++++++++++++++ tests/app.d/ui_multi_select.py | 40 ++++++++++++++++++ tests/ui_combo_box.spec.ts | 39 +++++++++++++++++ ...nders-basic-combo-box-1-chromium-linux.png | Bin 0 -> 5489 bytes ...enders-basic-combo-box-1-firefox-linux.png | Bin 0 -> 12784 bytes ...renders-basic-combo-box-1-webkit-linux.png | Bin 0 -> 5410 bytes tests/ui_multi_select.spec.ts | 22 ++++++++++ ...rs-basic-multi-select-1-chromium-linux.png | Bin 0 -> 19217 bytes ...ers-basic-multi-select-1-firefox-linux.png | Bin 0 -> 36018 bytes ...ders-basic-multi-select-1-webkit-linux.png | Bin 0 -> 19368 bytes ...t-with-initial-values-1-chromium-linux.png | Bin 0 -> 19217 bytes ...ct-with-initial-values-1-firefox-linux.png | Bin 0 -> 36018 bytes ...ect-with-initial-values-1-webkit-linux.png | Bin 0 -> 19368 bytes 16 files changed, 156 insertions(+), 16 deletions(-) delete mode 100644 plugins/ui/docs/snapshots/cef9fe521d17201b470cbf62cf48b92a.json create mode 100644 tests/app.d/ui_combo_box.py create mode 100644 tests/app.d/ui_multi_select.py create mode 100644 tests/ui_combo_box.spec.ts create mode 100644 tests/ui_combo_box.spec.ts-snapshots/UI-combo-box-renders-basic-combo-box-1-chromium-linux.png create mode 100644 tests/ui_combo_box.spec.ts-snapshots/UI-combo-box-renders-basic-combo-box-1-firefox-linux.png create mode 100644 tests/ui_combo_box.spec.ts-snapshots/UI-combo-box-renders-basic-combo-box-1-webkit-linux.png create mode 100644 tests/ui_multi_select.spec.ts create mode 100644 tests/ui_multi_select.spec.ts-snapshots/UI-multi-select-renders-basic-multi-select-1-chromium-linux.png create mode 100644 tests/ui_multi_select.spec.ts-snapshots/UI-multi-select-renders-basic-multi-select-1-firefox-linux.png create mode 100644 tests/ui_multi_select.spec.ts-snapshots/UI-multi-select-renders-basic-multi-select-1-webkit-linux.png create mode 100644 tests/ui_multi_select.spec.ts-snapshots/UI-multi-select-renders-controlled-multi-select-with-initial-values-1-chromium-linux.png create mode 100644 tests/ui_multi_select.spec.ts-snapshots/UI-multi-select-renders-controlled-multi-select-with-initial-values-1-firefox-linux.png create mode 100644 tests/ui_multi_select.spec.ts-snapshots/UI-multi-select-renders-controlled-multi-select-with-initial-values-1-webkit-linux.png diff --git a/plugins/ui/docs/snapshots/cef9fe521d17201b470cbf62cf48b92a.json b/plugins/ui/docs/snapshots/cef9fe521d17201b470cbf62cf48b92a.json deleted file mode 100644 index cb3f90984..000000000 --- a/plugins/ui/docs/snapshots/cef9fe521d17201b470cbf62cf48b92a.json +++ /dev/null @@ -1 +0,0 @@ -{"file":"components/multi_select.md","objects":{"my_multi_select_form_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_multi_select_form_example","props":{"children":{"__dhElemName":"deephaven.ui.components.Form","props":{"children":[{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Ice cream flavors","name":"flavors","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Chocolate"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Mint"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Vanilla"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Strawberry"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Cookies and Cream"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Coffee"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Mango"}}]}},{"__dhElemName":"deephaven.ui.components.Button","props":{"variant":"accent","style":"fill","type":"submit","children":"Submit"}}],"validationBehavior":"aria","labelPosition":"top","onSubmit":{"__dhCbid":"cb0"}}}}},"state":"{}"}}}} \ No newline at end of file diff --git a/plugins/ui/src/js/package.json b/plugins/ui/src/js/package.json index 84ed37a73..dc217b2b0 100644 --- a/plugins/ui/src/js/package.json +++ b/plugins/ui/src/js/package.json @@ -39,25 +39,25 @@ "react-dom": "^18.0.0 || ^19.0.0" }, "dependencies": { - "@deephaven/chart": "^1.17.0", - "@deephaven/components": "^1.17.0", - "@deephaven/console": "^1.17.0", - "@deephaven/dashboard": "^1.17.1", - "@deephaven/dashboard-core-plugins": "^1.18.0", - "@deephaven/golden-layout": "^1.17.1", - "@deephaven/grid": "^1.18.0", + "@deephaven/chart": "^1.21.0", + "@deephaven/components": "^1.21.0", + "@deephaven/console": "^1.21.0", + "@deephaven/dashboard": "^1.21.0", + "@deephaven/dashboard-core-plugins": "^1.21.0", + "@deephaven/golden-layout": "^1.21.0", + "@deephaven/grid": "^1.21.0", "@deephaven/icons": "^1.2.0", - "@deephaven/iris-grid": "^1.18.0", - "@deephaven/jsapi-bootstrap": "^1.17.0", - "@deephaven/jsapi-components": "^1.17.0", + "@deephaven/iris-grid": "^1.21.0", + "@deephaven/jsapi-bootstrap": "^1.21.0", + "@deephaven/jsapi-components": "^1.21.0", "@deephaven/jsapi-types": "^1.0.0-dev0.39.6", - "@deephaven/jsapi-utils": "^1.16.0", + "@deephaven/jsapi-utils": "^1.21.0", "@deephaven/log": "^1.8.0", - "@deephaven/plugin": "^1.18.0", - "@deephaven/react-hooks": "^1.14.0", - "@deephaven/redux": "^1.17.0", + "@deephaven/plugin": "^1.21.0", + "@deephaven/react-hooks": "^1.21.0", + "@deephaven/redux": "^1.21.0", "@deephaven/test-utils": "^1.8.0", - "@deephaven/utils": "^1.10.0", + "@deephaven/utils": "^1.21.0", "@fortawesome/react-fontawesome": "^0.2.0", "@internationalized/date": "^3.5.5", "classnames": "^2.5.1", diff --git a/tests/app.d/tests.app b/tests/app.d/tests.app index 31e450cea..add9d8ec2 100644 --- a/tests/app.d/tests.app +++ b/tests/app.d/tests.app @@ -18,4 +18,6 @@ file_11=ag_grid.py file_12=theme_demo.py file_13=ui_nested_dashboard.py file_14=ui_query_params.py +file_15=ui_combo_box.py +file_16=ui_multi_select.py diff --git a/tests/app.d/ui_combo_box.py b/tests/app.d/ui_combo_box.py new file mode 100644 index 000000000..3e583c048 --- /dev/null +++ b/tests/app.d/ui_combo_box.py @@ -0,0 +1,38 @@ +from deephaven import ui + + +@ui.component +def ui_combo_box_basic(): + value, set_value = ui.use_state(None) + return ui.flex( + ui.combo_box( + "Option A", + "Option B", + "Option C", + label="Select an option", + on_change=set_value, + ), + ui.text(f"Selected: {value}"), + direction="column", + ) + + +@ui.component +def ui_combo_box_controlled(): + value, set_value = ui.use_state("Option B") + return ui.flex( + ui.combo_box( + "Option A", + "Option B", + "Option C", + label="Controlled", + selected_key=value, + on_change=set_value, + ), + ui.text(f"Selected: {value}"), + direction="column", + ) + + +cb_basic = ui_combo_box_basic() +cb_controlled = ui_combo_box_controlled() diff --git a/tests/app.d/ui_multi_select.py b/tests/app.d/ui_multi_select.py new file mode 100644 index 000000000..70f883619 --- /dev/null +++ b/tests/app.d/ui_multi_select.py @@ -0,0 +1,40 @@ +from deephaven import ui + + +@ui.component +def ui_multi_select_basic(): + values, set_values = ui.use_state(None) + return ui.flex( + ui.multi_select( + "Option A", + "Option B", + "Option C", + "Option D", + label="Select options", + on_change=set_values, + ), + ui.text(f"Count: {len(values) if values else 0}"), + direction="column", + ) + + +@ui.component +def ui_multi_select_controlled(): + values, set_values = ui.use_state(["Option A", "Option C"]) + return ui.flex( + ui.multi_select( + "Option A", + "Option B", + "Option C", + "Option D", + label="Controlled", + selected_keys=values, + on_change=set_values, + ), + ui.text(f"Count: {len(values) if values else 0}"), + direction="column", + ) + + +ms_basic = ui_multi_select_basic() +ms_controlled = ui_multi_select_controlled() diff --git a/tests/ui_combo_box.spec.ts b/tests/ui_combo_box.spec.ts new file mode 100644 index 000000000..852beeef9 --- /dev/null +++ b/tests/ui_combo_box.spec.ts @@ -0,0 +1,39 @@ +import { expect, test } from '@playwright/test'; +import { openPanel, gotoPage, SELECTORS } from './utils'; + +test.describe('UI combo_box', () => { + test('renders basic combo box', async ({ page }) => { + await gotoPage(page, ''); + await openPanel(page, 'cb_basic', SELECTORS.REACT_PANEL_VISIBLE); + + const panel = page.locator(SELECTORS.REACT_PANEL_VISIBLE); + await expect(panel.getByText('Selected: None')).toBeVisible(); + await expect(panel).toHaveScreenshot(); + }); + + test('renders controlled combo box with initial value', async ({ page }) => { + await gotoPage(page, ''); + await openPanel(page, 'cb_controlled', SELECTORS.REACT_PANEL_VISIBLE); + + const panel = page.locator(SELECTORS.REACT_PANEL_VISIBLE); + await expect(panel.getByText('Selected: Option B')).toBeVisible(); + }); + + test('selects an option', async ({ page }) => { + await gotoPage(page, ''); + await openPanel(page, 'cb_basic', SELECTORS.REACT_PANEL_VISIBLE); + + const panel = page.locator(SELECTORS.REACT_PANEL_VISIBLE); + + // Click the combo box trigger button to open the dropdown + await panel.getByRole('button', { name: 'Show suggestions' }).click(); + + // Wait for the listbox to appear and select an option + const listbox = page.getByRole('listbox'); + await expect(listbox).toBeVisible(); + await listbox.getByRole('option', { name: 'Option A' }).click(); + + // Verify the selection was applied + await expect(panel.getByText('Selected: Option A')).toBeVisible(); + }); +}); diff --git a/tests/ui_combo_box.spec.ts-snapshots/UI-combo-box-renders-basic-combo-box-1-chromium-linux.png b/tests/ui_combo_box.spec.ts-snapshots/UI-combo-box-renders-basic-combo-box-1-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..250ab645e53f90979c6299ccf6d2db5c35147d4f GIT binary patch literal 5489 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 Y(SS#%#WWR%0q}Ft%=$d$oae9q1!@bAdjJ3c literal 0 HcmV?d00001 diff --git a/tests/ui_combo_box.spec.ts-snapshots/UI-combo-box-renders-basic-combo-box-1-webkit-linux.png b/tests/ui_combo_box.spec.ts-snapshots/UI-combo-box-renders-basic-combo-box-1-webkit-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..55f8503a72e03f3fdcab1e11afceeb6957f1be38 GIT binary patch literal 5410 zcmeHLX;4$ywvJ+pia`5l#FjR;7X*<>L;)Ecn#LF)ji6Bi2?~PDA_-GSf_hu3Q9y%$ zjG;w9Kn59QNP^g)3}KXnKp-NLKp+GHB!ML49lh_zt$Ni}eczw=r|VRmy{q=#YhLSH zd;j)}tCQ-k{kuRQkSgT-PZvR;Z-D8RQ#-Z-!|B%jbYR$i9qRNGNb%(>AeW?pK;JPS zKb`T2$>mSP#$1}oYg^X19jWopM@OD-+xm~|CC0nFHUF>=yW-*T;@Q3Qpiusi>-;*M zkN0oZn-t!hf88)#T2cSNyP{$wDg9DegT-q%*0+9Ucb@4UI!(leb5gGa`G(!MegdNQ zbEl^6L{5rZ1yfTbLF8hS!9ELg%on*@TEe`^TY$;vo^8i~;m{5r;D7epHsJry9}|F; z9cMsFAkepWLEAx~AGdAU4d}9C>pl=D;oJXxpc=PTQtMtqsH?l#;uWMkORsoWWa;G) zK}C9#_W%^5ay|69_kV~Q`lC>NVO*zvvA#%M{@IBTUtz|6zo~Q-b>xU7aWB{+?5rS` znvYkdejj60oEp&mI=e;pYajxiGt<`IAdMZ;Fa(mXy$&|G6zPmVtvgI&N&1zoOm8O@X z9dUZ3j28gQi=A=m7QgtxjQ26Mq0d{wqdUIK!%WVVxLQp~%+8f3<$KC5nm~xUS@CaQ zf^(8OVOFA97|QW~cQY zR`eQS!(u0z;K((Rrat>vxe&I3WaZK<$!VH^hekDLaLO?&DDX>TS=)#HlySnX4tC1k z@A6Nr4HIIQwB1c#O9m|`ZEk$R^Gh&R1qI{Z5BG)OJ|zxQ(1Qd;&~mqNlZ`*pf=Zd2 z)2?j(H0H#%mzMsj?uBJ`44X8)icu80&M|!Fm(L}>q^`{?PR`H64&3hkgNmyBN-1t3 zKM<8K;yY>Uc`vO$qvHPy8`G~9PM)C$S4wAI>_qK*WU>&`Asb$)+yOYD(jXkf%?=LF zjh!A|BX^Q;QyBB&_~°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 I&-&c`ZzN-!yZ`_I literal 0 HcmV?d00001 diff --git a/tests/ui_multi_select.spec.ts b/tests/ui_multi_select.spec.ts new file mode 100644 index 000000000..dd75c6252 --- /dev/null +++ b/tests/ui_multi_select.spec.ts @@ -0,0 +1,22 @@ +import { expect, test } from '@playwright/test'; +import { openPanel, gotoPage, SELECTORS } from './utils'; + +test.describe('UI multi_select', () => { + test('renders basic multi select', async ({ page }) => { + await gotoPage(page, ''); + await openPanel(page, 'ms_basic', SELECTORS.REACT_PANEL_VISIBLE); + + const panel = page.locator(SELECTORS.REACT_PANEL_VISIBLE); + await expect(panel).toHaveScreenshot(); + }); + + test('renders controlled multi select with initial values', async ({ + page, + }) => { + await gotoPage(page, ''); + await openPanel(page, 'ms_controlled', SELECTORS.REACT_PANEL_VISIBLE); + + const panel = page.locator(SELECTORS.REACT_PANEL_VISIBLE); + await expect(panel).toHaveScreenshot(); + }); +}); diff --git a/tests/ui_multi_select.spec.ts-snapshots/UI-multi-select-renders-basic-multi-select-1-chromium-linux.png b/tests/ui_multi_select.spec.ts-snapshots/UI-multi-select-renders-basic-multi-select-1-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..247b14e679c1af1c9abd0999c11cab0ca86de45e GIT binary patch literal 19217 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�k?xTI(M5F%kI0`>*=lca4f${n{ko0j5Q%4@+Z!e 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�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�k?xTI(M5F%kI0`>*=lca4f${n{ko0j5Q%4@+Z!e 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�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 Date: Fri, 22 May 2026 17:35:48 -0500 Subject: [PATCH 20/20] docs updates --- plugins/ui/docs/sidebar.json | 6 +++++- .../ui/docs/snapshots/d90e47400a6a9800a0dcf18d62cfa277.json | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 plugins/ui/docs/snapshots/d90e47400a6a9800a0dcf18d62cfa277.json diff --git a/plugins/ui/docs/sidebar.json b/plugins/ui/docs/sidebar.json index 41a688a24..2d7c4e19a 100644 --- a/plugins/ui/docs/sidebar.json +++ b/plugins/ui/docs/sidebar.json @@ -326,6 +326,10 @@ "label": "meter", "path": "components/meter.md" }, + { + "label": "multi_select", + "path": "components/multi_select.md" + }, { "label": "number_field", "path": "components/number_field.md" @@ -359,7 +363,7 @@ "path": "components/range_slider.md" }, { - "label": "route", + "label": "router", "path": "components/router.md" }, { diff --git a/plugins/ui/docs/snapshots/d90e47400a6a9800a0dcf18d62cfa277.json b/plugins/ui/docs/snapshots/d90e47400a6a9800a0dcf18d62cfa277.json new file mode 100644 index 000000000..cb3f90984 --- /dev/null +++ b/plugins/ui/docs/snapshots/d90e47400a6a9800a0dcf18d62cfa277.json @@ -0,0 +1 @@ +{"file":"components/multi_select.md","objects":{"my_multi_select_form_example":{"type":"deephaven.ui.Element","data":{"document":{"__dhElemName":"__main__.ui_multi_select_form_example","props":{"children":{"__dhElemName":"deephaven.ui.components.Form","props":{"children":[{"__dhElemName":"deephaven.ui.components.MultiSelect","props":{"menuTrigger":"input","align":"end","direction":"bottom","shouldFlip":true,"formValue":"text","validationBehavior":"aria","label":"Ice cream flavors","name":"flavors","labelPosition":"top","children":[{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Chocolate"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Mint"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Vanilla"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Strawberry"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Cookies and Cream"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Coffee"}},{"__dhElemName":"deephaven.ui.components.Item","props":{"children":"Mango"}}]}},{"__dhElemName":"deephaven.ui.components.Button","props":{"variant":"accent","style":"fill","type":"submit","children":"Submit"}}],"validationBehavior":"aria","labelPosition":"top","onSubmit":{"__dhCbid":"cb0"}}}}},"state":"{}"}}}} \ No newline at end of file