diff --git a/plugins/ui/docs/components/breadcrumbs.md b/plugins/ui/docs/components/breadcrumbs.md new file mode 100644 index 000000000..399be1aaf --- /dev/null +++ b/plugins/ui/docs/components/breadcrumbs.md @@ -0,0 +1,309 @@ +# Breadcrumbs + +Breadcrumbs show hierarchy and navigational context for a user's location within an application. + +```python +from deephaven import ui + + +breadcrumbs_example = ui.view( + ui.breadcrumbs( + ui.item("Deephaven", key="deephaven"), + ui.item("Products", key="products"), + ui.item("Community Core", key="community_core"), + ), + width="100%", +) +``` + +## Content + +`ui.breadcrumbs` accepts `item` elements as children, each with a `key` prop. Basic usage of breadcrumbs, seen in the example above, shows multiple items populated with a string. + +## Events + +Use the `on_action` prop to specify a callback to handle press events on items. + +```python +from deephaven import ui + + +@ui.component +def breadcrumbs_action_example(): + selected, set_selected = ui.use_state("None") + + return ( + ui.view( + ui.breadcrumbs( + ui.item("Deephaven", key="deephaven"), + ui.item("Products", key="products"), + ui.item("Community Core", key="community_core"), + on_action=set_selected, + ), + ui.text(f"{selected} clicked"), + width="100%", + ), + ) + + +my_breadcrumbs_action_example = breadcrumbs_action_example() +``` + +## Links + +By default, interacting with an item in breadcrumbs triggers `on_action`. By passing the `href` prop to the `ui.item` component, items may also be links to another page or website. The target window to open the link in can be configured using the `target` prop. + +```python +from deephaven import ui + + +breadcrumbs_link_example = ui.view( + ui.breadcrumbs( + ui.item( + "Deephaven", + key="deephaven", + href="https://deephaven.io/", + target="_blank", + ), + ui.item( + "Community Core", + key="community_core", + href="https://deephaven.io/community/", + target="_blank", + ), + ui.item( + "Getting Started", + key="getting_started", + href="https://deephaven.io/core/docs/getting-started/quickstart/", + target="_blank", + ), + ), + width="100%", +) +``` + +## Size + +The size of the breadcrumbs including spacing and layout can be set using the `size` prop. By default this is set to `"L"`. + +```python +from deephaven import ui + + +breadcrumbs_size_example = ui.view( + ui.breadcrumbs( + ui.item("Deephaven", key="deephaven"), + ui.item("Products", key="products"), + ui.item("Community Core", key="community_core"), + ), + ui.breadcrumbs( + ui.item("Deephaven", key="deephaven"), + ui.item("Products", key="products"), + ui.item("Community Core", key="community_core"), + size="M", + ), + ui.breadcrumbs( + ui.item("Deephaven", key="deephaven"), + ui.item("Products", key="products"), + ui.item("Community Core", key="community_core"), + size="S", + ), + width="100%", +) +``` + +## Multiline + +Use the `is_multiline` prop to place the last item below the other items. This adds emphasis to the current location as a page title or heading. + +```python +from deephaven import ui + + +breadcrumbs_multiline_example = ui.view( + ui.breadcrumbs( + ui.item("Deephaven", key="deephaven"), + ui.item("Products", key="products"), + ui.item("Community Core", key="community_core"), + is_multiline=True, + ), + width="100%", +) +``` + +## Root context + +Some applications find that always displaying the root item is useful to orient users. Use the `show_root` prop to keeps the root visible when other items are truncated into the menu. + +```python +from deephaven import ui + + +breadcrumbs_root_context_example = ui.view( + ui.breadcrumbs( + ui.item("Deephaven", key="deephaven"), + ui.item("Products", key="products"), + ui.item("Community Core", key="community_core"), + ui.item("Getting Started", key="getting_started"), + ui.item("Create Tables", key="create_tables"), + show_root=True, + ), + width="300px", +) +``` + +## Disabled + +Use the `is_disabled` prop to show items but indicate that navigation is not available. This can be used to maintain layout continuity. + +```python +from deephaven import ui + + +breadcrumbs_disabled_example = ui.view( + ui.breadcrumbs( + ui.item("Deephaven", key="deephaven"), + ui.item("Products", key="products"), + ui.item("Community Core", key="community_core"), + is_disabled=True, + ), + width="100%", +) +``` + +## Overflow behavior + +Breadcrumbs collapses items into a menu when space is limited. It will only show a maximum of 4 visible items including the root and menu button, if either are visible. + +If the root item cannot be rendered in the available horizontal space, it will be collapsed into the menu regardless of the `show_root` prop. + +Note that the last breadcrumb item will automatically truncate with an ellipsis instead of collapsing into the menu. + +```python +from deephaven import ui + + +@ui.component +def breadcrumbs_overflow_example(): + return [ + ui.view( + ui.breadcrumbs( + ui.item("Deephaven", key="deephaven"), + ui.item("Products", key="products"), + ui.item("Community Core", key="community_core"), + ui.item("Getting Started", key="getting_started"), + ui.item("Create Tables", key="create_tables"), + show_root=True, + ), + border_width="thin", + border_color="accent-400", + width="100%", + ), + ui.view( + ui.breadcrumbs( + ui.item("Deephaven", key="deephaven"), + ui.item("Products", key="products"), + ui.item("Community Core", key="community_core"), + ui.item("Getting Started", key="getting_started"), + ui.item("Create Tables", key="create_tables"), + show_root=True, + ), + border_width="thin", + border_color="accent-400", + width="200px", + ), + ui.view( + ui.breadcrumbs( + ui.item("Deephaven", key="deephaven"), + ui.item("Products", key="products"), + ui.item("Community Core", key="community_core"), + ui.item("Getting Started", key="getting_started"), + ui.item("Create Tables", key="create_tables"), + ), + border_width="thin", + border_color="accent-400", + width="100px", + ), + ] + + +my_breadcrumbs_overflow_example = breadcrumbs_overflow_example() +``` + +## Detailed example + +Below is an example using the generated `tips` dataset from the Deephaven Express API. It allows you to explore the data in a hierarchical order of day, time, sex, and smoker status. + +```python +import deephaven.plot.express as dx +from deephaven.table import Table +from deephaven import ui + + +@ui.component +def table_breadcrumb_filterer( + table: Table, filter_columns: list[str], all_item_text="All" +): + items, set_items = ui.use_state([ui.item(all_item_text)]) + option_column, set_option_column = ui.use_state(filter_columns[0]) + filters, set_filters = ui.use_state([]) + + filtered_table = ui.use_memo(lambda: table.where(filters), [table, filters]) + column_value_table = ui.use_memo( + lambda: filtered_table.select_distinct(option_column), + [filtered_table, option_column], + ) + column_values = ui.use_column_data(column_value_table) + + def handle_action(key): + current_index = filter_columns.index(option_column) + set_items(items + [ui.item(f"{key}", key=option_column)]) + if current_index < len(filter_columns) - 1: + set_option_column(filter_columns[current_index + 1]) + set_filters(filters + [f"{option_column} == '{key}'"]) + + def handle_back(key): + if key not in filter_columns: + set_items([ui.item(all_item_text)]) + set_option_column(filter_columns[0]) + set_filters([]) + return + + selected_index = filter_columns.index(key) + set_items(items[: selected_index + 2]) + set_option_column(filter_columns[selected_index + 1]) + set_filters(filters[: selected_index + 1]) + + show_filter = len(filters) < len(filter_columns) + + return ui.flex( + ui.flex( + ui.breadcrumbs(*items, show_root=True, on_action=handle_back, flex_grow=1), + ui.view( + ui.menu_trigger( + ui.action_button(f"Filter by {option_column}", ui.icon("filter")), + ui.menu( + *[ui.item(value) for value in column_values], + on_action=handle_action, + ), + ), + ) + if show_filter + else None, + ), + filtered_table.view( + formulas=["TotalBill", "Tip", "Size"] + filter_columns[len(filters) :] + ), + direction="column", + ) + + +_tips = dx.data.tips() +my_tips = table_breadcrumb_filterer(_tips, ["Day", "Time", "Sex", "Smoker"], "All Tips") +``` + +## API reference + +```{eval-rst} +.. dhautofunction:: deephaven.ui.breadcrumbs +``` diff --git a/plugins/ui/docs/sidebar.json b/plugins/ui/docs/sidebar.json index 0254f9e3a..ed0fe78c0 100644 --- a/plugins/ui/docs/sidebar.json +++ b/plugins/ui/docs/sidebar.json @@ -108,6 +108,10 @@ "label": "badge", "path": "components/badge.md" }, + { + "label": "breadcrumbs", + "path": "components/breadcrumbs.md" + }, { "label": "button", "path": "components/button.md" diff --git a/plugins/ui/src/deephaven/ui/components/__init__.py b/plugins/ui/src/deephaven/ui/components/__init__.py index 4d448473b..feb0b1360 100644 --- a/plugins/ui/src/deephaven/ui/components/__init__.py +++ b/plugins/ui/src/deephaven/ui/components/__init__.py @@ -6,6 +6,7 @@ component_element, ) from .badge import badge +from .breadcrumbs import breadcrumbs from .button import button from .button_group import button_group from .calendar import calendar @@ -85,6 +86,7 @@ "avatar", "component_element", "badge", + "breadcrumbs", "button", "button_group", "calendar", diff --git a/plugins/ui/src/deephaven/ui/components/breadcrumbs.py b/plugins/ui/src/deephaven/ui/components/breadcrumbs.py new file mode 100644 index 000000000..f879cfe0d --- /dev/null +++ b/plugins/ui/src/deephaven/ui/components/breadcrumbs.py @@ -0,0 +1,185 @@ +from __future__ import annotations + +from typing import Callable + +from .basic import component_element +from .section import Item +from ..elements import Element +from .types import ( + AlignSelf, + CSSProperties, + DimensionValue, + JustifySelf, + LayoutFlex, + Position, +) +from ..types import BreadcrumbsSize + + +def breadcrumbs( + *children: Item, + is_disabled: bool | None = None, + size: BreadcrumbsSize | None = None, + show_root: bool | None = None, + is_multiline: bool | None = None, + auto_focus_current: bool | None = None, + on_action: Callable[[str], 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_column: str | None = None, + grid_row: 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, + min_width: DimensionValue | None = None, + max_width: DimensionValue | None = None, + height: DimensionValue | None = None, + min_height: 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, +) -> Element: + """ + Breadcrumbs show hierarchy and navigational context for a user's location within an application. + + Args: + *children: The items to render within the breadcrumbs. + is_disabled: Whether the Breadcrumbs are disabled. + size: The size of the breadcrumbs inlcuding spacing and layout. + show_root: Whether to always show the root item if the items are collapsed. + is_multiline: Whether the last item will be placed below other items. + auto_focus_current: Whether to autoFocus the last item. + on_action: Handler that is called when an item is clicked. + flex: When used in a flex layout, specifies how the element will grow or shrink to fit the space available. + flex_grow: When used in a flex layout, specifies how much the element will grow to fit the space available. + flex_shrink: When used in a flex layout, specifies how much the element will shrink to fit the space available. + flex_basis: When used in a flex layout, specifies the initial size of the element. + align_self: Overrides the align_items property of a flex or grid container. + justify_self: Specifies how the element is justified inside a flex or grid container. + order: The layout order for the element within a flex or grid container. + grid_area: The name of the grid area to place the element in. + grid_row: The name of the grid row to place the element in. + grid_row_start: The name of the grid row to start the element in. + grid_row_end: The name of the grid row to end the element in. + grid_column: The name of the grid column to place the element in. + grid_column_start: The name of the grid column to start the element in. + grid_column_end: The name of the grid column to end the element in. + margin: The margin to apply around the element. + margin_top: The margin to apply above the element. + margin_bottom: The margin to apply below the element. + margin_start: The margin to apply before the element. + margin_end: The margin to apply after the element. + margin_x: The margin to apply to the left and right of the element. + margin_y: The margin to apply to the top and bottom of the element. + width: The width of the element. + height: The height of the element. + min_width: The minimum width of the element. + min_height: The minimum height of the element. + max_width: The maximum width of the element. + max_height: The maximum height of the element. + position: Specifies how the element is positioned. + top: The distance from the top of the containing element. + bottom: The distance from the bottom of the containing element. + start: The distance from the start of the containing element. + end: The distance from the end of the containing element. + left: The distance from the left of the containing element. + right: The distance from the right of the containing element. + z_index: The stack order of the element. + is_hidden: Whether the element is hidden. + id: A unique identifier for the element. + aria_label: The label for the element. + aria_labelledby: The id of the element that labels the element. + aria_describedby: The id of the element that describes the element. + aria_details: The details for the element. + UNSAFE_class_name: A CSS class to apply to the element. + UNSAFE_style: A CSS style to apply to the element. + key: A unique identifier used by React to render elements in a list. + + + Returns: + The rendered breadcrumbs element. + """ + return component_element( + "Breadcrumbs", + children=children, + is_disabled=is_disabled, + size=size, + show_root=show_root, + is_multiline=is_multiline, + auto_focus_current=auto_focus_current, + on_action=on_action, + 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_column=grid_column, + grid_row=grid_row, + 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, + min_width=min_width, + max_width=max_width, + height=height, + min_height=min_height, + 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_labelled_by=aria_labelledby, + aria_described_by=aria_describedby, + aria_details=aria_details, + UNSAFE_class_name=UNSAFE_class_name, + UNSAFE_style=UNSAFE_style, + key=key, + ) diff --git a/plugins/ui/src/deephaven/ui/types/types.py b/plugins/ui/src/deephaven/ui/types/types.py index 21133f291..25599e75c 100644 --- a/plugins/ui/src/deephaven/ui/types/types.py +++ b/plugins/ui/src/deephaven/ui/types/types.py @@ -503,6 +503,7 @@ class SliderChange(TypedDict): ] Granularity = Literal["DAY", "HOUR", "MINUTE", "SECOND"] ListViewDensity = Literal["COMPACT", "NORMAL", "SPACIOUS"] +BreadcrumbsSize = Literal["S", "M", "L"] DividerSize = Literal["S", "M", "L"] ListViewOverflowMode = Literal["truncate", "wrap"] ActionGroupDensity = Literal["compact", "regular"] diff --git a/plugins/ui/src/js/src/elements/model/ElementConstants.ts b/plugins/ui/src/js/src/elements/model/ElementConstants.ts index 150a30be8..79cdbb8b3 100644 --- a/plugins/ui/src/js/src/elements/model/ElementConstants.ts +++ b/plugins/ui/src/js/src/elements/model/ElementConstants.ts @@ -28,6 +28,7 @@ export const ELEMENT_NAME = { actionMenu: uiComponentName('ActionMenu'), avatar: uiComponentName('Avatar'), badge: uiComponentName('Badge'), + breadcrumbs: uiComponentName('Breadcrumbs'), button: uiComponentName('Button'), buttonGroup: uiComponentName('ButtonGroup'), calendar: uiComponentName('Calendar'), diff --git a/plugins/ui/src/js/src/widget/WidgetUtils.tsx b/plugins/ui/src/js/src/widget/WidgetUtils.tsx index cacfd433b..fd816e5ee 100644 --- a/plugins/ui/src/js/src/widget/WidgetUtils.tsx +++ b/plugins/ui/src/js/src/widget/WidgetUtils.tsx @@ -7,6 +7,7 @@ import type { JSONRPCServerAndClient } from 'json-rpc-2.0'; import { ActionMenu, Avatar, + Breadcrumbs, ButtonGroup, SpectrumCheckbox as Checkbox, CheckboxGroup, @@ -129,6 +130,7 @@ export const elementComponentMap: Record, unknown> = { [ELEMENT_NAME.actionMenu]: ActionMenu, [ELEMENT_NAME.avatar]: Avatar, [ELEMENT_NAME.badge]: Badge, + [ELEMENT_NAME.breadcrumbs]: Breadcrumbs, [ELEMENT_NAME.button]: Button, [ELEMENT_NAME.buttonGroup]: ButtonGroup, [ELEMENT_NAME.calendar]: Calendar, diff --git a/tests/app.d/ui_render_all.py b/tests/app.d/ui_render_all.py index fad976cd3..819f394a6 100644 --- a/tests/app.d/ui_render_all.py +++ b/tests/app.d/ui_render_all.py @@ -51,6 +51,11 @@ def ui_components1(): ui.badge("Licensed", variant="positive"), ui.button_group(ui.button("One"), ui.button("Two")), ui.button("Button"), + ui.breadcrumbs( + ui.item("Deephaven", key="deephaven"), + ui.item("Products", key="products"), + ui.item("Community Core", key="community_core"), + ), ui.calendar(value="2021-01-01"), ui.checkbox("Checkbox"), ui.column("Column child A", "Column child B", "Column child C"), @@ -66,14 +71,14 @@ def ui_components1(): ui.flex("Flex default child A", "Flex default child B"), ui.flex("Flex column child A", "Flex column child B", direction="column"), ui.form("Form"), - ui.fragment("Fragment"), - ui.grid("Grid A", "Grid B"), ) @ui.component def ui_components2(): return ( + ui.fragment("Fragment"), + ui.grid("Grid A", "Grid B"), ui.heading("Heading"), ui.icon("vsSymbolMisc"), ui.illustrated_message( diff --git a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-chromium-linux.png b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-chromium-linux.png index a80469299..9f751f495 100644 Binary files a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-chromium-linux.png and b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-chromium-linux.png differ diff --git a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-firefox-linux.png b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-firefox-linux.png index 8337ff61c..1f07bd64c 100644 Binary files a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-firefox-linux.png and b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-firefox-linux.png differ diff --git a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-webkit-linux.png b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-webkit-linux.png index 06c1639df..4929dcd2e 100644 Binary files a/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-webkit-linux.png and b/tests/ui.spec.ts-snapshots/UI-all-components-render-1-1-webkit-linux.png differ 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 062323e81..ccf926bb3 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 cb94d7c6e..ee99dc4d4 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 f4831c2ba..4ced905b5 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