From 0564299ae58a2df109f607691cb6d5bc9cfc50b4 Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Tue, 9 Jul 2024 14:22:58 -0500 Subject: [PATCH] feat: UI ComboBox component (#588) ui.combo_box ## Testing ```python from deephaven import ui @ui.component def ui_combo_box(): value, set_value = ui.use_state("") combo = ui.combo_box( "Text 1", "Text 2", "Text 3", label="Text", on_selection_change=set_value, selected_key=value, ) text = ui.text("Selection: " + str(value)) return combo, text my_combo_box = ui_combo_box() ``` ```python import deephaven.ui as ui from deephaven import time_table import datetime # Ticking table with initial row count of 200 that adds a row every second initial_row_count = 200 _table = time_table( "PT1S", start_time=datetime.datetime.now() - datetime.timedelta(seconds=initial_row_count), ).update( [ "Id=new Integer(i)", "Display=new String(`Display `+i)", ] ) @ui.component def ui_combo_box_item_table_source(table): value, set_value = ui.use_state("") combo = ui.combo_box( ui.item_table_source(table, key_column="Id", label_column="Display"), label="Text", on_change=set_value, selected_key=value, ) text = ui.text(f"Selection: {value}") return combo, text my_combo_box_item_table_source = ui_combo_box_item_table_source(_table) ``` resolves #201 --------- Co-authored-by: Mike Bender --- plugins/ui/docs/README.md | 83 ++++++++++++++++++- .../src/deephaven/ui/components/__init__.py | 1 - .../src/deephaven/ui/components/combo_box.py | 2 +- plugins/ui/src/js/src/elements/ComboBox.tsx | 44 ++++++++++ plugins/ui/src/js/src/elements/Picker.tsx | 22 ++++- .../elements/hooks/useFocusEventCallback.ts | 4 +- .../hooks/useKeyboardEventCallback.ts | 4 +- .../js/src/elements/hooks/usePickerProps.ts | 36 +++++--- 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 + 11 files changed, 178 insertions(+), 22 deletions(-) create mode 100644 plugins/ui/src/js/src/elements/ComboBox.tsx diff --git a/plugins/ui/docs/README.md b/plugins/ui/docs/README.md index cbe59f909..e72371b19 100644 --- a/plugins/ui/docs/README.md +++ b/plugins/ui/docs/README.md @@ -348,7 +348,7 @@ def ui_picker_table(): column_types, label="Text", on_change=set_value, - selected_keys=value, + selected_key=value, ) text = ui.text(f"Selection: {value}") @@ -356,7 +356,7 @@ def ui_picker_table(): return ui.flex(pick_table, text, direction="column", margin=10, gap=10) -pick_table = ui_picker_table() +my_picker_table = ui_picker_table() ``` ![Use a picker to select from a table](_assets/pick_table.png) @@ -391,7 +391,7 @@ def ui_picker_table_source(): ui.item_table_source(column_types, key_column="Id", label_column="Display"), label="Text", on_change=set_value, - selected_keys=value, + selected_key=value, ) text = ui.text(f"Selection: {value}") @@ -399,11 +399,86 @@ def ui_picker_table_source(): return ui.flex(pick_table, text, direction="column", margin=10, gap=10) -pick_table_source = ui_picker_table_source() +my_picker_table_source = ui_picker_table_source() ``` ![Use a picker to select from a table source](_assets/pick_table_source.png) +## ComboBox (string values) + +The `ui.combo_box` component can be used to select from a list of items. It also provides a search field to filter available results. Note that the search behavior differs slightly for different data types. +- Numeric types - only support exact match +- Text based data types - support partial search matching +- Date types support searching by different date parts (e.g. `2024`, `2024-01`, `2024-01-02`, `2024-01-02 00`, `2024-07-06 00:43`, `2024-07-06 00:43:14`, `2024-07-06 00:43:14.247`) + +Here's a basic example for selecting from a list of string values and displaying the selected key in a text field. + +```python +from deephaven import ui + + +@ui.component +def ui_combo_box(): + value, set_value = ui.use_state("") + + combo = ui.combo_box( + "Text 1", + "Text 2", + "Text 3", + label="Text", + on_selection_change=set_value, + selected_key=value, + ) + + text = ui.text("Selection: " + str(value)) + + return combo, text + + +my_combo_box = ui_combo_box() +``` + +## ComboBox (item table source) + +A `combo_box` can also take an `item_table_source`. It will use the columns specified. + +```python +import deephaven.ui as ui +from deephaven import time_table +import datetime + +# Ticking table with initial row count of 200 that adds a row every second +initial_row_count = 200 +_table = time_table( + "PT1S", + start_time=datetime.datetime.now() - datetime.timedelta(seconds=initial_row_count), +).update( + [ + "Id=new Integer(i)", + "Display=new String(`Display `+i)", + ] +) + + +@ui.component +def ui_combo_box_item_table_source(table): + value, set_value = ui.use_state("") + + combo = ui.combo_box( + ui.item_table_source(table, key_column="Id", label_column="Display"), + label="Text", + on_change=set_value, + selected_key=value, + ) + + text = ui.text(f"Selection: {value}") + + return combo, text + + +my_combo_box_item_table_source = ui_combo_box_item_table_source(_table) +``` + ## ListView (string values) A list view that can be used to create a list of selectable items. Here's a basic example for selecting from a list of string values and displaying the selected key in a text field. diff --git a/plugins/ui/src/deephaven/ui/components/__init__.py b/plugins/ui/src/deephaven/ui/components/__init__.py index 06f21319b..05805470f 100644 --- a/plugins/ui/src/deephaven/ui/components/__init__.py +++ b/plugins/ui/src/deephaven/ui/components/__init__.py @@ -47,7 +47,6 @@ from .text_field import text_field from .toggle_button import toggle_button from .view import view -from .types import * from . import html diff --git a/plugins/ui/src/deephaven/ui/components/combo_box.py b/plugins/ui/src/deephaven/ui/components/combo_box.py index fc77072c0..028ae7c9f 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 +from typing import Callable, Any from .types import ( FocusEventCallable, diff --git a/plugins/ui/src/js/src/elements/ComboBox.tsx b/plugins/ui/src/js/src/elements/ComboBox.tsx new file mode 100644 index 000000000..5fa46037a --- /dev/null +++ b/plugins/ui/src/js/src/elements/ComboBox.tsx @@ -0,0 +1,44 @@ +import { useSelector } from 'react-redux'; +import { + ComboBox as DHComboBox, + ComboBoxProps as DHComboBoxProps, +} from '@deephaven/components'; +import { + ComboBox as DHComboBoxJSApi, + ComboBoxProps as DHComboBoxJSApiProps, +} from '@deephaven/jsapi-components'; +import { isElementOfType } from '@deephaven/react-hooks'; +import { getSettings, RootState } from '@deephaven/redux'; +import { + SerializedPickerProps, + usePickerProps, + WrappedDHPickerJSApiProps, +} from './hooks/usePickerProps'; +import ObjectView from './ObjectView'; +import { useReExportedTable } from './hooks/useReExportedTable'; + +export function ComboBox( + props: SerializedPickerProps< + DHComboBoxProps | WrappedDHPickerJSApiProps + > +): JSX.Element | null { + const settings = useSelector(getSettings); + const { children, ...pickerProps } = usePickerProps(props); + + const isObjectView = isElementOfType(children, ObjectView); + const table = useReExportedTable(children); + + if (isObjectView) { + return ( + table && ( + // eslint-disable-next-line react/jsx-props-no-spreading + + ) + ); + } + + // eslint-disable-next-line react/jsx-props-no-spreading + return {children}; +} + +export default ComboBox; diff --git a/plugins/ui/src/js/src/elements/Picker.tsx b/plugins/ui/src/js/src/elements/Picker.tsx index 79ce0ada4..0d65bf6b3 100644 --- a/plugins/ui/src/js/src/elements/Picker.tsx +++ b/plugins/ui/src/js/src/elements/Picker.tsx @@ -1,13 +1,27 @@ import { useSelector } from 'react-redux'; -import { Picker as DHPicker } from '@deephaven/components'; -import { Picker as DHPickerJSApi } from '@deephaven/jsapi-components'; +import { + Picker as DHPicker, + PickerProps as DHPickerProps, +} from '@deephaven/components'; +import { + Picker as DHPickerJSApi, + PickerProps as DHPickerJSApiProps, +} from '@deephaven/jsapi-components'; import { isElementOfType } from '@deephaven/react-hooks'; import { getSettings, RootState } from '@deephaven/redux'; -import { SerializedPickerProps, usePickerProps } from './hooks/usePickerProps'; +import { + SerializedPickerProps, + usePickerProps, + WrappedDHPickerJSApiProps, +} from './hooks/usePickerProps'; import ObjectView from './ObjectView'; import useReExportedTable from './hooks/useReExportedTable'; -export function Picker(props: SerializedPickerProps): JSX.Element | null { +export function Picker( + props: SerializedPickerProps< + DHPickerProps | WrappedDHPickerJSApiProps + > +): JSX.Element | null { const settings = useSelector(getSettings); const { children, ...pickerProps } = usePickerProps(props); diff --git a/plugins/ui/src/js/src/elements/hooks/useFocusEventCallback.ts b/plugins/ui/src/js/src/elements/hooks/useFocusEventCallback.ts index 17b480ecc..2a989703d 100644 --- a/plugins/ui/src/js/src/elements/hooks/useFocusEventCallback.ts +++ b/plugins/ui/src/js/src/elements/hooks/useFocusEventCallback.ts @@ -26,6 +26,8 @@ export type SerializedFocusEventCallback = ( event: SerializedFocusEvent ) => void; +export type DeserializedFocusEventCallback = (e: FocusEvent) => void; + /** * Get a callback function to be passed into spectrum components * @param callback FocusEvent callback to be called with the serialized event @@ -33,7 +35,7 @@ export type SerializedFocusEventCallback = ( */ export function useFocusEventCallback( callback?: SerializedFocusEventCallback -): (e: FocusEvent) => void { +): DeserializedFocusEventCallback { return useCallback( (e: FocusEvent) => { callback?.(serializeFocusEvent(e)); diff --git a/plugins/ui/src/js/src/elements/hooks/useKeyboardEventCallback.ts b/plugins/ui/src/js/src/elements/hooks/useKeyboardEventCallback.ts index b04a207ca..770b1d410 100644 --- a/plugins/ui/src/js/src/elements/hooks/useKeyboardEventCallback.ts +++ b/plugins/ui/src/js/src/elements/hooks/useKeyboardEventCallback.ts @@ -26,6 +26,8 @@ export type SerializedKeyboardEventCallback = ( event: SerializedKeyboardEvent ) => void; +export type DeserializedKeyboardEventCallback = (e: KeyboardEvent) => void; + /** * Get a callback function to be passed into spectrum components * @param callback KeyboardEvent callback to be called with the serialized event @@ -33,7 +35,7 @@ export type SerializedKeyboardEventCallback = ( */ export function useKeyboardEventCallback( callback?: SerializedKeyboardEventCallback -): (e: KeyboardEvent) => void { +): DeserializedKeyboardEventCallback { return useCallback( (e: KeyboardEvent) => { callback?.(serializeKeyboardEvent(e)); diff --git a/plugins/ui/src/js/src/elements/hooks/usePickerProps.ts b/plugins/ui/src/js/src/elements/hooks/usePickerProps.ts index a1f0531b0..2dede120e 100644 --- a/plugins/ui/src/js/src/elements/hooks/usePickerProps.ts +++ b/plugins/ui/src/js/src/elements/hooks/usePickerProps.ts @@ -1,12 +1,12 @@ -import { PickerProps as DHPickerProps } from '@deephaven/components'; -import { PickerProps as DHPickerJSApiProps } from '@deephaven/jsapi-components'; import { ReactElement } from 'react'; import ObjectView, { ObjectViewProps } from '../ObjectView'; import { + DeserializedFocusEventCallback, SerializedFocusEventCallback, useFocusEventCallback, } from './useFocusEventCallback'; import { + DeserializedKeyboardEventCallback, SerializedKeyboardEventCallback, useKeyboardEventCallback, } from './useKeyboardEventCallback'; @@ -25,28 +25,44 @@ export interface SerializedPickerEventProps { onKeyUp?: SerializedKeyboardEventCallback; } -type WrappedDHPickerJSApiProps = Omit & { +export interface DeserializedPickerEventProps { + /** Handler that is called when the element receives focus. */ + onFocus?: DeserializedFocusEventCallback; + + /** Handler that is called when the element loses focus. */ + onBlur?: DeserializedFocusEventCallback; + + /** Handler that is called when a key is pressed */ + onKeyDown?: DeserializedKeyboardEventCallback; + + /** Handler that is called when a key is released */ + onKeyUp?: DeserializedKeyboardEventCallback; +} + +export type WrappedDHPickerJSApiProps = Omit & { children: ReactElement; }; -export type SerializedPickerProps = ( - | DHPickerProps - | WrappedDHPickerJSApiProps -) & - SerializedPickerEventProps; +export type SerializedPickerProps = TProps & SerializedPickerEventProps; + +export type DeserializedPickerProps = Omit< + TProps, + keyof SerializedPickerEventProps +> & + DeserializedPickerEventProps; /** * Wrap Picker props with the appropriate serialized event callbacks. * @param props Props to wrap * @returns Wrapped props */ -export function usePickerProps({ +export function usePickerProps({ onFocus, onBlur, onKeyDown, onKeyUp, ...otherProps -}: SerializedPickerProps): DHPickerProps | WrappedDHPickerJSApiProps { +}: SerializedPickerProps): DeserializedPickerProps { const serializedOnFocus = useFocusEventCallback(onFocus); const serializedOnBlur = useFocusEventCallback(onBlur); const serializedOnKeyDown = useKeyboardEventCallback(onKeyDown); diff --git a/plugins/ui/src/js/src/elements/index.ts b/plugins/ui/src/js/src/elements/index.ts index 2570229c4..f4f51ab81 100644 --- a/plugins/ui/src/js/src/elements/index.ts +++ b/plugins/ui/src/js/src/elements/index.ts @@ -1,6 +1,7 @@ export * from './ActionButton'; export * from './ActionGroup'; export * from './Button'; +export * from './ComboBox'; export * from './Form'; export * from './hooks'; export * from './HTMLElementView'; diff --git a/plugins/ui/src/js/src/elements/model/ElementConstants.ts b/plugins/ui/src/js/src/elements/model/ElementConstants.ts index 94f2de96e..60043e49a 100644 --- a/plugins/ui/src/js/src/elements/model/ElementConstants.ts +++ b/plugins/ui/src/js/src/elements/model/ElementConstants.ts @@ -29,6 +29,7 @@ export const ELEMENT_NAME = { button: uiComponentName('Button'), buttonGroup: uiComponentName('ButtonGroup'), checkbox: uiComponentName('Checkbox'), + comboBox: uiComponentName('ComboBox'), content: uiComponentName('Content'), contextualHelp: uiComponentName('ContextualHelp'), flex: uiComponentName('Flex'), diff --git a/plugins/ui/src/js/src/widget/WidgetUtils.tsx b/plugins/ui/src/js/src/widget/WidgetUtils.tsx index 595758e60..97cf89a52 100644 --- a/plugins/ui/src/js/src/widget/WidgetUtils.tsx +++ b/plugins/ui/src/js/src/widget/WidgetUtils.tsx @@ -50,6 +50,7 @@ import { ActionButton, ActionGroup, Button, + ComboBox, Form, IllustratedMessage, ListView, @@ -97,6 +98,7 @@ export const elementComponentMap = { [ELEMENT_NAME.button]: Button, [ELEMENT_NAME.buttonGroup]: ButtonGroup, [ELEMENT_NAME.checkbox]: Checkbox, + [ELEMENT_NAME.comboBox]: ComboBox, [ELEMENT_NAME.content]: Content, [ELEMENT_NAME.contextualHelp]: ContextualHelp, [ELEMENT_NAME.flex]: Flex,