Skip to content

Commit

Permalink
feat: UI ComboBox component (#588)
Browse files Browse the repository at this point in the history
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 <mikebender@deephaven.io>
  • Loading branch information
bmingles and mofojed authored Jul 9, 2024
1 parent 66dc4bf commit 0564299
Show file tree
Hide file tree
Showing 11 changed files with 178 additions and 22 deletions.
83 changes: 79 additions & 4 deletions plugins/ui/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,15 +348,15 @@ def ui_picker_table():
column_types,
label="Text",
on_change=set_value,
selected_keys=value,
selected_key=value,
)

text = ui.text(f"Selection: {value}")

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)
Expand Down Expand Up @@ -391,19 +391,94 @@ 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}")

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.
Expand Down
1 change: 0 additions & 1 deletion plugins/ui/src/deephaven/ui/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion plugins/ui/src/deephaven/ui/components/combo_box.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import Callable
from typing import Callable, Any

from .types import (
FocusEventCallable,
Expand Down
44 changes: 44 additions & 0 deletions plugins/ui/src/js/src/elements/ComboBox.tsx
Original file line number Diff line number Diff line change
@@ -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<DHComboBoxJSApiProps>
>
): JSX.Element | null {
const settings = useSelector(getSettings<RootState>);
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
<DHComboBoxJSApi {...pickerProps} table={table} settings={settings} />
)
);
}

// eslint-disable-next-line react/jsx-props-no-spreading
return <DHComboBox {...pickerProps}>{children}</DHComboBox>;
}

export default ComboBox;
22 changes: 18 additions & 4 deletions plugins/ui/src/js/src/elements/Picker.tsx
Original file line number Diff line number Diff line change
@@ -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<DHPickerJSApiProps>
>
): JSX.Element | null {
const settings = useSelector(getSettings<RootState>);
const { children, ...pickerProps } = usePickerProps(props);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ 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
* @returns A callback to be passed into the Spectrum component that transforms the event and calls the provided callback
*/
export function useFocusEventCallback(
callback?: SerializedFocusEventCallback
): (e: FocusEvent) => void {
): DeserializedFocusEventCallback {
return useCallback(
(e: FocusEvent) => {
callback?.(serializeFocusEvent(e));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ 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
* @returns A callback to be passed into the Spectrum component that transforms the event and calls the provided callback
*/
export function useKeyboardEventCallback(
callback?: SerializedKeyboardEventCallback
): (e: KeyboardEvent) => void {
): DeserializedKeyboardEventCallback {
return useCallback(
(e: KeyboardEvent) => {
callback?.(serializeKeyboardEvent(e));
Expand Down
36 changes: 26 additions & 10 deletions plugins/ui/src/js/src/elements/hooks/usePickerProps.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -25,28 +25,44 @@ export interface SerializedPickerEventProps {
onKeyUp?: SerializedKeyboardEventCallback;
}

type WrappedDHPickerJSApiProps = Omit<DHPickerJSApiProps, 'table'> & {
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<TProps> = Omit<TProps, 'table'> & {
children: ReactElement<ObjectViewProps, typeof ObjectView>;
};

export type SerializedPickerProps = (
| DHPickerProps
| WrappedDHPickerJSApiProps
) &
SerializedPickerEventProps;
export type SerializedPickerProps<TProps> = TProps & SerializedPickerEventProps;

export type DeserializedPickerProps<TProps> = 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<TProps>({
onFocus,
onBlur,
onKeyDown,
onKeyUp,
...otherProps
}: SerializedPickerProps): DHPickerProps | WrappedDHPickerJSApiProps {
}: SerializedPickerProps<TProps>): DeserializedPickerProps<TProps> {
const serializedOnFocus = useFocusEventCallback(onFocus);
const serializedOnBlur = useFocusEventCallback(onBlur);
const serializedOnKeyDown = useKeyboardEventCallback(onKeyDown);
Expand Down
1 change: 1 addition & 0 deletions plugins/ui/src/js/src/elements/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
1 change: 1 addition & 0 deletions plugins/ui/src/js/src/elements/model/ElementConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
2 changes: 2 additions & 0 deletions plugins/ui/src/js/src/widget/WidgetUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
ActionButton,
ActionGroup,
Button,
ComboBox,
Form,
IllustratedMessage,
ListView,
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 0564299

Please sign in to comment.