diff --git a/plugins/ui/DESIGN.md b/plugins/ui/DESIGN.md index 3216d9cb2..dfa79ec6d 100644 --- a/plugins/ui/DESIGN.md +++ b/plugins/ui/DESIGN.md @@ -1825,7 +1825,6 @@ date_range_picker1 = ui.date_range_picker( # this creates a date picker with a granularity of seconds in UTC # the on_change handler is passed a range of instants dates2, set_dates2 = ui.use_state({"start": instant_start, "end": instant_end}) - date_range_picker2 = ui.date_range_picker( value=dates2, on_change=set_dates2 @@ -2070,6 +2069,139 @@ ui.picker( ) -> PickerElement ``` +###### ui.range_calendar + +A calendar that can be used to select a range of dates. + +The range is a dictionary with a `start` date and an `end` date; e.g., `{ "start": "2024-01-02", "end": "2024-01-05" }` + +The range calendar accepts the following date types as inputs: + +- `None` +- `LocalDate` +- `ZoneDateTime` +- `Instant` +- `int` +- `str` +- `datetime.datetime` +- `numpy.datetime64` +- `pandas.Timestamp` + +The input will be converted to one of three Java date types: + +1. `LocalDate`: A LocalDate is a date without a time zone in the ISO-8601 system, such as "2007-12-03" or "2057-01-28". +2. `Instant`: An Instant represents an unambiguous specific point on the timeline, such as 2021-04-12T14:13:07 UTC. +3. `ZonedDateTime`: A ZonedDateTime represents an unambiguous specific point on the timeline with an associated time zone, such as 2021-04-12T14:13:07 America/New_York. + +The format of the range calendar and the type of the value passed to the `on_change` handler +is determined by the type of the following props in order of precedence: + +1. `value` +2. `default_value` +3. `focused_value` +4. `default_focused_value` + +If none of these are provided, the `on_change` handler passes a range of `Instant`. + +```py +import deephaven.ui as ui +ui.range_calendar( + value: DateRange | None = None, + default_value: DateRange | None = None, + focused_value: Date | None = None, + default_focused_value: Date | None = None, + min_value: Date | None = None, + max_value: Date | None = None, + on_change: Callable[[DateRange], None] | None = None, + **props: Any +) -> RangeCalendarElement +``` + +###### Parameters + +| Parameter | Type | Description | +| ----------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------- | +| `value` | `DateRange \| None` | The current value (controlled). | +| `default_value` | `DateRange \| None` | The default value (uncontrolled). | +| `focused_value` | `Date \| None` | The focused value (controlled). | +| `default_focused_value` | `Date \| None` | The default focused value (uncontrolled). | +| `min_value` | `Date \| None` | The minimum allowed date that a user may select. | +| `max_value` | `Date \| None` | The maximum allowed date that a user may select. | +| `on_change` | `Callable[[DateRange], None] \| None` | Handler that is called when the value changes. | +| `**props` | `Any` | Any other [RangeCalendar](https://react-spectrum.adobe.com/react-spectrum/RangeCalendar.html) prop | + +```py + +import deephaven.ui as ui +from deephaven.time import to_j_local_date, dh_today, to_j_instant, to_j_zdt + +zdt_start = to_j_zdt("1995-03-22T11:11:11.23142 America/New_York") +zdt_end = to_j_zdt("1995-03-25T11:11:11.23142 America/New_York") +instant_start = to_j_instant("2022-01-01T00:00:00 ET") +instant_end = to_j_instant("2022-01-05T00:00:00 ET") +local_start = to_j_local_date("2024-05-06") +local_end = to_j_local_date("2024-05-10") + +# simple range calendar that takes a range and is uncontrolled +range_calendar1 = ui.range_calendar( + default_value={"start": local_start, "end": local_end} +) + +# simple range calendar that takes a range directly and is controlled +# the on_change handler is passed a range of instants +dates, set_dates = ui.use_state({"start": instant_start, "end": instant_end}) + +range_calendar2 = ui.range_calendar( + value=dates, + on_change=set_dates +) + +# this creates a range calendar in the specified time zone +# the on_change handler is passed a zoned date time +dates, set_dates = ui.use_state(None) + +range_calendar3 = ui.range_calendar( + default_value=zdt_start, + on_change=set_dates +) + +# this creates a range calendar in UTC +# the on_change handler is passed an instant +dates, set_dates = ui.use_state(None) + +range_calendar4 = ui.range_calendar( + default_value=instant_start, + on_change=set_dates +) + +# this creates a range calendar +# the on_change handler is passed a local date +dates, set_dates = ui.use_state(None) + +range_calendar5 = ui.range_calendar( + default_value=local_start, + on_change=set_dates +) + +# this creates a range calendar the on_change handler is passed an instant +dates, set_dates = ui.use_state(None) + +range_calendar7 = ui.range_calendar( + on_change=set_dates +) + +# this create a calendar, a min and max value +min_value = to_j_local_date("2022-01-01") +max_value = to_j_local_date("2022-12-31") +dates, set_dates = ui.use_state({"start": local_start, "end": local_end}) +range_calendar8 = ui.range_calendar( + value=dates, + min_value=min_value, + max_value=max_value, + on_change=set_dates +) +``` + ###### Parameters | Parameter | Type | Description | @@ -2544,6 +2676,71 @@ ui_table.sort( | `by` | `str \| Sequence[str]` | The column(s) to sort by. May be a single column name, or a list of column names. | | `direction` | `TableSortDirection \| Sequence[TableSortDirection] \| None` | The sort direction(s) to use. If provided, that must match up with the columns provided. Defaults to "ASC". | +##### ui.table Formatting Rules + +Tables can be formatted via the `formatting` prop and a Dataclass that contains the different formatting options. We choose to use Dataclass as effectively a more strict dictionary. This allows for better type checking and easier to understand code. + +Formatting can be a variety of different options including background color, font, font color, font size, text alignment, cell rendering mode (data bars, button, etc.), and number formatting. It can be thought of as any visual manipulation of the base table display and should be flexible enough to allow for any kind of formatting that the user desires (assuming the rule is supported). + +The formatting rules should have 3 main properties: + +1. The formatting to apply. +2. Where to apply the formatting. +3. An optional conditional expression that will determine if the formatting should be applied (the `where` arg). + +Outside of these 3 properties, it is up to the Dataclass to identify what kind of formatting it is and up to the web to apply that formatting. + +On the client, formatting rules should be applied in the order they are defined in the list and stop as soon as a rule is applied. This should only prevent later rules from being applied if they are the same type of rule. For example, if a color rule is applied to a cell, a number formatting rule should still be applied to that cell if it is defined later in the list, but another color rule should not be applied. + +We will use a single dataclass that encompasses both row and column formatting. If `cols` is provided, it is a column rule, else it is a row rule. This dataclass will support the different formatting types as a keyword argument. This allows for multiple different formatting rules to be applied to a column or row with the same condition without repeating. + +In the future we may implement column and row formatting dataclasses which would extend the main formatting class and require or prohibit `cols`. + +We should support some method of applying value formatting to all columns that support it. This would be useful for number formatting, date formatting, etc. We could support `cols="*"` or if there is no `cols` (indicating a row rule), we could apply the value formatting to all columns that are supported in the matching rows (or all rows if no `where`). + +We could also support regex (or just wildcards) in the `cols` field to apply the rule to multiple columns at once. This could be useful if a user wanted to format all columns that end in percent as a percentage. Something like `FORMAT(cols="*Percent", format="0.00%")`. We could require regex strings be surrounded by `/` to differentiate them from normal strings with wildcards. + +```py +from deephaven import ui, time_table + +_t = time_table("PT1S").update("X=i % 10", "Y=i % 10", "Z=i % 100") + +t = ui.table( + t, + formatting=[ + ui.table.FORMAT(cols="X", color="RED"), + ui.table.FORMAT(cols="Y", color="BLUE", where="Y % 2 == 0"), + ui.table.FORMAT(cols="Y", value="0.00"), + ui.table.FORMAT(cols=["A", "B"], color="PURPLE", value="0.00%", where="A > 5"), + ui.table.FORMAT(cols="Z", mode=ui.table.DATABAR(value_col="Z", min=0, max=100, positive_color="GREEN", negative_color="RED"), + ui.table.FORMAT(where="X > 5", color="GREEN") + ] +) +``` + +##### ui.table Formatting Rule Types + +There are 3 main types of formatting rules: those which affect basic visual aspects (color, font, etc.), those which affect the display value (number formatting, etc.), and those which affect the cell rendering mode (data bars, buttons, etc.). Multiple different visual rules can be applied at the same time, but only one display value rule and one cell rendering mode rule can be applied at a time. It doesn't make sense to have a cell that is both a data bar and a button, for example. If a rule is applied conditionally, multiple display or cell rendering rules should be allowed to be applied in a column. + +Some examples of potential rules that fall into each category: + +1. Visual (each should have a keyword arg in the dataclass) + - Background Color + - Font + - Font Color + - Font Size + - Text Alignment +2. Display Value (one keyword arg such as `display` in the dataclass) + - Number Formatting + - Date Formatting + - DateTime Formatting + - String Formatting +3. Cell Rendering Mode (one keyword arg such as `mode` in the dataclass) + - Data Bars + - Buttons + - Checkboxes + - Icons + ###### ui.time_field A time field that can be used to select a time. diff --git a/plugins/ui/docs/components/button_group.md b/plugins/ui/docs/components/button_group.md new file mode 100644 index 000000000..48b32e584 --- /dev/null +++ b/plugins/ui/docs/components/button_group.md @@ -0,0 +1,121 @@ +# Button Group + +A button group is a UI component that groups buttons with related actions together and will automatically handle layout overflow nicely. Only buttons can be used within button groups. + +## Example + +```python +from deephaven import ui + + +my_button_group_basic = ui.button_group( + ui.button("Rate Now", variant="accent"), + ui.button("No, thanks", variant="primary", style="outline"), + ui.button("Remind me later", variant="primary", style="outline"), +) +``` + +## UI Recommendations + +Recommendations for creating button groups: + +1. The most critical action in a button group should use an accent, or negative button style, while other actions should be primary outline buttons. +2. Button groups should be left-aligned to follow content such as blocks of text, center-aligned in empty states, and right-aligned in container components like dialogs, popovers, or cards. +3. Button priority should match text alignment: for left-aligned text, the most critical button is on the left; for right- or center-aligned text, the most critical button is on the right. +4. Icons should be used for higher-priority actions if used in the button group. If the most critical action does not have an icon, avoid using icons for the other lower-priority actions. + +Consider using an [`action_group`](./action_group.md) to allow the user to select from a list of actions. + + +## Content + +A button group is used to handle button overflow and, thus, expects buttons as children. It switches to a vertical layout when horizontal space is limited. + +```python +from deephaven import ui + + +my_button_group_content_space_example = ui.view( + ui.button_group( + ui.button("Rate Now", variant="accent", style="outline"), + ui.button("Remind me later", variant="primary", style="outline"), + ui.button("No, thanks", variant="primary", style="outline"), + ), + width=200, + border_width="thin", + padding="size-100", +) +``` + + +## Orientation + +Setting the `orientation` prop to "vertical" will prevent any spacing-related dynamic orientation changes. + +The button group will remain in the orientation regardless of the width. + +```python +from deephaven import ui + + +my_button_group_orientation_example = ui.button_group( + ui.button("No, thanks", variant="primary", style="outline"), + ui.button("Remind me later", variant="primary", style="outline"), + ui.button("Rate Now", variant="accent"), + orientation="vertical", +) +``` + + +## Alignment + +By default, button groups are start-aligned to accord with content, but they can be set to have a different alignment using the `alignment` prop. + +```python +from deephaven import ui + + +@ui.component +def ui_button_group_alignment_examples(): + return [ + ui.button_group( + ui.button("No, thanks", variant="primary", style="outline"), + ui.button("Remind me later", variant="primary", style="outline"), + ui.button("Rate Now", variant="accent"), + align="center", + ), + ui.button_group( + ui.button("No, thanks", variant="primary", style="outline"), + ui.button("Remind me later", variant="primary", style="outline"), + ui.button("Rate Now", variant="accent"), + align="end", + ), + ] + + +my_button_group_alignment_examples = ui_button_group_alignment_examples() +``` + + +## Disabled state + +The `is_disabled` prop disables the button group to prevent user interaction. This is useful when the button group should be visible but not available for selection. + + +```python +from deephaven import ui + + +my_button_group_is_disabled_example = ui.button_group( + ui.button("No, thanks", variant="primary", style="outline"), + ui.button("Remind me later", variant="primary", style="outline"), + ui.button("Rate Now", variant="accent"), + is_disabled=True, +) +``` + +## API Reference + +```{eval-rst} +.. dhautofunction:: deephaven.ui.button_group +``` diff --git a/plugins/ui/docs/components/checkbox_group.md b/plugins/ui/docs/components/checkbox_group.md new file mode 100644 index 000000000..aca3cdda9 --- /dev/null +++ b/plugins/ui/docs/components/checkbox_group.md @@ -0,0 +1,357 @@ +# Checkbox Group + +Checkbox group areas allow the selection of one or more items from a list of choices, represented by checkboxes. + +## Example + +```python +from deephaven import ui + + +my_checkbox_group_basic = ui.checkbox_group( + "Soccer", + "Basketball", + "Baseball", + label="Favourite Sports", +) +``` + +## UI Recommendations + +Recommendations for creating checkbox groups: + +1. Every checkbox group should have a [label](#labeling) specified. Without one, the checkbox group is ambiguous. In the rare case that context is sufficient, the label is unnecessary; you must still include an aria-label via the `aria_label` prop. +2. While labels can be placed either on top or on the side of the checkbox groups, top labels are the default recommendation. Top labels work better with longer copy, localization, and responsive layouts. Side labels are more useful when vertical space is limited. +3. Checkbox groups can be either horizontal or vertical. By default, they are vertical; the orientation should only be horizontal if vertical space is limited. +4. Checkbox groups can be marked as optional or required, with required groups indicated by either a “(required)” label or an asterisk icon, which should be accompanied by help text. +5. Checkbox groups should use help text for error messaging and descriptions, providing context for why a selection is required or clarifying the options. + + +Consider using [`checkbox_group`](./checkbox_group.md) to select or mark a single item as selected. + +## Content + +Checkbox groups accept checkboxes and primitive types as children. Checkboxes accept a child, which is rendered as the checkbox's label. + +```python +from deephaven import ui + + +my_checkbox_group_content_example = ui.checkbox_group( + "Soccer", + ui.checkbox("Basketball"), + label="Favourite Sports", +) +``` + + +## Value + +Checkbox groups allow selecting zero or more items, with initial values set via `default_value` or controlled values via `value`. + +```python +from deephaven import ui + + +@ui.component +def ui_checkbox_group_value_examples(): + value, set_value = ui.use_state(["Soccer"]) + return [ + ui.checkbox_group( + "Soccer", + "Basketball", + "Baseball", + label="Favourite Sports (uncontrolled)", + default_value=["Soccer", "Baseball"], + ), + ui.checkbox_group( + "Soccer", + "Basketball", + "Baseball", + label="Favourite Sports (controlled)", + value=value, + on_change=set_value, + ), + ] + + +my_checkbox_group_value_examples = ui_checkbox_group_value_examples() +``` + + +## HTML Forms + +Checkbox groups can support a `name` prop for integration with HTML forms, allowing for easy identification of a value on form submission. + +```python +from deephaven import ui + + +my_checkbox_name_example = ui.form( + ui.checkbox_group(ui.checkbox("Sample Label"), name="Sample Name") +) +``` + + +## Labeling + +The checkbox group 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_checkbox_group_label_examples(): + return [ + ui.checkbox_group( + ui.checkbox("Wizard", value="wizard"), + ui.checkbox("Dragon", value="dragon"), + label="Favorite avatars", + ), + ui.checkbox_group( + ui.checkbox("Wizard", value="wizard"), + ui.checkbox("Dragon", value="dragon"), + aria_label="Favorite avatars", + ), + ] + + +my_checkbox_group_label_examples = ui_checkbox_group_label_examples() +``` + + +The `is_required` prop and the `necessity_indicator` props can be used to show whether selecting an option in the checked group 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_checkbox_group_required_examples(): + return [ + ui.checkbox_group( + ui.checkbox("Wizard", value="wizard"), + ui.checkbox("Dragon", value="dragon"), + label="Favorite avatars", + is_required=True, + ), + ui.checkbox_group( + ui.checkbox("Wizard", value="wizard"), + ui.checkbox("Dragon", value="dragon"), + label="Favorite avatars", + is_required=True, + necessity_indicator="label", + ), + ui.checkbox_group( + ui.checkbox("Wizard", value="wizard"), + ui.checkbox("Dragon", value="dragon"), + label="Favorite avatars", + necessity_indicator="label", + ), + ] + + +my_checkbox_group_required_examples = ui_checkbox_group_required_examples() +``` + +## Events + +Checkbox groups accept an `on_change` prop, triggered whenever a checkbox within the group is clicked. + +```python +from deephaven import ui + + +@ui.component +def ui_checkbox_group_event_example(): + selected, set_selected = ui.use_state(["Soccer"]) + return [ + ui.checkbox_group( + "Soccer", + "Basketball", + "Baseball", + label="Favourite Sports (controlled)", + value=selected, + on_change=set_selected, + ), + f"You have selected: {selected}!", + ] + + +my_checkbox_group_event_example = ui_checkbox_group_event_example() +``` + +To require specific checkboxes to be checked, set the `is_required` prop at the Checkbox level, not the Checkbox Group. + +```python +from deephaven import ui + + +my_checkbox_group_individual_validation_example = ui.form( + ui.checkbox_group( + ui.checkbox("Terms and conditions", value="terms", is_required=True), + ui.checkbox("Privacy policy", value="privacy", is_required=True), + label="Agree to the following", + is_required=True, + ), + ui.button_group( + ui.button("Submit", type="submit", variant="primary"), + ui.button("Reset", type="reset", variant="secondary"), + ), + on_submit=lambda: print("Form submitted!"), + validation_behavior="native", +) +``` + + +## Orientation + +While aligned vertically by default, the axis with which the checkboxes align can be changed via the `orientation` prop. + +```python +from deephaven import ui + + +my_checkbox_group_orientation_example = ui.checkbox_group( + ui.checkbox("Wizard", value="wizard"), + ui.checkbox("Dragon", value="dragon"), + label="Favorite avatars", + orientation="horizontal", +) +``` + +## Label position + +By default, the position of a checkbox group's label is above the checkbox group, but it can be changed to the side using the `label_position` prop. + +```python +from deephaven import ui + + +my_checkbox_group_label_position_example = ui.checkbox_group( + ui.checkbox("Wizard", value="wizard"), + ui.checkbox("Dragon", value="dragon"), + label="Favorite avatars", + label_position="side", +) +``` + + +## Help text + +A checkbox group can have both a `description` and an `error_message`. The error message should offer specific guidance on how to correct the input. + +The `is_invalid` prop can be used to set whether the current checkbox group state is valid or invalid. + +```python +from deephaven import ui + + +@ui.component +def ui_checkbox_group_help_text_examples(): + return [ + ui.checkbox_group( + "Soccer", + "Basketball", + "Baseball", + label="Favourite sports", + description="Select an avatar from the two options.", + ), + ui.checkbox_group( + "Soccer", + "Basketball", + "Baseball", + label="Favourite sports", + description="Select favourite sports from the two options.", + error_message="Sample invalid error message.", + is_invalid=True, + ), + ] + + +my_checkbox_group_help_text_examples = ui_checkbox_group_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 checkbox group. + +```python +from deephaven import ui + + +my_checkbox_group_contextual_help_example = ui.checkbox_group( + "Soccer", + "Basketball", + "Baseball", + label="Favorite sports", + contextual_help=ui.contextual_help(ui.heading("Content tips")), +) +``` + + +## Disabled state + +The `is_disabled` prop disables a checkbox group to prevent user interaction. This is useful when the checkbox group should be visible but not available for selection. + +```python +from deephaven import ui + + +my_checkbox_group_is_disabled_example = ui.checkbox_group( + "Soccer", + "Basketball", + "Baseball", + label="Favorite sports", + is_disabled=True, +) +``` + + +## Read only + +The `is_read_only` prop makes checkbox groups read-only to prevent user interaction. This is different from setting the `is_disabled` prop since the checkbox group remains focusable and its options remain visible. + +```python +from deephaven import ui + + +my_checkbox_group_is_read_only_example = ui.checkbox_group( + "Soccer", + "Basketball", + "Baseball", + label="Favorite sports", + is_read_only=True, +) +``` + +## Emphasized + +The `is_emphasized` prop makes the selected checkbox the user's accent color, adding a visual prominence to the selection. + + +```python +from deephaven import ui + + +my_checkbox_group_is_emphasized_example = ui.checkbox_group( + "Soccer", + "Basketball", + "Baseball", + label="Favorite sports", + is_emphasized=True, +) +``` + + + +## API Reference + +```{eval-rst} +.. dhautofunction:: deephaven.ui.checkbox_group +``` + diff --git a/plugins/ui/docs/components/list_view.md b/plugins/ui/docs/components/list_view.md new file mode 100644 index 000000000..c66ddc668 --- /dev/null +++ b/plugins/ui/docs/components/list_view.md @@ -0,0 +1,313 @@ +# List View + +List view displays a list of interactive items, and allows a user to navigate, select, or perform an action. It offers greater flexibility in the contents it can render and can distinguish between row selection and actions performed on a row. This makes list view an ideal component for turning table columns into interactive lists. + +## Example + +```python +from deephaven import ui + + +@ui.component +def ui_list_view(): + return ui.list_view( + ui.item("Option 1"), + ui.item("Option 2"), + ui.item("Option 3"), + ui.item("Option 4"), + default_selected_keys=["Option 2", "Option 3"], + ) + + +my_list_view = ui_list_view() +``` + +## Table Source Example + +List view items can also be generated from a table directly or using `item_table_source`. + +### Passing Table Directly + +This method is ideal for quickly displaying a static dataset. By default, the first column is used as the key and label. + +```python +from deephaven import ui, new_table +from deephaven.column import string_col + +_colors = new_table( + [ + string_col("Colors", ["Red", "Blue", "Green"]), + ] +) + + +@ui.component +def ui_list_view_table(): + return ui.list_view(_colors) + + +my_list_view_table = ui_list_view_table() +``` + +### Using item_table_source + +`item_table_source` is used to create complex items from a table (ie., defining which columns are the keys/labels of the data). + +```python +from deephaven import ui, new_table +from deephaven.column import string_col + +_table = new_table( + [ + string_col("Keys", ["key-0", "key-1", "key-2"]), + string_col("Labels", ["Option 0", "Option 1", "Option 2"]), + ] +) + + +@ui.component +def ui_list_view_table_source(): + source = ui.item_table_source(_table, key_column="Keys", label_column="Labels") + return ui.list_view(source) + + +my_list_view_table_source = ui_list_view_table_source() +``` + +## Events + +List view accepts an action that can be triggered when a user performs an action on an item. + +```python +from deephaven import ui, new_table +from deephaven.column import string_col + +_table = new_table( + [ + string_col("Keys", ["key-0", "key-1", "key-2"]), + string_col("Labels", ["Option 0", "Option 1", "Option 2"]), + ] +) + + +@ui.component +def ui_list_view_actions(): + action_item_keys, set_action_item_keys = ui.use_state(["", ""]) + on_action = ui.use_callback( + lambda action_key, item_key: set_action_item_keys([action_key, str(item_key)]), + [], + ) + + source = ui.item_table_source( + _table, + key_column="Keys", + label_column="Labels", + actions=ui.list_action_group( + "Edit", + "Delete", + on_action=on_action, + ), + ) + lv = ui.list_view(source) + + text_action = ui.text("Action: " + " ".join(map(str, action_item_keys))) + + return lv, text_action + + +my_list_view_actions = ui_list_view_actions() +``` + +List view can also accept a handler that is called when the selection is changed. + +```python +from deephaven import ui, new_table +from deephaven.column import string_col + +_table = new_table( + [ + string_col("Keys", ["key-0", "key-1", "key-2"]), + string_col("Labels", ["Option 0", "Option 1", "Option 2"]), + ] +) + + +@ui.component +def ui_list_view_selection(): + value, set_value = ui.use_state(["key-2"]) + + def handle_change(e): + set_value(e) + print("Selection: " + ", ".join(map(str, e))) + + source = ui.item_table_source( + _table, + key_column="Keys", + label_column="Labels", + ) + lv = ui.list_view(source, on_change=handle_change) + + return lv + + +my_list_view_selection = ui_list_view_selection() +``` + +## Disabled Options + +To disable certain rows in the `ListView` component, use the `disabled_keys` prop. By setting this prop with an array of keys, you can prevent interaction with those rows, providing greater control and customization options for the `ListView` behavior. + +```python +from deephaven import ui, new_table +from deephaven.column import string_col + +_table = new_table( + [ + string_col("Keys", ["key-0", "key-1", "key-2"]), + string_col("Labels", ["Option 0", "Option 1", "Option 2"]), + ] +) + + +@ui.component +def ui_list_view_disabled(): + value, set_value = ui.use_state(["key-2"]) + + source = ui.item_table_source( + _table, + key_column="Keys", + label_column="Labels", + ) + lv = ui.list_view( + source, selected_keys=value, on_change=set_value, disabled_keys=["key-0"] + ) + + return lv + + +my_list_view_disabled = ui_list_view_disabled() +``` + +## Quiet State + +```python +from deephaven import ui + + +@ui.component +def ui_list_view_quiet(): + value, set_value = ui.use_state(["Text 2"]) + + quiet_list = ui.list_view( + "Text 1", + "Text 2", + "Text 3", + aria_label="List View - Quiet", + on_change=set_value, + selected_keys=value, + is_quiet=True, + ) + + default_list = ui.list_view( + "Text 1", + "Text 2", + "Text 3", + aria_label="List View - Default", + on_change=set_value, + selected_keys=value, + ) + return quiet_list, default_list + + +my_list_view_quiet = ui_list_view_quiet() +``` + +## Modifying Density + +To adjust the vertical padding of each row in the list view, use the `density` prop. + +```python +from deephaven import ui + + +@ui.component +def ui_list_view_density(): + value, set_value = ui.use_state(["Text 2"]) + + compact_list = ui.list_view( + "Text 1", + "Text 2", + "Text 3", + aria_label="List View - Compact", + on_change=set_value, + selected_keys=value, + density="compact", + ) + + spacious_list = ui.list_view( + "Text 1", + "Text 2", + "Text 3", + aria_label="List View - Spacious", + on_change=set_value, + selected_keys=value, + density="spacious", + ) + return compact_list, spacious_list + + +my_list_view_density = ui_list_view_density() +``` + +## Overflow Mode +The default behavior is to truncate content that overflows its row. Text can be wrapped instead by adding `wrap` to the `overflow_mode` prop. + +Note: Currently not supported if a table source is used. + +```python +from deephaven import ui + + +@ui.component +def ui_list_view_overflow(): + value, set_value = ui.use_state(["Text 2"]) + + truncated_list = ui.list_view( + "Really long Text 1", + "Really long Text 2", + "Really long Text 3", + aria_label="List View - Quiet", + on_change=set_value, + selected_keys=value, + overflow_mode="truncate", + width="150px", + ) + + wrapped_list = ui.list_view( + "Really long Text 1", + "Really long Text 2", + "Really long Text 3", + aria_label="List View - Quiet", + on_change=set_value, + selected_keys=value, + overflow_mode="wrap", + width="150px", + ) + return truncated_list, wrapped_list + + +my_list_view_overflow = ui_list_view_overflow() +``` + + +## API reference + +```{eval-rst} +.. dhautofunction:: deephaven.ui.list_view +``` + +## Item Table Source API reference + +```{eval-rst} +.. dhautofunction:: deephaven.ui.item_table_source +``` \ No newline at end of file diff --git a/plugins/ui/docs/components/range_calendar.md b/plugins/ui/docs/components/range_calendar.md new file mode 100644 index 000000000..57587134a --- /dev/null +++ b/plugins/ui/docs/components/range_calendar.md @@ -0,0 +1,251 @@ +# Range Calendar + +Range calendars display a grid of days in one or more months and allow users to select a contiguous range of dates. + +## Example + +```python +from deephaven import ui + +my_range_calendar_basic = ui.range_calendar(aria_label="Trip Dates") +``` + +## Date types + +The range is a dictionary with a `start` date and an `end` date; e.g., `{ "start": "2024-01-02", "end": "2024-01-05" }`. + +The range calendar accepts the following date types as inputs: + +- `None` +- `LocalDate` +- `ZoneDateTime` +- `Instant` +- `int` +- `str` +- `datetime.datetime` +- `numpy.datetime64` +- `pandas.Timestamp` + +The input will be converted to one of three Java date types: + +1. `LocalDate`: A LocalDate is a date without a time zone in the ISO-8601 system, such as "2007-12-03" or "2057-01-28". +2. `Instant`: An Instant represents an unambiguous specific point on the timeline, such as 2021-04-12T14:13:07 UTC. +3. `ZonedDateTime`: A ZonedDateTime represents an unambiguous specific point on the timeline with an associated time zone, such as 2021-04-12T14:13:07 America/New_York. + +The format of the range calendar and the type of the value passed to the `on_change` handler is determined by the type of the following props in order of precedence: + +1. `value` +2. `default_value` +3. `focused_value` +4. `default_focused_value` + +If none of these are provided, the `on_change` handler passes a range of `Instant`. + +```python +from deephaven import ui +from deephaven.time import to_j_local_date, to_j_instant, to_j_zdt + + +@ui.component +def range_calendar_example(start, end): + dates, set_dates = ui.use_state({"start": start, "end": end}) + return [ + ui.range_calendar(on_change=set_dates, value=dates), + ui.text(str(dates["start"])), + ui.text(str(dates["end"])), + ] + + +zdt_start = to_j_zdt("1995-03-22T11:11:11.23142 America/New_York") +zdt_end = to_j_zdt("1995-03-25T11:11:11.23142 America/New_York") +instant_start = to_j_instant("2022-01-01T00:00:00 ET") +instant_end = to_j_instant("2022-01-05T00:00:00 ET") +local_start = to_j_local_date("2024-05-06") +local_end = to_j_local_date("2024-05-10") + +my_zoned_example = range_calendar_example(zdt_start, zdt_end) +my_instant_example = range_calendar_example(instant_start, instant_end) +my_local_example = range_calendar_example(local_start, local_end) +``` + +## Value + +A range calendar has no selection by default. An initial, uncontrolled value can be provided to the range calendar using the `default_value` prop. Alternatively, a controlled value can be provided using the `value` prop. + +```python +from deephaven import ui + + +@ui.component +def example(): + value, set_value = ui.use_state({"start": "2020-02-03", "end": "2020-02-08"}) + return ui.flex( + ui.range_calendar( + aria_label="Date range (uncontrolled)", + default_value={"start": "2020-02-03", "end": "2020-02-08"}, + ), + ui.range_calendar( + aria_label="Date range (controlled)", value=value, on_change=set_value + ), + gap="size-300", + wrap=True, + ) + + +my_example = example() +``` + +## Labeling + +An `aria_label` must be provided to the Calendar for accessibility. If it is labeled by a separate element, an `aria_labelledby` prop must be provided using the id of the labeling element instead. + +## Events + +Range calendar accepts an `on_change` prop which is triggered whenever a date is selected by the user. + +```python +from deephaven import ui + + +@ui.component +def event_example(): + value, set_value = ui.use_state({"start": "2020-02-03", "end": "2020-02-08"}) + return ui.range_calendar( + aria_label="Calendar (controlled)", value=value, on_change=set_value + ) + + +my_event_example = event_example() +``` + +## Validation + +By default, range calendar allows selecting any date range. The `min_value` and `max_value` props can also be used to prevent the user from selecting dates outside a certain range. + +This example only accepts dates after today. + +```python +from deephaven import ui +from deephaven.time import dh_today + + +my_range_calendar_min_value_example = ui.range_calendar( + aria_label="Appointment Date", min_value=dh_today() +) +``` + +## Controlling the focused date + +By default, the selected date is focused when a Calendar first mounts. If no `value` or `default_value` prop is provided, then the current date is focused. However, range calendar supports controlling which date is focused using the `focused_value` and `on_focus_change` props. This also determines which month is visible. The `default_focused_value` prop allows setting the initial focused date when the range calendar first mounts, without controlling it. + +This example focuses July 1, 2021 by default. The user may change the focused date, and the `on_focus_change` event updates the state. Clicking the button resets the focused date back to the initial value. + +```python +from deephaven import ui +from deephaven.time import to_j_local_date + +default_date = to_j_local_date("2021-07-01") + + +@ui.component +def focused_example(): + value, set_value = ui.use_state(default_date) + return ui.flex( + ui.action_button( + "Reset focused date", on_press=lambda: set_value(default_date) + ), + ui.range_calendar(focused_value=value, on_focus_change=set_value), + direction="column", + align_items="start", + gap="size-200", + ) + + +my_focused_example = focused_example() +``` + +## Disabled state + +The `is_disabled` prop disables the range calendar to prevent user interaction. This is useful when the range calendar should be visible but not available for selection. + +```python +from deephaven import ui + + +my_range_calendar_is_disabled_example = ui.range_calendar( + is_disabled=True, +) +``` + +## Read only + +The `is_read_only` prop makes the range calendar's value immutable. Unlike `is_disabled`, the range calendar remains focusable. + +```python +from deephaven import ui + + +my_range_calendar_is_read_only_example = ui.range_calendar( + is_read_only=True, +) +``` + +## Visible Months + +By default, the range calendar displays a single month. The `visible_months` prop allows displaying up to 3 months at a time. + +```python +from deephaven import ui + + +my_range_calendar_visible_months_example = ui.range_calendar( + visible_months=3, +) +``` + +## Page Behavior + +By default, when pressing the next or previous buttons, pagination will advance by the `visible_months` value. This behavior can be changed to page by single months instead, by setting `page_behavior` to `single`. + +```python +from deephaven import ui + + +my_range_calendar_page_behavior_example = ui.range_calendar( + visible_months=3, page_behavior="single" +) +``` + +## Time table filtering + +Calendars can be used to filter tables with time columns. + +```python +from deephaven.time import dh_now +from deephaven import time_table, ui + + +@ui.component +def date_table_filter(table, start_date, end_date, time_col="Timestamp"): + dates, set_dates = ui.use_state({"start": start_date, "end": end_date}) + start = dates["start"] + end = dates["end"] + return [ + ui.range_calendar(value=dates, on_change=set_dates), + table.where(f"{time_col} >= start && {time_col} < end"), + ] + + +SECONDS_IN_DAY = 86400 +today = dh_now() +_table = time_table("PT1s").update_view( + ["Timestamp=today.plusSeconds(SECONDS_IN_DAY*i)", "Row=i"] +) +date_filter = date_table_filter(_table, today, today.plusSeconds(SECONDS_IN_DAY * 10)) +``` + +## API Reference + +```{eval-rst} +.. dhautofunction:: deephaven.ui.range_calendar +``` diff --git a/plugins/ui/docs/components/toggle_button.md b/plugins/ui/docs/components/toggle_button.md new file mode 100644 index 000000000..c41756c58 --- /dev/null +++ b/plugins/ui/docs/components/toggle_button.md @@ -0,0 +1,164 @@ +# Toggle Button + +A toggle button allows users to toggle a selection on or off, providing a way to switch between two states, such as enabling or disabling a feature. + +## Example + +```python +from deephaven import ui + +my_toggle_button_basic = ui.toggle_button("Pin") +``` + +## UI Recommendations + +If you want to represent a binary choice for the user, consider using a [`checkbox`](./checkbox.md). + + +## Content + +A toggle button accepts a label, an icon, or both as children. + +```python +from deephaven import ui + + +my_toggle_button = ui.toggle_button(ui.icon("pin"), ui.text("Pin content")) +``` + + +## Accessibility + +If no text is passed into the toggle button, and hence, it has no visible label, the `aria_label` prop should be set for accessibility. + +```python +from deephaven import ui + + +my_toggle_button_accessibility_example = ui.toggle_button( + ui.icon("pin"), aria_label="pin content" +) +``` + + +## Value + +A toggle button is not selected by default. Use the `default_selected` prop to set the initial state (uncontrolled) or the `is_selected` prop to control the selected state. + +```python +from deephaven import ui + + +@ui.component +def ui_toggle_button_value_examples(): + selected, set_selected = ui.use_state(False) + return [ + ui.text("Toggle Button (uncontrolled)"), + ui.toggle_button("Pin", default_selected=True, width="90px"), + ui.text("Toggle Button (controlled)"), + ui.toggle_button( + "Pin", is_selected=selected, on_change=set_selected, width="90px" + ), + ] + + +my_toggle_button_value_examples = ui_toggle_button_value_examples() +``` + + +## Events + +The `on_change` property is triggered whenever the value in the toggle button group selection is changed. + +```python +from deephaven import ui + + +@ui.component +def ui_toggle_button_on_change_example(): + is_selected, set_is_selected = ui.use_state(False) + return [ + ui.toggle_button( + "Pin", + is_selected=is_selected, + on_change=set_is_selected, + ), + ui.text( + f"The toggle button is: `{'selected' if is_selected else 'not selected'}`" + ), + ] + + +my_toggle_button_on_change_example = ui_toggle_button_on_change_example() +``` + + +## Quiet state + +The `is_quiet` prop makes a toggle button "quiet". This can be useful when the toggle button and its corresponding styling should not distract users from surrounding content. + +```python +from deephaven import ui + + +my_toggle_button_is_quiet_example = ui.toggle_button( + "Pin", + is_quiet=True, +) +``` + + +## Disabled state + +The `is_disabled` prop disables a toggle button to prevent user interaction. This is useful when the toggle button should be visible but not available for selection. + +```python +from deephaven import ui + + +my_toggle_button_is_disabled_example = ui.toggle_button( + "Pin", + is_disabled=True, +) +``` + + +## Emphasized + +The `is_emphasized` prop makes the toggle button the user's accent color when selected, adding a visual prominence to the selection. + +```python +from deephaven import ui + + +my_toggle_button_is_emphasized_example = ui.toggle_button( + "Pin", + is_emphasized=True, +) +``` + + +## Static Color + +The `static_color` prop can be used when the toggle button is placed over a colored background. + +```python +from deephaven import ui + + +my_toggle_button_static_color_example = ui.view( + ui.toggle_button( + ui.icon("pin"), + ui.text("Pin content"), + static_color="white", + ), + background_color="blue-700", + padding="size-500", +) +``` + +## API Reference + +```{eval-rst} +.. dhautofunction:: deephaven.ui.toggle_button +``` \ No newline at end of file diff --git a/plugins/ui/docs/sidebar.json b/plugins/ui/docs/sidebar.json index 8fbb765f6..970429771 100644 --- a/plugins/ui/docs/sidebar.json +++ b/plugins/ui/docs/sidebar.json @@ -33,6 +33,10 @@ "label": "button", "path": "components/button.md" }, + { + "label": "button_group", + "path": "components/button_group.md" + }, { "label": "checkbox", "path": "components/checkbox.md" @@ -77,6 +81,10 @@ "label": "radio_group", "path": "components/radio_group.md" }, + { + "label": "range_calendar", + "path": "components/range_calendar.md" + }, { "label": "range_slider", "path": "components/range_slider.md" @@ -97,6 +105,10 @@ "label": "text_area", "path": "components/text_area.md" }, + { + "label": "toggle_button", + "path": "components/toggle_button.md" + }, { "label": "view", "path": "components/view.md" diff --git a/plugins/ui/src/deephaven/ui/components/__init__.py b/plugins/ui/src/deephaven/ui/components/__init__.py index 10dcbad90..049beb11e 100644 --- a/plugins/ui/src/deephaven/ui/components/__init__.py +++ b/plugins/ui/src/deephaven/ui/components/__init__.py @@ -8,6 +8,7 @@ from .button_group import button_group from .calendar import calendar from .checkbox import checkbox +from .checkbox_group import checkbox_group from .column import column from .combo_box import combo_box from .content import content @@ -37,6 +38,7 @@ from .progress_circle import progress_circle from .radio import radio from .radio_group import radio_group +from .range_calendar import range_calendar from .range_slider import range_slider from .row import row from .section import section @@ -67,6 +69,7 @@ "button_group", "calendar", "checkbox", + "checkbox_group", "column", "combo_box", "component", @@ -97,6 +100,7 @@ "progress_circle", "radio", "radio_group", + "range_calendar", "range_slider", "row", "section", diff --git a/plugins/ui/src/deephaven/ui/components/action_group.py b/plugins/ui/src/deephaven/ui/components/action_group.py index 04c6d01f4..715d57eca 100644 --- a/plugins/ui/src/deephaven/ui/components/action_group.py +++ b/plugins/ui/src/deephaven/ui/components/action_group.py @@ -89,7 +89,7 @@ def action_group( """ An action grouping of action items that are related to each other. Args: - *children: The children of the contextual help popover. + *children: The children of the action group. is_emphasized: Whether the action buttons should be displayed with emphasized style. density: Sets the amount of space between buttons. is_justified: Whether the ActionButtons should be justified in their container. diff --git a/plugins/ui/src/deephaven/ui/components/button_group.py b/plugins/ui/src/deephaven/ui/components/button_group.py index 7bd2087ac..e57b2b84a 100644 --- a/plugins/ui/src/deephaven/ui/components/button_group.py +++ b/plugins/ui/src/deephaven/ui/components/button_group.py @@ -91,6 +91,12 @@ def button_group( margin_end: The margin for the logical end side of the element, depending on layout direction. margin_x: The margin for the left and right sides of the element. margin_y: The margin for the top and bottom sides 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 position. top: The top position of the element. bottom: The bottom position of the element. @@ -104,6 +110,10 @@ def button_group( UNSAFE_class_name: Set the CSS className for the element. Only use as a last resort. Use style props instead. UNSAFE_style: Set the inline style for the element. Only use as a last resort. Use style props instead. key: A unique identifier used by React to render elements in a list. + + Returns: + The rendered button group element. + """ return component_element( "ButtonGroup", diff --git a/plugins/ui/src/deephaven/ui/components/checkbox_group.py b/plugins/ui/src/deephaven/ui/components/checkbox_group.py new file mode 100644 index 000000000..073328090 --- /dev/null +++ b/plugins/ui/src/deephaven/ui/components/checkbox_group.py @@ -0,0 +1,238 @@ +from __future__ import annotations +from typing import Any, Callable + + +from .types import ( + Orientation, + AlignSelf, + CSSProperties, + DimensionValue, + JustifySelf, + LayoutFlex, + Position, + ValidationBehavior, + FocusEventCallable, +) +from .basic import component_element +from ..elements import Element +from ..types import Key, Selection + + +def checkbox_group( + *children: Any, + orientation: Orientation = "vertical", + is_emphasized: bool | None = None, + value: Selection | None = None, + default_value: Selection | None = None, + is_disabled: bool | None = None, + is_read_only: bool | None = None, + name: str | None = None, + label: Any | None = None, + description: Any | None = None, + error_message: Any | None = None, + is_required: bool | None = None, + is_invalid: bool | None = None, + validation_behavior: ValidationBehavior | None = "aria", + label_position: str | None = None, + label_align: str | None = None, + necessity_indicator: str | None = None, + contextual_help: Any | None = None, + show_error_icon: bool | None = None, + on_change: Callable[[Key], None] | None = None, + on_focus: FocusEventCallable | None = None, + on_blur: FocusEventCallable | None = None, + on_focus_change: Callable[[bool], 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_column: str | None = None, + grid_column_start: str | None = None, + grid_column_end: str | None = None, + grid_row_start: str | None = None, + grid_row_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, + left: DimensionValue | None = None, + right: DimensionValue | None = None, + start: DimensionValue | None = None, + end: 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, + aria_errormessage: str | None = None, + UNSAFE_class_name: str | None = None, + UNSAFE_style: CSSProperties | None = None, +) -> Element: + """ + A grouping of checkbox's that are related to each other. + + Args: + + *children: The children of the checkbox group. + orientation: The axis the CheckboxGroup should align with. + is_emphasized: Whether the checkbox's should be displayed with emphasized style. + value: The selected checkbox within the checkbox group (controlled). + default_value: The default selected checkbox within the checkbox group (uncontrolled). + is_disabled: Whether the checkbox group is disabled. + is_read_only: Whether the checkbox group is read only. + name: The name of the input element, used when submitting an HTML form. + label: The label of the checkbox group. + description: A description for the checkbox group. Provides a hint such as specific requirements for what to choose. + error_message: An error message to be displayed when the checkbox group is an errored state. + is_required: Whether user input is required on the input before form submission. + is_invalid: Whether the checkbox group is in an invalid state. + 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. + 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. + show_error_icon: Whether an error icon is rendered. + on_change: Handler that is called when the selection 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. + 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 the element will grow to fit the space available. + flex_shrink: When used in a flex layout, specifies how the element will shrink to fit the space available. + flex_basis: When used in a flex layout, specifies the initial main size of the element. + align_self: Overrides the alignItems property of a flex or grid container. + justify_self: Species 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: When used in a grid layout specifies, specifies the named grid area that the element should be placed in within the grid. + grid_row: When used in a grid layout, specifies the row the element should be placed in within the grid. + grid_column: When used in a grid layout, specifies the column the element should be placed in within the grid. + grid_row_start: When used in a grid layout, specifies the starting row to span within the grid. + grid_row_end: When used in a grid layout, specifies the ending row to span within the grid. + grid_column_start: When used in a grid layout, specifies the starting column to span within the grid. + grid_column_end: When used in a grid layout, specifies the ending column to span within the grid. + margin: The margin for all four sides of the element. + margin_top: The margin for the top side of the element. + margin_bottom: The margin for the bottom side of the element. + margin_start: The margin for the logical start side of the element, depending on layout direction. + margin_end: The margin for the logical end side of the element, depending on layout direction. + margin_x: The margin for the left and right sides of the element. + margin_y: The margin for the top and bottom sides 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 position. + top: The top position of the element. + bottom: The bottom position of the element. + left: The left position of the element. + right: The right position of the element. + start: The logical start position of the element, depending on layout direction. + end: The logical end position of the element, depending on layout direction. + z_index: The stacking order for the element + is_hidden: Hides the element. + id: The unique identifier of the element. + aria_label: Defines a string value that labels the current element. + aria_labelledby: Identifies the element (or elements) that labels the current element. + aria_describedby: Identifies the element (or elements) that describes the object. + aria_details: Identifies the element (or elements) that provide a detailed, extended description for the object. + aria_errormessage: Identifies the element that provides an error message for the object. + UNSAFE_class_name: Set the CSS className for the element. Only use as a last resort. Use style props instead. + UNSAFE_style: Set the inline style for the element. Only use as a last resort. Use style props instead. + + Returns: + The rendered checkbox group element. + + """ + return component_element( + "CheckboxGroup", + children=children, + orientation=orientation, + is_emphasized=is_emphasized, + value=value, + default_value=default_value, + is_disabled=is_disabled, + is_read_only=is_read_only, + name=name, + label=label, + description=description, + error_message=error_message, + is_required=is_required, + is_invalid=is_invalid, + validation_behavior=validation_behavior, + label_position=label_position, + label_align=label_align, + necessity_indicator=necessity_indicator, + contextual_help=contextual_help, + show_error_icon=show_error_icon, + on_change=on_change, + on_focus=on_focus, + on_blur=on_blur, + on_focus_change=on_focus_change, + flex=flex, + flex_grow=flex_grow, + flex_shrink=flex_shrink, + flex_basis=flex_basis, + align_self=align_self, + justify_self=justify_self, + order=order, + grid_area=grid_area, + grid_row=grid_row, + grid_column=grid_column, + grid_column_start=grid_column_start, + grid_column_end=grid_column_end, + grid_row_start=grid_row_start, + grid_row_end=grid_row_end, + margin=margin, + margin_top=margin_top, + margin_bottom=margin_bottom, + margin_start=margin_start, + margin_end=margin_end, + margin_x=margin_x, + margin_y=margin_y, + width=width, + height=height, + min_width=min_width, + min_height=min_height, + max_width=max_width, + max_height=max_height, + position=position, + top=top, + bottom=bottom, + left=left, + right=right, + start=start, + end=end, + z_index=z_index, + is_hidden=is_hidden, + id=id, + aria_label=aria_label, + aria_labelledby=aria_labelledby, + aria_describedby=aria_describedby, + aria_details=aria_details, + aria_errormessage=aria_errormessage, + UNSAFE_class_name=UNSAFE_class_name, + UNSAFE_style=UNSAFE_style, + ) diff --git a/plugins/ui/src/deephaven/ui/components/list_view.py b/plugins/ui/src/deephaven/ui/components/list_view.py index 365a40385..a7956ed00 100644 --- a/plugins/ui/src/deephaven/ui/components/list_view.py +++ b/plugins/ui/src/deephaven/ui/components/list_view.py @@ -9,7 +9,24 @@ from .._internal.utils import create_props, unpack_item_table_source from .basic import component_element from .item import Item -from ..types import ListViewDensity, Selection, SelectionMode +from ..types import ( + ListViewDensity, + ListViewOverflowMode, + Selection, + SelectionMode, + SelectionStyle, + Key, +) +from .types import ( + LoadingState, + DisabledBehavior, + LayoutFlex, + DimensionValue, + AlignSelf, + JustifySelf, + Position, + CSSProperties, +) ListViewElement = Element @@ -25,14 +42,64 @@ def list_view( *children: Item | Table | ItemTableSource, density: ListViewDensity | None = "COMPACT", - default_selected_keys: Selection | None = None, - selected_keys: Selection | None = None, - selection_mode: SelectionMode | None = "MULTIPLE", + is_quiet: bool | None = None, + loading_state: LoadingState | None = None, + overflow_mode: ListViewOverflowMode = "truncate", render_empty_state: Element | None = None, + disabled_behavior: DisabledBehavior | None = None, + disabled_keys: Selection | None = None, + selection_mode: SelectionMode | None = "MULTIPLE", + disallow_empty_selection: bool | None = None, + selected_keys: Selection | None = None, + default_selected_keys: Selection | None = None, + selection_style: SelectionStyle | None = None, + on_action: Callable[[Key, str], None] | None = None, on_selection_change: Callable[[Selection], None] | None = None, on_change: Callable[[Selection], 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_column: str | None = None, + grid_row_start: str | None = None, + grid_row_end: 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, + left: DimensionValue | None = None, + right: DimensionValue | None = None, + start: DimensionValue | None = None, + end: 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, - **props: Any, ) -> ListViewElement: """ A list view that can be used to create a list of items. Children should be one of three types: @@ -47,25 +114,65 @@ def list_view( Args: *children: The options to render within the list_view. - density: - Sets the amount of vertical padding within each cell. - default_selected_keys: - The initial selected keys in the collection (uncontrolled). - selected_keys: - The currently selected keys in the collection (controlled). - selection_mode: - By default `"MULTIPLE"`, which allows multiple selection. - May also be `"SINGLE"` to allow only single selection, or `"None"`/`None` to allow no selection. - render_empty_state: - Sets what the `list_view` should render when there is no content to display. - on_selection_change: - Handler that is called when the selection changes. - on_change: - Alias of `on_selection_change`. Handler that is called when the selection changes. - key: - A unique identifier used by React to render elements in a list. - **props: - Any other ListView prop, except items, dragAndDropHooks, and onLoadMore. + density: Sets the amount of vertical padding within each cell. + is_quiet: Whether the ListView should use the quiet style. + loading_state: The loading state of the ListView. Determines whether to show a loading spinner. + overflow_mode: The behaviour of the text when it overflows the cell. + render_empty_state: Sets what the `list_view` should render when there is no content to display. + disabled_behavior: Whether disabled_keys applies to all interactions or just selection. + disabled_keys: The keys that should be disabled. These cannot be selected, focused, or interacted with + selection_mode: By default `"MULTIPLE"`, which allows multiple selection. May also be `"SINGLE"` to allow only single selection, or `"None"`/`None` to allow no selection. + disallow_empty_selection: Whether the ListView should disallow empty selection. + selected_keys: The currently selected keys in the collection (controlled). + default_selected_keys: The initial selected keys in the collection (uncontrolled). + selection_style: How the selection should be displayed. + on_action: Handler that is called when the user performs an action on an item. The user event depends on the collection's selection_style and interaction modality. + on_change: Handler that is called when the selection changes. + on_selection_change: Deprecated. Use on_change instead.Handler that is called when the selection changes. + 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 the element will grow to fit the space available. + flex_shrink: When used in a flex layout, specifies how the element will shrink to fit the space available. + flex_basis: When used in a flex layout, specifies the initial main size of the element. + align_self: Overrides the alignItems property of a flex or grid container. + justify_self: Species 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: When used in a grid layout, specifies the named grid area that the element should be placed in within the grid. + grid_row: When used in a grid layout, specifies the row the element should be placed in within the grid. + grid_column: When used in a grid layout, specifies the column the element should be placed in within the grid. + grid_row_start: When used in a grid layout, specifies the starting row to span within the grid. + grid_row_end: When used in a grid layout, specifies the ending row to span within the grid. + grid_column_start: When used in a grid layout, specifies the starting column to span within the grid. + grid_column_end: When used in a grid layout, specifies the ending column to span within the grid. + margin: The margin for all four sides of the element. + margin_top: The margin for the top side of the element. + margin_bottom: The margin for the bottom side of the element. + margin_start: The margin for the logical start side of the element, depending on layout direction. + margin_end: The margin for the logical end side of the element, depending on layout direction. + margin_x: The margin for the left and right sides of the element. + margin_y: The margin for the top and bottom sides 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 top position of the element. + bottom: The bottom position of the element. + left: The left position of the element. + right: The right position of the element. + start: The logical start position of the element, depending on layout direction. + end: The logical end position of the element, depending on layout direction. + z_index: The stacking order for the element + is_hidden: Hides the element. + id: The unique identifier of the element. + aria_label: Defines a string value that labels the current element. + aria_labelledby: Identifies the element (or elements) that labels the current element. + aria_describedby: Identifies the element (or elements) that describes the object. + aria_details: Identifies the element (or elements) that provide a detailed, extended description for the object. + UNSAFE_class_name: Set the CSS className for the element. Only use as a last resort. Use style props instead. + UNSAFE_style: Set the inline style for the element. Only use as a last resort. Use style props instead. + key: A unique identifier used by React to render elements in a list. Returns: The rendered ListView. diff --git a/plugins/ui/src/deephaven/ui/components/number_field.py b/plugins/ui/src/deephaven/ui/components/number_field.py index 9804545ca..7cd867a2a 100644 --- a/plugins/ui/src/deephaven/ui/components/number_field.py +++ b/plugins/ui/src/deephaven/ui/components/number_field.py @@ -1,13 +1,10 @@ from __future__ import annotations from typing import Any, Callable from .types import ( - # Events FocusEventCallable, KeyboardEventCallable, - # Validation TextFieldValidationState, NecessityIndicator, - # Layout AlignSelf, CSSProperties, DimensionValue, @@ -15,6 +12,7 @@ LayoutFlex, Position, LabelPosition, + NumberFieldFormatOptions, Alignment, ) from .basic import component_element @@ -27,7 +25,7 @@ def number_field( decrement_aria_label: str | None = None, increment_aria_label: str | None = None, is_wheel_disabled: bool | None = None, - # format_options, # omitted because need to connect it to Deephaven formatting options as well + format_options: NumberFieldFormatOptions | None = None, is_disabled: bool | None = None, is_read_only: bool | None = None, is_required: bool | None = None, @@ -189,6 +187,7 @@ def number_field( decrement_aria_label=decrement_aria_label, increment_aria_label=increment_aria_label, is_wheel_disabled=is_wheel_disabled, + format_options=format_options, is_disabled=is_disabled, is_read_only=is_read_only, is_required=is_required, diff --git a/plugins/ui/src/deephaven/ui/components/range_calendar.py b/plugins/ui/src/deephaven/ui/components/range_calendar.py new file mode 100644 index 000000000..a3082fa83 --- /dev/null +++ b/plugins/ui/src/deephaven/ui/components/range_calendar.py @@ -0,0 +1,214 @@ +from __future__ import annotations + +from typing import Any, Callable + +from .types import ( + LayoutFlex, + DimensionValue, + AlignSelf, + JustifySelf, + Position, + AriaPressed, + CSSProperties, + PageBehavior, +) + +from ..elements import Element +from .._internal.utils import create_props, convert_date_props, wrap_local_date_callable +from ..types import Date, LocalDateConvertible, DateRange +from .basic import component_element +from .make_component import make_component +from deephaven.time import dh_now + +RangeCalendarElement = Element + +# All the props that can be date types +_SIMPLE_DATE_PROPS = { + "focused_value", + "default_focused_value", + "min_value", + "max_value", +} +_RANGE_DATE_PROPS = {"value", "default_value"} +_CALLABLE_DATE_PROPS = {"on_change"} +_ON_FOCUS_CHANGE_KEY = "on_focus_change" + +# The priority of the date props to determine the format of the date passed to the callable date props +_DATE_PROPS_PRIORITY = [ + "value", + "default_value", + "focused_value", + "default_focused_value", +] + + +def _convert_range_calendar_props( + props: dict[str, Any], +) -> dict[str, Any]: + """ + Convert range_calendar props to Java date types. + + Args: + props: The props passed to the range_calendar. + + Returns: + The converted props. + """ + + convert_date_props( + props, + _SIMPLE_DATE_PROPS, + _RANGE_DATE_PROPS, + _CALLABLE_DATE_PROPS, + _DATE_PROPS_PRIORITY, + ) + + if props.get(_ON_FOCUS_CHANGE_KEY) is not None: + props[_ON_FOCUS_CHANGE_KEY] = wrap_local_date_callable( + props[_ON_FOCUS_CHANGE_KEY] + ) + + return props + + +@make_component +def range_calendar( + value: DateRange | None = None, + default_value: DateRange | None = None, + focused_value: Date | None = None, + default_focused_value: Date | None = None, + min_value: Date | None = None, + max_value: Date | None = None, + page_behavior: PageBehavior | None = None, + is_invalid: bool | None = None, + is_disabled: bool | None = None, + is_read_only: bool | None = None, + auto_focus: bool | None = None, + error_message: Element | None = None, + visible_months: int | None = None, + on_focus_change: Callable[[LocalDateConvertible], None] | None = None, + on_change: Callable[[DateRange], 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_pressed: AriaPressed | None = None, + aria_details: str | None = None, + UNSAFE_class_name: str | None = None, + UNSAFE_style: CSSProperties | None = None, + key: str | None = None, +) -> RangeCalendarElement: + """ + A range_calendar allows the user to select a range of dates. + + + Args: + value: The current value (controlled). + default_value: The default value (uncontrolled). + focused_value: Controls the currently focused date within the range_calendar. + default_focused_value: The date that is focused when the range_calendar first mounts (uncountrolled). + min_value: The minimum allowed date that a user may select. + max_value: The maximum allowed date that a user may select. + page_behavior: Controls the behavior of paging. Pagination either works by + advancing the visible page by visibleDuration (default) + or one unit of visibleDuration. + is_invalid: Whether the current selection is invalid according to application logic. + is_disabled: Whether the input is disabled. + is_read_only: Whether the input can be selected but not changed by the user. + auto_focus: Whether the element should receive focus on render. + error_message: An error message for the field. + visible_months: The number of months to display at once. Up to 3 months are supported. + on_focus_change: Function called when the focus state changes. + on_change: Handler that is called when the value changes. + The exact `Date` type will be the same as the type passed to + `value`, `default_value`, `focused_value`, or `default_focused_value` in that order of precedence. + 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_pressed: Whether the element is pressed. + 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 range_calendar element. + """ + _, props = create_props(locals()) + + _convert_range_calendar_props(props) + + return component_element("RangeCalendar", **props) diff --git a/plugins/ui/src/deephaven/ui/components/toggle_button.py b/plugins/ui/src/deephaven/ui/components/toggle_button.py index ebd49984c..d9f1cafc2 100644 --- a/plugins/ui/src/deephaven/ui/components/toggle_button.py +++ b/plugins/ui/src/deephaven/ui/components/toggle_button.py @@ -167,6 +167,10 @@ def toggle_button( 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 toggle button element. + """ return component_element( diff --git a/plugins/ui/src/deephaven/ui/components/types/Intl/number_field.py b/plugins/ui/src/deephaven/ui/components/types/Intl/number_field.py new file mode 100644 index 000000000..cb9849cf3 --- /dev/null +++ b/plugins/ui/src/deephaven/ui/components/types/Intl/number_field.py @@ -0,0 +1,501 @@ +from typing import Literal, Union +import sys + +if sys.version_info < (3, 11): + from typing_extensions import TypedDict, NotRequired +else: + from typing import TypedDict, NotRequired + + +NumberSystems = Literal[ + "adlm", + "ahom", + "arab", + "arabext", + "armn", + "armnlow", + "bali", + "beng", + "bhks", + "brah", + "cakm", + "cham", + "cyrl", + "deva", + "ethi", + "finance", + "fullwide", + "geor", + "gong", + "gonm", + "grek", + "greklow", + "gujr", + "guru", + "hanidays", + "hanidec", + "hans", + "hansfin", + "hant", + "hantfin", + "hebr", + "hmng", + "hmnp", + "java", + "jpan", + "jpanfin", + "jpanyear", + "kali", + "khmr", + "knda", + "lana", + "lanatham", + "laoo", + "latn", + "lepc", + "limb", + "mathbold", + "mathdbl", + "mathmono", + "mathsanb", + "mathsans", + "mlym", + "modi", + "mong", + "mroo", + "mtei", + "mymr", + "mymrshan", + "mymrtlng", + "native", + "newa", + "nkoo", + "olck", + "orya", + "osma", + "rohg", + "roman", + "romanlow", + "saur", + "shrd", + "sind", + "sinh", + "sora", + "sund", + "takr", + "talu", + "taml", + "tamldec", + "telu", + "thai", + "tirh", + "tibt", + "traditio", + "vaii", + "wara", + "wcho", +] + +CurrencyCodes = Literal[ + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BRL", + "BSD", + "BTN", + "BWP", + "BYN", + "BZD", + "CAD", + "CDF", + "CHF", + "CLP", + "CNY", + "COP", + "CRC", + "CUC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "FOK", + "GBP", + "GEL", + "GGP", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "IMP", + "INR", + "IQD", + "IRR", + "ISK", + "JEP", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KID", + "KMF", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRU", + "MUR", + "MVR", + "MWK", + "MXN", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLL", + "SOS", + "SRD", + "SSP", + "STN", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TVD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "UYU", + "UZS", + "VES", + "VND", + "VUV", + "WST", + "XAF", + "XCD", + "XOF", + "XPF", + "YER", + "ZAR", + "ZMW", + "ZWL", +] + +Units = Literal[ + "acre", + "bit", + "byte", + "celsius", + "centimeter", + "day", + "degree", + "fahrenheit", + "fluid-ounce", + "foot", + "gallon", + "gigabit", + "gigabyte", + "gram", + "hectare", + "hour", + "inch", + "kilobit", + "kilobyte", + "kilogram", + "kilometer", + "liter", + "megabit", + "megabyte", + "meter", + "mile", + "mile-scandinavian", + "millimeter", + "milliliter", + "millisecond", + "minute", + "month", + "ounce", + "percent", + "petabyte", + "pound", + "second", + "stone", + "terabit", + "terabyte", + "week", + "yard", + "year", +] + + +class Options(TypedDict): + """ + Options for formatting the value of a NumberField. + This also affects the characters allowed in the input. + """ + + # Locale Options + locale_matcher: NotRequired[Literal["lookup", "best fit"]] + """ + The locale matching algorithm to use. + Possible values are "lookup" to use the runtime's locale matching algorithm, or "best fit" to use the CLDR locale matching algorithm. + The default is "best fit". + """ + + numbering_matching: NotRequired[NumberSystems] + """ + The numbering system to use. + Possible values are the numbering systems specified in the Unicode CLDR, such as "arab" for Arabic-Indic digits or "latn" for Latin digits. + For more information see the supported numbering systems in the getNumberingSystems page on MDN. + + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getNumberingSystems + """ + + # Style Options + style: NotRequired[Literal["decimal", "percent", "currency", "unit"]] + """ + The formatting style to use. + Possible values are "decimal" for plain number formatting, "percent" for percent formatting, "currency" for currency formatting, or "unit" for unit formatting. + """ + + currency: NotRequired[CurrencyCodes] + """ + The currency to use in currency formatting. + Possible values are the ISO 4217 currency codes, such as "USD" for the US dollar, "EUR" for the euro, or "CNY" for the Chinese yuan. + The default is "USD". + """ + + currency_display: NotRequired[Literal["symbol", "narrowSymbol", "code", "name"]] + """ + How to display the currency in currency formatting. + Possible values are "symbol" to use a localized currency symbol such as €, "narrowSymbol" to use a narrow form of the symbol such as ƒ, "code" to use the ISO currency code, or "name" to use a localized currency name such as "dollar". + """ + + currency_sign: NotRequired[Literal["standard", "accounting"]] + """ + Determines how to display negative values in currency formatting. + In many locales, the "accounting" format wraps negative numbers in parentheses instead of using a minus sign. + Possible values are "standard" and "accounting". The default is "standard". + """ + + unit: NotRequired[Units] + """ + The unit to use in unit formatting. + Possible values are the units specified in the Unicode CLDR. Only a subset of units was selected from the full list such as "meter" for meters or "mile" for miles. + Check https://tc39.es/ecma402/#table-sanctioned-single-unit-identifiers for the full list of supported units. + """ + + unit_display: NotRequired[Literal["long", "short", "narrow"]] + """ + How to display the unit in unit formatting. + Possible values are "long" to use a full unit name such as "16 meters", "short" to use an abbreviated unit name such as "16 m", or "narrow" to use a narrow form of the unit name such as "16m". + Default is "short". + """ + + # Digit Options + minimum_integer_digits: NotRequired[int] + """ + The minimum number of integer digits to use. + Possible values are from 1 to 21. + The default is 1. + """ + + minimum_fraction_digits: NotRequired[int] + """ + The minimum number of fraction digits to use. + Possible values are from 0 to 100; the default for plain number and percent formatting is 0; the default for currency formatting is 2 if not provided in the ISO 4217 currency code list. + """ + + maximum_fraction_digits: NotRequired[int] + """ + The maximum number of fraction digits to use. + Possible values are from 0 to 100. + Plain number formatting: The default is the larger of minimum_fraction_digits and 3. + Percent formatting: The default is the larger of minimum_fraction_digits and 0. + Currency formatting: The default is the larger of minimum_fraction_digits and the number of minor unit digits in the ISO 4217 list. + """ + + minimum_significant_digits: NotRequired[int] + """ + The minimum number of significant digits to use. + Possible values are from 1 to 21. + Default is 1. + """ + + maximum_significant_digits: NotRequired[int] + """ + The maximum number of significant digits to use. + Possible values are from 1 to 21. + Default is 21. + """ + + rounding_priority: NotRequired[Literal["auto", "morePrecision", "lessPrecision"]] + """ + Specifies how rounding conflicts is resolved if both fraction_digits and significant_digits are provided. + Possible values are "auto" to use the significant digits property, "morePrecision" to prioritize rounding to a more precise number, or "lessPrecision" to prioritize rounding to a less precise number. + The default is "auto". + """ + + rounding_increment: NotRequired[ + Literal[1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 200, 2500, 5000] + ] + """ + The rounding increment to use. + Possible values are 1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, or 5000. + Cannot be mixed with significant-digits rounding or any setting of rounding_priority other than auto. + """ + + rounding_mode: NotRequired[ + Literal[ + "ceil", + "floor", + "expand", + "trunc", + "halfCeil", + "halfFloor", + "halfExpand", + "halfTrunc", + "halfEven", + ] + ] + """ + The rounding mode to use. + Possible values are "ceil" to round towards positive infinity, "floor" to round towards negative infinity, "expand" to round away from zero, "trunc" to round towards zero, "halfCeil" to round towards the nearest neighbor or to positive infinity if equidistant, "halfFloor" to round towards the nearest neighbor or to negative infinity if equidistant, "halfExpand" to round towards the nearest neighbor or to infinity if equidistant, "halfTrunc" to round towards the nearest neighbor or to zero if equidistant, or "halfEven" to round towards the nearest neighbor or to the even neighbor if equidistant. + The default is "halfExpand". + """ + + trailing_zero_display: NotRequired[Literal["auto", "stripIfInteger"]] + """ + How to display trailing zeros in fraction digits. + Possible values are "auto" to display trailing zeros according to minimum_fraction_digits and minimum_significant_digits, or "stripIfInteger" to remove trailing zeros in fraction digits if they are all zero. + The default is "auto". + """ + + # Other Options + notation: NotRequired[Literal["standard", "scientific", "engineering", "compact"]] + """ + Possible values are "standard" for plain number formatting, "scientific" for order-of-magnitude for formatted number, "engineering" for the exponent of ten when divisible by three, or "compact" for string representing exponent; defaults to using the "short" form. + Default is "standard". + """ + + compact_display: NotRequired[Literal["short", "long"]] + """ + Possible values are "short" for compact notation, or "long" for full notation. + Default is "short". + """ + + use_grouping: NotRequired[Literal["always", "auto", "min2", True, False]] + """ + Whether to use grouping separators such as thousands separators. + Possible values are "always" to always use grouping separators, "auto" to use grouping separators according to the locale and the number of digits, "min2" to use grouping separators if there are more than two digits, or true and false to use grouping separators according to the locale. + Default is 'min2' if notation is 'compact', and 'auto' otherwise. + """ + + sign_display: NotRequired[ + Literal["auto", "never", "always", "exceptZero", "negative"] + ] + """ + Whether to display the sign of the number. + Possible values are "auto" to display the sign for negative numbers only, "never" to never display the sign, "always" to always display the sign, "exceptZero" to display the sign for all numbers except zero, or "negative" to display the sign for negative numbers only. + Default is "auto". + """ + + +class NumberFieldFormatOptions(TypedDict): + """ + Options for formatting the value of a NumberField. + This also affects the characters allowed in the input. + """ + + numbering_system: NotRequired[str] + + compact_display: NotRequired[Literal["short", "long"]] + + notation: NotRequired[Literal["standard", "scientific", "engineering", "compact"]] + + sign_display: NotRequired[Literal["auto", "never", "always", "exceptZero"]] + + unit: NotRequired[str] + + unit_display: NotRequired[Literal["long", "short", "narrow"]] + + currency_sign: NotRequired[Literal["standard", "accounting"]] diff --git a/plugins/ui/src/deephaven/ui/components/types/__init__.py b/plugins/ui/src/deephaven/ui/components/types/__init__.py index 4208d7bf6..12557f66e 100644 --- a/plugins/ui/src/deephaven/ui/components/types/__init__.py +++ b/plugins/ui/src/deephaven/ui/components/types/__init__.py @@ -6,3 +6,4 @@ from .progress import * from .validate import * from .icon_types import * +from .Intl.number_field import * diff --git a/plugins/ui/src/deephaven/ui/components/types/layout.py b/plugins/ui/src/deephaven/ui/components/types/layout.py index dfccfa931..68cac2a9d 100644 --- a/plugins/ui/src/deephaven/ui/components/types/layout.py +++ b/plugins/ui/src/deephaven/ui/components/types/layout.py @@ -129,7 +129,6 @@ Overflow = Union[Literal["visible", "hidden", "clip", "scroll", "auto"], str] OverflowMode = Literal["wrap", "collapse"] - Alignment = Literal["start", "end"] ButtonGroupAlignment = Literal["start", "center", "end"] diff --git a/plugins/ui/src/deephaven/ui/components/types/validate.py b/plugins/ui/src/deephaven/ui/components/types/validate.py index 81687390d..a95ff39d9 100644 --- a/plugins/ui/src/deephaven/ui/components/types/validate.py +++ b/plugins/ui/src/deephaven/ui/components/types/validate.py @@ -22,3 +22,5 @@ AutoCompleteModes = Literal["on", "off"] AutoCapitalizeModes = Literal["off", "none", "on", "sentences", "words", "characters"] + +DisabledBehavior = Literal["selection", "all"] diff --git a/plugins/ui/src/deephaven/ui/types/types.py b/plugins/ui/src/deephaven/ui/types/types.py index d0fa28513..6e1f2ccb1 100644 --- a/plugins/ui/src/deephaven/ui/types/types.py +++ b/plugins/ui/src/deephaven/ui/types/types.py @@ -463,6 +463,7 @@ class SliderChange(TypedDict): TableData = Dict[ColumnName, ColumnData] SelectionArea = Literal["CELL", "ROW", "COLUMN"] SelectionMode = Literal["SINGLE", "MULTIPLE"] +SelectionStyle = Literal["checkbox", "highlight"] Sentinel = Any TransformedData = Any ActionMenuDirection = Literal["bottom", "top", "left", "right", "start", "end"] @@ -502,6 +503,7 @@ class SliderChange(TypedDict): ] Granularity = Literal["DAY", "HOUR", "MINUTE", "SECOND"] ListViewDensity = Literal["COMPACT", "NORMAL", "SPACIOUS"] +ListViewOverflowMode = Literal["truncate", "wrap"] ActionGroupDensity = Literal["compact", "regular"] TabDensity = Literal["compact", "regular"] Dependencies = Union[Tuple[Any], List[Any]] diff --git a/plugins/ui/src/js/src/elements/RangeCalendar.tsx b/plugins/ui/src/js/src/elements/RangeCalendar.tsx new file mode 100644 index 000000000..6289a5f65 --- /dev/null +++ b/plugins/ui/src/js/src/elements/RangeCalendar.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { + RangeCalendar as DHCRangeCalendar, + RangeCalendarProps as DHCRangeCalendarProps, +} from '@deephaven/components'; +import { DateValue } from '@internationalized/date'; +import { + SerializedRangeCalendarProps, + useRangeCalendarProps, +} from './hooks/useRangeCalendarProps'; +import { RangeValue } from './hooks'; +import useDebouncedOnChange from './hooks/useDebouncedOnChange'; + +const EMPTY_FUNCTION = () => undefined; + +export function RangeCalendar( + props: SerializedRangeCalendarProps> +): JSX.Element { + const rangeCalendarProps = useRangeCalendarProps(props); + const { + value: propValue, + onChange: propOnChange = EMPTY_FUNCTION, + ...otherProps + } = rangeCalendarProps; + + const [value, onChange] = useDebouncedOnChange< + RangeValue | undefined | null + >(propValue, propOnChange); + + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +} + +RangeCalendar.displayName = 'RangeCalendar'; + +export default RangeCalendar; diff --git a/plugins/ui/src/js/src/elements/hooks/useDateComponentProps.ts b/plugins/ui/src/js/src/elements/hooks/useDateComponentProps.ts index e372c708d..8357a3dfa 100644 --- a/plugins/ui/src/js/src/elements/hooks/useDateComponentProps.ts +++ b/plugins/ui/src/js/src/elements/hooks/useDateComponentProps.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from 'react'; -import { DateValue, CalendarDateTime } from '@internationalized/date'; +import { DateValue } from '@internationalized/date'; import { DeserializedFocusEventCallback, SerializedFocusEventCallback, @@ -16,6 +16,7 @@ import { Granularity, parseDateValue, parseNullableDateValue, + dateValuetoIsoString, } from '../utils/DateTimeUtils'; export type SerializedDateValue = string | null; @@ -123,12 +124,7 @@ export function serializeDateValue( return null; } - if (value instanceof CalendarDateTime) { - // Use Instance for CalendarDateTime - return `${value.toString()}Z`; - } - - return value.toString(); + return dateValuetoIsoString(value); } /** diff --git a/plugins/ui/src/js/src/elements/hooks/useDateRangePickerProps.ts b/plugins/ui/src/js/src/elements/hooks/useDateRangePickerProps.ts index 996f3a9a8..fc84c1438 100644 --- a/plugins/ui/src/js/src/elements/hooks/useDateRangePickerProps.ts +++ b/plugins/ui/src/js/src/elements/hooks/useDateRangePickerProps.ts @@ -15,6 +15,7 @@ import { MappedDateValue, Granularity, parseDateValue, + dateValuetoIsoString, } from '../utils/DateTimeUtils'; export interface RangeValue { @@ -30,7 +31,9 @@ export type SerializedDateRangeValueCallback = ( export type DeserializedDateRangeValueCallback = | (() => void) - | ((value: RangeValue> | null) => Promise); + | (( + value: RangeValue> | null | undefined + ) => Promise); export interface SerializedDateRangePickerPropsInterface { /** Handler that is called when the element receives focus. */ @@ -123,8 +126,8 @@ export function serializeDateRangeValue( return null; } - const start = value.start.toString(); - const end = value.end.toString(); + const start = dateValuetoIsoString(value.start); + const end = dateValuetoIsoString(value.end); return { start, end }; } diff --git a/plugins/ui/src/js/src/elements/hooks/useRangeCalendarProps.ts b/plugins/ui/src/js/src/elements/hooks/useRangeCalendarProps.ts new file mode 100644 index 000000000..78a731a5a --- /dev/null +++ b/plugins/ui/src/js/src/elements/hooks/useRangeCalendarProps.ts @@ -0,0 +1,176 @@ +import { useCallback, useMemo } from 'react'; +import { CalendarDate, DateValue } from '@internationalized/date'; +import { SerializedDateValueCallback } from './useDateComponentProps'; +import { parseCalendarValue } from '../utils/DateTimeUtils'; +import { DeserializedCalendarCallback } from './useCalendarProps'; +import { + RangeValue, + SerializedDateRangeValueCallback, + DeserializedDateRangeValueCallback, + useOnChangeDateRangeCallback, +} from './useDateRangePickerProps'; + +export interface SerializedRangeCalendarPropsInterface { + /** Handler that is called when the value changes */ + onChange?: SerializedDateRangeValueCallback; + + /** Handler that is called when the focused date changes. */ + onFocusChange?: SerializedDateValueCallback; + + /** The current value (controlled) */ + value?: RangeValue | null; + + /** The default value (uncontrolled) */ + defaultValue?: RangeValue | null; + + /** The minimum allowed date that a user may select */ + minValue?: string; + + /** The maximum allowed date that a user may select */ + maxValue?: string; + + /** Controls the currently focused date within the calendar. */ + focusedValue?: string; + + /** The date that is focused when the calendar first mounts (uncountrolled). */ + defaultFocusedValue?: string; +} + +export interface DeserializedRangeCalendarPropsInterface { + /** Handler that is called when the value changes */ + onChange?: DeserializedDateRangeValueCallback; + + /** Handler that is called when the focused date changes. */ + onFocusChange?: DeserializedCalendarCallback; + + /** The current value (controlled) */ + value?: RangeValue | null; + + /** The default value (uncontrolled) */ + defaultValue?: RangeValue | null; + + /** The minimum allowed date that a user may select */ + minValue?: DateValue; + + /** The maximum allowed date that a user may select */ + maxValue?: DateValue; + + /** Controls the currently focused date within the calendar. */ + focusedValue?: DateValue; + + /** The date that is focused when the calendar first mounts (uncountrolled). */ + defaultFocusedValue?: DateValue; +} + +export type SerializedRangeCalendarProps = TProps & + SerializedRangeCalendarPropsInterface; + +export type DeserializedRangeCalendarProps = Omit< + TProps, + keyof SerializedRangeCalendarPropsInterface +> & + DeserializedRangeCalendarPropsInterface; + +/** + * Get a callback function that can be passed to the onChange event handler + * props of a Spectrum Date component. + * @param callback Callback to be called with the serialized value + * @returns A callback to be passed into the Spectrum component that transforms + * the value and calls the provided callback + */ +export function useOnFocusedChangeCallback( + callback?: SerializedDateValueCallback +): DeserializedCalendarCallback { + return useCallback( + (value?: CalendarDate) => { + if (callback == null) { + return; + } + callback(value == null ? null : value.toString()); + }, + [callback] + ); +} + +/** + * Use memo to get a Calendar DateValue from a string. + * + * @param value the string date value + * @returns DateValue + */ +export default function useCalendarValueMemo( + value?: string +): DateValue | undefined { + return useMemo(() => parseCalendarValue(value), [value]); +} + +/** + * Use memo to get a Range of Calendar DateValue from a nullable string. + * + * @param value the string date value + * @returns Range of DateValue or null + */ +export function useNullableRangeCalendarValueMemo( + value?: RangeValue | null +): RangeValue | null | undefined { + return useMemo(() => parseNullableRangeCalendarValue(value), [value]); +} + +export function parseNullableRangeCalendarValue( + value?: RangeValue | null +): RangeValue | null | undefined { + if (value == null) { + return value; + } + + const start = parseCalendarValue(value.start); + const end = parseCalendarValue(value.end); + + if (start === undefined || end === undefined) { + return undefined; + } + + return { start, end }; +} + +/** + * Wrap Date component props with the appropriate serialized event callbacks. + * @param props Props to wrap + * @returns Wrapped props + */ +export function useRangeCalendarProps({ + onChange: serializedOnChange, + onFocusChange: serializedOnFocusChange, + value: serializedValue, + defaultValue: serializedDefaultValue, + minValue: serializedMinValue, + maxValue: serializedMaxValue, + focusedValue: serializedFocusedValue, + defaultFocusedValue: serializedDefaultFocusedValue, + ...otherProps +}: SerializedRangeCalendarProps): DeserializedRangeCalendarProps { + const onChange = useOnChangeDateRangeCallback(serializedOnChange); + const onFocusChange = useOnFocusedChangeCallback(serializedOnFocusChange); + const deserializedValue = useNullableRangeCalendarValueMemo(serializedValue); + const deserializedDefaultValue = useNullableRangeCalendarValueMemo( + serializedDefaultValue + ); + const deserializedMinValue = useCalendarValueMemo(serializedMinValue); + const deserializedMaxValue = useCalendarValueMemo(serializedMaxValue); + const deserializedFocusedValue = useCalendarValueMemo(serializedFocusedValue); + const deserializedDefaultFocusedValue = useCalendarValueMemo( + serializedDefaultFocusedValue + ); + + return { + onChange: serializedOnChange == null ? undefined : onChange, + onFocusChange: serializedOnFocusChange == null ? undefined : onFocusChange, + value: deserializedValue, + defaultValue: deserializedDefaultValue, + minValue: deserializedMinValue, + maxValue: deserializedMaxValue, + focusedValue: deserializedFocusedValue, + defaultFocusedValue: deserializedDefaultFocusedValue, + ...otherProps, + }; +} diff --git a/plugins/ui/src/js/src/elements/index.ts b/plugins/ui/src/js/src/elements/index.ts index ca11bf1fe..896341621 100644 --- a/plugins/ui/src/js/src/elements/index.ts +++ b/plugins/ui/src/js/src/elements/index.ts @@ -22,6 +22,7 @@ export * from './ProgressBar'; export * from './ProgressCircle'; export * from './Radio'; export * from './RadioGroup'; +export * from './RangeCalendar'; export * from './RangeSlider'; export * from './Slider'; export * from './Tabs'; diff --git a/plugins/ui/src/js/src/elements/model/ElementConstants.ts b/plugins/ui/src/js/src/elements/model/ElementConstants.ts index c83e16441..0b6f246f0 100644 --- a/plugins/ui/src/js/src/elements/model/ElementConstants.ts +++ b/plugins/ui/src/js/src/elements/model/ElementConstants.ts @@ -30,6 +30,7 @@ export const ELEMENT_NAME = { buttonGroup: uiComponentName('ButtonGroup'), calendar: uiComponentName('Calendar'), checkbox: uiComponentName('Checkbox'), + checkboxGroup: uiComponentName('CheckboxGroup'), comboBox: uiComponentName('ComboBox'), content: uiComponentName('Content'), contextualHelp: uiComponentName('ContextualHelp'), @@ -53,6 +54,7 @@ export const ELEMENT_NAME = { progressCircle: uiComponentName('ProgressCircle'), radio: uiComponentName('Radio'), radioGroup: uiComponentName('RadioGroup'), + rangeCalendar: uiComponentName('RangeCalendar'), rangeSlider: uiComponentName('RangeSlider'), section: uiComponentName('Section'), slider: uiComponentName('Slider'), diff --git a/plugins/ui/src/js/src/elements/utils/DateTimeUtils.test.ts b/plugins/ui/src/js/src/elements/utils/DateTimeUtils.test.ts index 6cc55f489..5c531f375 100644 --- a/plugins/ui/src/js/src/elements/utils/DateTimeUtils.test.ts +++ b/plugins/ui/src/js/src/elements/utils/DateTimeUtils.test.ts @@ -1,3 +1,8 @@ +import { + CalendarDate, + CalendarDateTime, + ZonedDateTime, +} from '@internationalized/date'; import { parseDateValue, parseNullableDateValue, @@ -6,6 +11,7 @@ import { parseNullableTimeValue, parseCalendarValue, parseNullableCalendarValue, + dateValuetoIsoString, } from './DateTimeUtils'; const DEFAULT_TIME_ZONE = 'UTC'; @@ -201,3 +207,22 @@ describe('parseCalendarValue', () => { expect(() => parseCalendarValue(invalidDate)).toThrow(); }); }); + +describe('dateValuetoIsoString', () => { + it('handles a CalendarDate', () => { + const date = new CalendarDate(2021, 3, 4); + expect(dateValuetoIsoString(date)).toEqual('2021-03-04'); + }); + + it('handles a CalendarDateTime', () => { + const date = new CalendarDateTime(2021, 3, 4, 5, 6, 7); + expect(dateValuetoIsoString(date)).toEqual('2021-03-04T05:06:07Z'); + }); + + it('handles a ZonedDateTime', () => { + const date = new ZonedDateTime(2021, 3, 4, 'America/New_York', 0, 0, 0, 0); + expect(dateValuetoIsoString(date)).toEqual( + '2021-03-04T00:00:00+00:00[America/New_York]' + ); + }); +}); diff --git a/plugins/ui/src/js/src/elements/utils/DateTimeUtils.ts b/plugins/ui/src/js/src/elements/utils/DateTimeUtils.ts index 9fe5f080e..9cf1891c2 100644 --- a/plugins/ui/src/js/src/elements/utils/DateTimeUtils.ts +++ b/plugins/ui/src/js/src/elements/utils/DateTimeUtils.ts @@ -254,3 +254,12 @@ export function parseNullableTimeValue( return parseTimeValue(timeZone, value); } + +export function dateValuetoIsoString(value: DateValue): string { + if (value instanceof CalendarDateTime) { + // Use Instance for CalendarDateTime + return `${value.toString()}Z`; + } + + return value.toString(); +} diff --git a/plugins/ui/src/js/src/widget/WidgetUtils.tsx b/plugins/ui/src/js/src/widget/WidgetUtils.tsx index 8858072e4..515d04ae9 100644 --- a/plugins/ui/src/js/src/widget/WidgetUtils.tsx +++ b/plugins/ui/src/js/src/widget/WidgetUtils.tsx @@ -8,6 +8,7 @@ import { ActionMenu, ButtonGroup, SpectrumCheckbox as Checkbox, + CheckboxGroup, Content, ContextualHelp, Heading, @@ -64,6 +65,7 @@ import { ProgressCircle, Radio, RadioGroup, + RangeCalendar, RangeSlider, Slider, TabPanels, @@ -109,6 +111,7 @@ export const elementComponentMap = { [ELEMENT_NAME.buttonGroup]: ButtonGroup, [ELEMENT_NAME.calendar]: Calendar, [ELEMENT_NAME.checkbox]: Checkbox, + [ELEMENT_NAME.checkboxGroup]: CheckboxGroup, [ELEMENT_NAME.comboBox]: ComboBox, [ELEMENT_NAME.content]: Content, [ELEMENT_NAME.contextualHelp]: ContextualHelp, @@ -132,6 +135,7 @@ export const elementComponentMap = { [ELEMENT_NAME.progressCircle]: ProgressCircle, [ELEMENT_NAME.radio]: Radio, [ELEMENT_NAME.radioGroup]: RadioGroup, + [ELEMENT_NAME.rangeCalendar]: RangeCalendar, [ELEMENT_NAME.rangeSlider]: RangeSlider, [ELEMENT_NAME.section]: Section, [ELEMENT_NAME.slider]: Slider, diff --git a/plugins/ui/test/deephaven/ui/test_range_calendar.py b/plugins/ui/test/deephaven/ui/test_range_calendar.py new file mode 100644 index 000000000..c2590e4c3 --- /dev/null +++ b/plugins/ui/test/deephaven/ui/test_range_calendar.py @@ -0,0 +1,98 @@ +import unittest + +from .BaseTest import BaseTestCase + + +class CalendarTest(BaseTestCase): + def test_convert_date_props(self): + from deephaven.time import to_j_instant, to_j_zdt, to_j_local_date + from deephaven.ui.components.range_calendar import _convert_range_calendar_props + from deephaven.ui._internal.utils import ( + get_jclass_name, + _convert_to_java_date, + ) + + def verify_is_local_date(dateStr): + self.assertEqual( + get_jclass_name(_convert_to_java_date(dateStr)), "java.time.LocalDate" + ) + + def verify_is_instant(dateStr): + self.assertEqual( + get_jclass_name(_convert_to_java_date(dateStr)), "java.time.Instant" + ) + + def verify_is_zdt(dateStr): + self.assertEqual( + get_jclass_name(_convert_to_java_date(dateStr)), + "java.time.ZonedDateTime", + ) + + def empty_on_change(): + pass + + props1 = { + "focused_value": "2021-01-01", + "value": {"start": "2021-01-01 UTC", "end": "2021-01-02 UTC"}, + "default_value": {"start": "2021-01-01 ET", "end": "2021-01-02 ET"}, + "min_value": to_j_zdt("2021-01-01 ET"), + "max_value": to_j_local_date("2021-01-01"), + } + + props2 = { + "value": { + "start": to_j_local_date("2021-01-01"), + "end": to_j_local_date("2021-01-02"), + }, + "default_value": { + "start": to_j_zdt("2021-01-01 ET"), + "end": to_j_zdt("2021-01-02 ET"), + }, + "focused_value": to_j_instant("2021-01-01 UTC"), + "on_change": verify_is_local_date, + } + + props3 = { + "default_value": { + "start": to_j_instant("2021-01-01 UTC"), + "end": to_j_instant("2021-01-02 UTC"), + }, + "focused_value": to_j_zdt("2021-01-01 ET"), + "on_change": verify_is_instant, + } + + props4 = { + "focused_value": to_j_zdt("2021-01-01 ET"), + "on_change": verify_is_zdt, + } + + props5 = {"on_change": verify_is_instant} + + props6 = {"on_change": empty_on_change} + + _convert_range_calendar_props(props1) + _convert_range_calendar_props(props2) + _convert_range_calendar_props(props3) + _convert_range_calendar_props(props4) + _convert_range_calendar_props(props5) + _convert_range_calendar_props(props6) + + verify_is_local_date(props1["max_value"]) + verify_is_zdt(props1["min_value"]) + verify_is_zdt(props1["value"]["start"]) + verify_is_zdt(props1["value"]["end"]) + verify_is_zdt(props1["default_value"]["start"]) + verify_is_zdt(props1["default_value"]["end"]) + verify_is_local_date(props1["focused_value"]) + + props2["on_change"]({"start": "2021-01-01", "end": "2021-01-02"}) + props3["on_change"]({"start": "2021-01-01 UTC", "end": "2021-01-02 UTC"}) + props4["on_change"]({"start": "2021-01-01 ET", "end": "2021-01-02 ET"}) + props5["on_change"]({"start": "2021-01-01 UTC", "end": "2021-01-02 UTC"}) + + # pass an Instant but it should be dropped with no error + props6["on_change"]({"start": "2021-01-01 UTC", "end": "2021-01-02 UTC"}) + + +if __name__ == "__main__": + unittest.main() diff --git a/sphinx_ext/deephaven_autodoc.py b/sphinx_ext/deephaven_autodoc.py index 4ed206e33..5d7abc703 100644 --- a/sphinx_ext/deephaven_autodoc.py +++ b/sphinx_ext/deephaven_autodoc.py @@ -33,6 +33,7 @@ class FunctionMetadata(TypedDict): name: str +# total is False to allow for popping some keys class SignatureData(TypedDict): parameters: Params return_description: str @@ -44,6 +45,8 @@ class SignatureData(TypedDict): SignatureValue = Union[str, Params] +AUTOFUNCTION_COMMENT_PREFIX = "AutofunctionCommentPrefix:" + def extract_parameter_defaults( node: sphinx.addnodes.desc_parameterlist, @@ -103,7 +106,7 @@ def extract_list_item(node: docutils.nodes.list_item) -> ParamData: """ field = node.astext().replace("\n", " ") try: - match = re.match(r"(.+) \((.*)\) -- (.+)", field) + match = re.match(r"(.+?) \((.*?)\) -- (.+)", field, re.DOTALL) if match is None: raise ValueError( f"Could not match {field} to extract param data. " @@ -154,7 +157,7 @@ def extract_field_body( # this is still a parameter, likely the only one in its signature return [extract_list_item(child)] elif is_paragraph: - return node.astext().replace("\n", " ") + return node.astext().replace("\n", "
") elif isinstance(child, docutils.nodes.bullet_list): return extract_list_items(child) raise ValueError( @@ -225,7 +228,7 @@ def extract_content_data( if isinstance(child_node, docutils.nodes.field_list): result.update(extract_field_list(child_node)) elif isinstance(child_node, docutils.nodes.paragraph): - result["description"] = child_node.astext().replace("\n", " ") + result["description"] = child_node.astext().replace("\n", "
") return result @@ -305,11 +308,19 @@ def to_mdx(node: sphinx.addnodes.desc) -> docutils.nodes.comment: dat = json.dumps(result) - param_table = f"" + return_description = result["return_description"] + return_type = result["return_type"] + + autofunction_markdown = ( + f"{AUTOFUNCTION_COMMENT_PREFIX}" + rf"{return_description}

" + rf"**Returns:** {return_type}

" + rf"" + ) # This is a little hacky, but this way the markdown renderer will not escape the special characters # such as * and \. The comment markers will be removed by make_docs.py. - return docutils.nodes.comment("", "", docutils.nodes.raw("", param_table)) + return docutils.nodes.comment("", "", docutils.nodes.raw("", autofunction_markdown)) class DeephavenAutodoc(AutodocDirective): diff --git a/sphinx_ext/make_docs_utilities.py b/sphinx_ext/make_docs_utilities.py index ec0c4b2e0..f15c5d760 100644 --- a/sphinx_ext/make_docs_utilities.py +++ b/sphinx_ext/make_docs_utilities.py @@ -4,6 +4,7 @@ import os import contextlib from typing import IO, Generator +from deephaven_autodoc import AUTOFUNCTION_COMMENT_PREFIX BUILT_DOCS = "docs/build/markdown" @@ -111,12 +112,15 @@ def remove_paramtable_comment( Returns: str: The line with the comment markers removed """ - if line.startswith("\n"): + if line.startswith(f"\n" + ): # remove the comment markers # these are added in deephaven_autodoc.py to prevent special characters from being escaped # by the markdown renderer - line = line.replace("", "") + line = line.replace("
", "\n") return line diff --git a/tests/app.d/ui_render_all.py b/tests/app.d/ui_render_all.py index 4b57c28ee..f0b35f5d2 100644 --- a/tests/app.d/ui_render_all.py +++ b/tests/app.d/ui_render_all.py @@ -105,6 +105,9 @@ def ui_components2(): label="Radio Group", orientation="HORIZONTAL", ), + ui.range_calendar( + default_value={"start": "2021-01-01", "end": "2021-01-02"}, + ), ui.range_slider(default_value={"start": 10, "end": 99}, label="Range Slider"), ui.row("Row child A", "Row child B"), ui.slider( diff --git a/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-chromium-linux.png b/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-chromium-linux.png index e3e4dfecc..47e150d27 100644 Binary files a/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-chromium-linux.png and b/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-chromium-linux.png differ diff --git a/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-firefox-linux.png b/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-firefox-linux.png index 6072c0a65..a39d8aff0 100644 Binary files a/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-firefox-linux.png and b/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-firefox-linux.png differ diff --git a/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-webkit-linux.png b/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-webkit-linux.png index 87812d8b2..b408899c5 100644 Binary files a/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-webkit-linux.png and b/tests/ui.spec.ts-snapshots/UI-all-components-render-2-1-webkit-linux.png differ