Skip to content

Commit 4ce8413

Browse files
authored
Add NestedBreadcrumbs menu component (#488)
* Added NestedBreadcrumbs Menu component * More tests and features * Small fixes * Add docs * Fix lint * Remove breakpoint * Fix tests
1 parent e8b2383 commit 4ce8413

File tree

6 files changed

+1054
-27
lines changed

6 files changed

+1054
-27
lines changed
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"id": "55e7b392-14c0-4411-bfcf-a46e7e117368",
7+
"metadata": {},
8+
"outputs": [],
9+
"source": [
10+
"import panel as pn\n",
11+
"import panel_material_ui as pmui\n",
12+
"\n",
13+
"pn.extension()"
14+
]
15+
},
16+
{
17+
"cell_type": "markdown",
18+
"id": "a1183801-6081-4061-8795-d7fd8e5e8f38",
19+
"metadata": {},
20+
"source": [
21+
"The `NestedBreadcrumbs` component is part of the `Menu` family. `Menu` components provide a structured way for users to navigate or choose between a defined set of items. Unlike `Breadcrumbs`, `NestedBreadcrumbs` represents a **tree**: for each level, a small chevron opens a menu to switch between **siblings at that depth**. Clicking a segment navigates to that depth.\n",
22+
"\n",
23+
"Each item in the `NestedBreadcrumbs` list is defined by a dictionary with several supported keys:\n",
24+
"\n",
25+
"## Item Structure\n",
26+
"\n",
27+
"Each item can include the following keys:\n",
28+
"\n",
29+
"* **`label`** (`str`, required): The text displayed for the segment.\n",
30+
"* **`href`** (`str`, optional): URL to link to (used on non-terminal segments).\n",
31+
"* **`target`** (`str`, optional): Link target (e.g. `\"_blank\"`).\n",
32+
"* **`icon`** (`str`, optional): Material icon name to render next to the label.\n",
33+
"* **`avatar`** (`str`, optional): A short avatar/text badge to show beside the label.\n",
34+
"* **`items`** (`list[dict]`, optional): Nested children for the next depth.\n",
35+
"* **`selectable`** (`bool`, optional): Whether this item can be selected in the sibling menu (defaults to `True`).\n",
36+
"\n",
37+
"Items are passed via the `items` parameter as a list containing a single **root** item. When a depth is selected, the explicit selection path is stored in `active` (see below).\n",
38+
"\n",
39+
"## Parameters\n",
40+
"\n",
41+
"### Core\n",
42+
"\n",
43+
"* **`active`** (`tuple[int, ...]`): The explicit selection path from the root (e.g. `(0, 2, 1)`).\n",
44+
" The UI may **auto-descend** for rendering (first-child tails), but only the explicit part is stored here.\n",
45+
"* **`auto_descend`** (`bool`, default `True`): Controls whether the UI auto-descends via first children when rendering.\n",
46+
" * `True`: The UI appends a first-child tail to `active` to compute `path` (for display).\n",
47+
" * `False`: No auto-descent; if the last explicit node has children, the last breadcrumb renders as an unselected **“Select…”** segment with a chevron menu.\n",
48+
"* **`disabled`** (`bool`): Whether the breadcrumb control is disabled.\n",
49+
"* **`items`** (`list[dict]`): A list with **one** root item; children live in each item’s `items`.\n",
50+
"* **`path`** (`tuple[int, ...]`): The currently visible path.\n",
51+
" The UI may **auto-descend** for rendering (first-child tails), but only the explicit part is stored here.\n",
52+
"\n",
53+
"### Display\n",
54+
"\n",
55+
"* **`color`** (`str`): Color variant for the **active** segment; one of your theme’s supported colors (e.g. `'primary'`, `'success'`, …).\n",
56+
"* **`separator`** (`str`, optional): Custom separator between segments. Defaults to the MUI NavigateNext icon.\n",
57+
"* **`max_items`** (`int`, optional): Maximum number of segments to display (older segments are collapsed per MUI behavior).\n",
58+
"\n",
59+
"### Styling\n",
60+
"\n",
61+
"* **`sx`** (`dict`): Component-level styling API.\n",
62+
"* **`theme_config`** (`dict`): Theming API.\n",
63+
"\n",
64+
"---"
65+
]
66+
},
67+
{
68+
"cell_type": "markdown",
69+
"id": "2a353334-70ba-4afc-9408-102763f0f665",
70+
"metadata": {},
71+
"source": [
72+
"### Basic Usage\n",
73+
"\n",
74+
"`NestedBreadcrumbs` like all menu components allows selecting between a number of `items` defined as dictionaries in a list. The "
75+
]
76+
},
77+
{
78+
"cell_type": "code",
79+
"execution_count": null,
80+
"id": "03ca8d3f-1b72-49b6-b419-6f36ffe5981c",
81+
"metadata": {},
82+
"outputs": [],
83+
"source": [
84+
"breadcrumb_items = [\n",
85+
" {\n",
86+
" 'label': 'Home',\n",
87+
" 'icon': 'home',\n",
88+
" 'secondary': 'Overview page',\n",
89+
" 'items': [\n",
90+
" {'label': 'Welcome', 'icon': 'handshake'},\n",
91+
" {'label': 'Getting Started', 'icon': 'rocket'}\n",
92+
" ]\n",
93+
" },\n",
94+
" {\n",
95+
" 'label': 'Gallery',\n",
96+
" 'icon': 'image',\n",
97+
" 'secondary': 'Visual overview',\n",
98+
" 'items': [\n",
99+
" {'label': 'Charts', 'icon': 'stacked_line_chart'},\n",
100+
" {'label': 'Maps', 'icon': 'map', 'items': [\n",
101+
" {'label': 'World', 'icon': 'public'}, \n",
102+
" ]},\n",
103+
" {'label': 'Animations', 'icon': 'animation'}\n",
104+
" ]\n",
105+
" },\n",
106+
" {\n",
107+
" 'label': 'API',\n",
108+
" 'icon': 'code',\n",
109+
" 'secondary': 'API Reference',\n",
110+
" 'items': [\n",
111+
" {'label': 'Endpoints', 'icon': 'terminal'},\n",
112+
" {'label': 'Schemas', 'icon': 'schema'}\n",
113+
" ]\n",
114+
" },\n",
115+
" {\n",
116+
" 'label': 'About',\n",
117+
" 'icon': 'info',\n",
118+
" 'selectable': False,\n",
119+
" 'items': [\n",
120+
" {'label': 'Team', 'icon': 'groups'},\n",
121+
" {'label': 'Contact', 'icon': 'mail'}\n",
122+
" ]\n",
123+
" },\n",
124+
"]\n",
125+
"\n",
126+
"breadcrumb_menu = pmui.NestedBreadcrumbs(\n",
127+
" items=breadcrumb_items\n",
128+
")\n",
129+
"\n",
130+
"pmui.Column(breadcrumb_menu, height=120)"
131+
]
132+
},
133+
{
134+
"cell_type": "markdown",
135+
"id": "86f8fb96-088d-494d-bc6a-db2e04f1cf8f",
136+
"metadata": {},
137+
"source": [
138+
"The `active` and `path` parameters represent the paths to the selected and rendered items expressed as tuples of indexes:"
139+
]
140+
},
141+
{
142+
"cell_type": "code",
143+
"execution_count": null,
144+
"id": "6bcbc467-406c-436c-9f4c-8924a0fb7d8f",
145+
"metadata": {},
146+
"outputs": [],
147+
"source": [
148+
"pmui.NestedBreadcrumbs(\n",
149+
" items=breadcrumb_items, active=(1, 1), path=(1, 1, 0)\n",
150+
")"
151+
]
152+
},
153+
{
154+
"cell_type": "markdown",
155+
"id": "10363988-4d98-4d74-ad4f-13d988263405",
156+
"metadata": {},
157+
"source": [
158+
"By default, the component will automatically descend the tree (`auto_descend=True`), extending the visible breadcrumb path by following the first child at each level below the current selection.\n",
159+
"\n",
160+
"This behavior ensures that users always see a fully expanded path to a leaf item, even when only a higher-level node is explicitly selected. When `auto_descend=False`, the component instead stops at the last explicitly selected item and renders a “Select…” placeholder segment with a chevron menu, prompting the user to choose the next level manually."
161+
]
162+
},
163+
{
164+
"cell_type": "code",
165+
"execution_count": null,
166+
"id": "34e8beb2-1e51-4fe7-993c-0a50b42798a7",
167+
"metadata": {},
168+
"outputs": [],
169+
"source": [
170+
"descend_menu = pmui.NestedBreadcrumbs(\n",
171+
" items=breadcrumb_items, auto_descend=False, active=(1, 1)\n",
172+
")\n",
173+
"\n",
174+
"pmui.Column(descend_menu, height=120)"
175+
]
176+
},
177+
{
178+
"cell_type": "markdown",
179+
"id": "cfa0964d-a065-433f-9a74-58986dbfcbc6",
180+
"metadata": {},
181+
"source": [
182+
"### Display Options\n",
183+
"\n",
184+
"#### `color`"
185+
]
186+
},
187+
{
188+
"cell_type": "code",
189+
"execution_count": null,
190+
"id": "f2058915-4b7b-4e25-a3ac-1ac02ea4cbbd",
191+
"metadata": {},
192+
"outputs": [],
193+
"source": [
194+
"pn.GridBox(*(\n",
195+
" breadcrumb_menu.clone(color=color, label=color, active=0)\n",
196+
" for color in pmui.NestedBreadcrumbs.param.color.objects\n",
197+
"), ncols=2)"
198+
]
199+
},
200+
{
201+
"cell_type": "markdown",
202+
"id": "850389a7-296e-4b12-8677-46d5017d5a46",
203+
"metadata": {},
204+
"source": [
205+
"### API Reference\n",
206+
"\n",
207+
"#### Parameters\n",
208+
"\n",
209+
"The `NestedBreadcrumbs` exposes a number of options which can be changed from both Python and Javascript. Try out the effect of these parameters interactively:"
210+
]
211+
},
212+
{
213+
"cell_type": "code",
214+
"execution_count": null,
215+
"id": "eed95ed2-1889-4617-9a16-1b89a3edad4f",
216+
"metadata": {},
217+
"outputs": [],
218+
"source": [
219+
"pmui.NestedBreadcrumbs(items=breadcrumb_items).api(jslink=True)"
220+
]
221+
},
222+
{
223+
"cell_type": "markdown",
224+
"id": "1716b823-fef1-4213-89a5-7a00dd75e6e2",
225+
"metadata": {},
226+
"source": [
227+
"### References\n",
228+
"\n",
229+
"**Panel Documentation:**\n",
230+
"\n",
231+
"- [How-to guides on interactivity](https://panel.holoviz.org/how_to/interactivity/index.html) - Learn how to add interactivity to your applications using widgets\n",
232+
"- [Setting up callbacks and links](https://panel.holoviz.org/how_to/links/index.html) - Connect parameters between components and create reactive interfaces\n",
233+
"- [Declarative UIs with Param](https://panel.holoviz.org/how_to/param/index.html) - Build parameter-driven applications\n",
234+
"\n",
235+
"**Material UI Breadcrumbs:**\n",
236+
"\n",
237+
"- [Material UI Breadcrumbs Reference](https://mui.com/material-ui/react-breadcrumbs) - Complete documentation for the underlying Material UI component\n",
238+
"- [Material UI Breadcrumbs API](https://mui.com/material-ui/api/breadcrumbs/) - Detailed API reference and configuration options"
239+
]
240+
}
241+
],
242+
"metadata": {
243+
"kernelspec": {
244+
"display_name": "Python 3 (ipykernel)",
245+
"language": "python",
246+
"name": "python3"
247+
},
248+
"language_info": {
249+
"codemirror_mode": {
250+
"name": "ipython",
251+
"version": 3
252+
},
253+
"file_extension": ".py",
254+
"mimetype": "text/x-python",
255+
"name": "python",
256+
"nbconvert_exporter": "python",
257+
"pygments_lexer": "ipython3",
258+
"version": "3.12.2"
259+
}
260+
},
261+
"nbformat": 4,
262+
"nbformat_minor": 5
263+
}

src/panel_material_ui/layout/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def _to_object_and_name(self, item):
9292
pane = panel(item)
9393
else:
9494
header = None
95-
pane = panel(item, name=name)
95+
pane = item if isinstance(item, Viewable) else panel(item, name=name)
9696
name = param_name(pane.name) if name is None else name
9797
return pane, header, name
9898

src/panel_material_ui/menu.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import Menu from "@mui/material/Menu"
55
import Paper from "@mui/material/Paper"
66
import Popper from "@mui/material/Popper"
77

8-
export function CustomMenu({open, anchorEl, onClose, children, sx}) {
8+
export function CustomMenu({open, anchorEl, onClose, children, sx, keepMounted}) {
99
const nb = document.querySelector(".jp-NotebookPanel");
1010

1111
if (nb == null) {
@@ -23,6 +23,7 @@ export function CustomMenu({open, anchorEl, onClose, children, sx}) {
2323
horizontal: "right",
2424
}}
2525
sx={sx}
26+
keepMounted={keepMounted}
2627
>
2728
{children}
2829
</Menu>
@@ -34,7 +35,7 @@ export function CustomMenu({open, anchorEl, onClose, children, sx}) {
3435
open={open}
3536
anchorEl={anchorEl}
3637
placement="bottom-end"
37-
style={{zIndex: 1500, width: anchorEl.current?.offsetWidth}}
38+
style={{zIndex: 1500, width: (anchorEl ? anchorEl.current : anchorEl)?.offsetWidth}}
3839
>
3940
{({TransitionProps, placement}) => (
4041
<Grow

0 commit comments

Comments
 (0)