Skip to content

Commit c30dca9

Browse files
authored
Implement docked variant for Drawer (#696)
1 parent 85ab0a4 commit c30dca9

3 files changed

Lines changed: 153 additions & 23 deletions

File tree

examples/reference/layouts/Drawer.ipynb

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,7 @@
1717
"cell_type": "markdown",
1818
"id": "a6c6a705-0ea1-4a19-a264-eacac2b2544e",
1919
"metadata": {},
20-
"source": [
21-
"The `Drawer` component (akin to a sidebar) provides ergonomic access to destinations in a site or app functionality.\n",
22-
"\n",
23-
"## Parameters:\n",
24-
"For details on other options for customizing the component see the layout and styling how-to guides.\n",
25-
"\n",
26-
"### Core\n",
27-
"\n",
28-
"* **`open`** (`boolean`): Whether the `Drawer` is open.\n",
29-
"\n",
30-
"### Display\n",
31-
"\n",
32-
"* **`anchor`** (`Literal[\"left\", \"right\", \"bottom\", \"right\"]`): Where to position the `Drawer`.\n",
33-
"* **`size`** (`int`): The width or height depending on whether the `Drawer` is rendered on the left/right or top/bottom respectively.\n",
34-
"* **`variant`** (`Literal[\"temporary\", \"persistent\", \"permanent\"]`): Whether the Drawer is temporary, persistent or permanent.\n",
35-
"\n",
36-
"---"
37-
]
20+
"source": "The `Drawer` component (akin to a sidebar) provides ergonomic access to destinations in a site or app functionality.\n\n## Parameters:\nFor details on other options for customizing the component see the layout and styling how-to guides.\n\n### Core\n\n* **`open`** (`boolean`): Whether the `Drawer` is open.\n\n### Display\n\n* **`anchor`** (`Literal[\"left\", \"right\", \"bottom\", \"top\"]`): Where to position the `Drawer`.\n* **`dock_position`** (`Literal[\"start\", \"middle\", \"end\"]`): Position of the toggle tab along the drawer edge (only applies to 'docked' variant).\n* **`size`** (`int`): The width or height depending on whether the `Drawer` is rendered on the left/right or top/bottom respectively.\n* **`variant`** (`Literal[\"temporary\", \"persistent\", \"permanent\", \"docked\"]`): Whether the Drawer is temporary, persistent, permanent or docked.\n\n---"
3821
},
3922
{
4023
"cell_type": "markdown",
@@ -140,6 +123,20 @@
140123
").preview()"
141124
]
142125
},
126+
{
127+
"cell_type": "markdown",
128+
"id": "cbad6d15",
129+
"source": "### Docked drawer\n\nA docked drawer renders a small toggle tab on the edge where it's anchored, allowing users to open and close it without needing a separate button. When closed, only the tab is visible; when open, the drawer slides out with the tab attached to close it again.\n\nUse `dock_position` to control where the tab appears along the drawer edge (`\"start\"`, `\"middle\"`, or `\"end\"`):",
130+
"metadata": {}
131+
},
132+
{
133+
"cell_type": "code",
134+
"id": "c6356ab8",
135+
"source": "drawer = Drawer(\n \"## Navigation\",\n Button(label=\"Home\"),\n Button(label=\"Settings\"),\n size=250,\n variant=\"docked\",\n anchor=\"left\",\n dock_position=\"middle\",\n)\n\nRow(drawer, Container(\"# Main Content\", width_option='sm')).preview()",
136+
"metadata": {},
137+
"execution_count": null,
138+
"outputs": []
139+
},
143140
{
144141
"cell_type": "code",
145142
"execution_count": null,

src/panel_material_ui/layout/Drawer.jsx

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,109 @@
11
import Drawer from "@mui/material/Drawer"
2+
import Icon from "@mui/material/Icon"
3+
import Paper from "@mui/material/Paper"
24
import {apply_flex} from "./utils"
35

6+
const TAB_SIZE = 24
7+
const TAB_LENGTH = 48
8+
9+
const anchorToChevron = {
10+
left: {open: "chevron_left", closed: "chevron_right"},
11+
right: {open: "chevron_right", closed: "chevron_left"},
12+
top: {open: "expand_less", closed: "expand_more"},
13+
bottom: {open: "expand_more", closed: "expand_less"},
14+
}
15+
16+
function getPositionStyle(anchor, dockPosition) {
17+
const isHorizontal = anchor === "left" || anchor === "right"
18+
if (isHorizontal) {
19+
switch (dockPosition) {
20+
case "start": return {top: `${TAB_LENGTH}px`}
21+
case "end": return {bottom: `${TAB_LENGTH}px`}
22+
default: return {top: "50%", transform: "translateY(-50%)"}
23+
}
24+
} else {
25+
switch (dockPosition) {
26+
case "start": return {left: `${TAB_LENGTH}px`}
27+
case "end": return {right: `${TAB_LENGTH}px`}
28+
default: return {left: "50%", transform: "translateX(-50%)"}
29+
}
30+
}
31+
}
32+
33+
function DockedTab({anchor, open, dockPosition, attached, onClick}) {
34+
const isHorizontal = anchor === "left" || anchor === "right"
35+
const icon = anchorToChevron[anchor][open ? "open" : "closed"]
36+
37+
const positionStyles = (() => {
38+
if (attached) {
39+
const offsetSide = anchor === "left" ? "right" : anchor === "right" ? "left" : anchor === "top" ? "bottom" : "top"
40+
return {
41+
[offsetSide]: `-${TAB_SIZE + 1}px`,
42+
...getPositionStyle(anchor, dockPosition),
43+
}
44+
}
45+
return {
46+
[anchor]: 0,
47+
...getPositionStyle(anchor, dockPosition),
48+
}
49+
})()
50+
51+
const tabStyle = {
52+
position: "absolute",
53+
...positionStyles,
54+
width: isHorizontal ? `${TAB_SIZE}px` : `${TAB_LENGTH}px`,
55+
height: isHorizontal ? `${TAB_LENGTH}px` : `${TAB_SIZE}px`,
56+
minWidth: 0,
57+
minHeight: 0,
58+
padding: 0,
59+
display: "flex",
60+
alignItems: "center",
61+
justifyContent: "center",
62+
cursor: "pointer",
63+
zIndex: 1,
64+
borderRadius: (() => {
65+
switch (anchor) {
66+
case "left": return "0 4px 4px 0"
67+
case "right": return "4px 0 0 4px"
68+
case "top": return "0 0 4px 4px"
69+
case "bottom": return "4px 4px 0 0"
70+
}
71+
})(),
72+
}
73+
74+
return (
75+
<Paper
76+
elevation={2}
77+
sx={tabStyle}
78+
onClick={onClick}
79+
role="button"
80+
aria-label={open ? "Close drawer" : "Open drawer"}
81+
>
82+
<Icon sx={{fontSize: "1.2rem"}}>{icon}</Icon>
83+
</Paper>
84+
)
85+
}
86+
487
export function render({model, view}) {
588
const [anchor] = model.useState("anchor")
89+
const [dockPosition] = model.useState("dock_position")
690
const [open, setOpen] = model.useState("open")
791
const [size] = model.useState("size")
892
const [sx] = model.useState("sx")
993
const [variant] = model.useState("variant")
1094
const objects = model.get_child("objects")
1195

96+
const isDocked = variant === "docked"
97+
1298
let dims
1399
if (!["top", "bottom"].includes(anchor)) {
14100
dims = {width: `${size}px`}
15-
if (variant !== "temporary") {
101+
if (!["temporary", "docked"].includes(variant)) {
16102
view.el.style.width = `${open ? size : 0}px`
17103
}
18104
} else {
19105
dims = {height: `${size}px`}
20-
if (variant !== "temporary") {
106+
if (!["temporary", "docked"].includes(variant)) {
21107
view.el.style.height = `${open ? size : 0}px`
22108
}
23109
}
@@ -32,6 +118,49 @@ export function render({model, view}) {
32118
return () => model.off("lifecycle:update_layout", handler)
33119
}, [])
34120

121+
if (isDocked) {
122+
const isHorizontal = anchor === "left" || anchor === "right"
123+
const containerStyle = {
124+
position: "fixed",
125+
top: 0,
126+
[anchor]: 0,
127+
[isHorizontal ? "width" : "height"]: open ? `${size + TAB_SIZE}px` : `${TAB_SIZE}px`,
128+
[isHorizontal ? "height" : "width"]: "100%",
129+
zIndex: 1200,
130+
pointerEvents: "none",
131+
}
132+
133+
const innerStyle = {
134+
position: "relative",
135+
[isHorizontal ? "width" : "height"]: "100%",
136+
[isHorizontal ? "height" : "width"]: "100%",
137+
pointerEvents: "auto",
138+
}
139+
140+
return (
141+
<div style={containerStyle}>
142+
<div style={innerStyle}>
143+
<Drawer
144+
anchor={anchor}
145+
open={open}
146+
onClose={() => setOpen(false)}
147+
slotProps={{paper: {sx: [dims, {overflow: "visible"}, sx || {}]}}}
148+
variant="persistent"
149+
>
150+
{objects.map((object, index) => {
151+
apply_flex(view.get_child_view(model.objects[index]), "column")
152+
return object
153+
})}
154+
<DockedTab anchor={anchor} open={open} dockPosition={dockPosition} attached onClick={() => setOpen(false)} />
155+
</Drawer>
156+
{!open && (
157+
<DockedTab anchor={anchor} open={false} dockPosition={dockPosition} attached={false} onClick={() => setOpen(true)} />
158+
)}
159+
</div>
160+
</div>
161+
)
162+
}
163+
35164
return (
36165
<Drawer anchor={anchor} open={open} onClose={() => setOpen(false)} slotProps={{paper: {sx: [dims, sx || {}]}}} variant={variant}>
37166
{objects.map((object, index) => {

src/panel_material_ui/layout/base.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,21 +1108,25 @@ class Drawer(MaterialListLike):
11081108
default="left", objects=["left", "right", "top", "bottom"],
11091109
doc="Anchor position for the drawer.") # type: ignore[assignment]
11101110

1111+
dock_position: t.Literal["start", "middle", "end"] = param.Selector(
1112+
default="middle", objects=["start", "middle", "end"],
1113+
doc="Position of the toggle tab along the drawer edge (only applies to 'docked' variant).") # type: ignore[assignment]
1114+
11111115
size = param.Integer(default=250, doc="""
11121116
The width (for left/right anchors) or height (for top/bottom anchors) of the drawer.""")
11131117

11141118
open = param.Boolean(default=False, doc="""
11151119
Whether the drawer is open.""")
11161120

1117-
variant: t.Literal["permanent", "persistent", "temporary"] = param.Selector(
1118-
default="temporary", objects=["permanent", "persistent", "temporary"],
1121+
variant: t.Literal["docked", "permanent", "persistent", "temporary"] = param.Selector(
1122+
default="temporary", objects=["docked", "permanent", "persistent", "temporary"],
11191123
doc="Variant style of the drawer.") # type: ignore[assignment]
11201124

11211125
_esm_base = "Drawer.jsx"
11221126

11231127
@param.depends("variant", watch=True, on_init=True)
11241128
def _force_zero_dimensions(self):
1125-
if self.variant == "temporary":
1129+
if self.variant in ("temporary", "docked"):
11261130
self.param.update(width=0, height=0, sizing_mode="fixed")
11271131

11281132
def create_toggle(

0 commit comments

Comments
 (0)