Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 71 additions & 6 deletions examples/reference/layouts/Drawer.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"outputs": [],
"source": [
"import panel as pn\n",
"from panel_material_ui import Button, Container, Drawer, Row\n",
"from panel_material_ui import (\n",
" AppBar, Button, Column, Container, Drawer, Row\n",
")\n",
"\n",
"pn.extension()"
]
Expand All @@ -29,10 +31,11 @@
"\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",
"* **`anchor`** (`Literal[\"left\", \"right\", \"top\", \"bottom\"]`): Where to position the `Drawer`.\n",
"* **`dock_position`** (`Literal[\"start\", \"middle\", \"end\"]`): Position of the toggle tab along the drawer edge (only applies to the `docked` variant).\n",
"* **`inline`** (`boolean`): Whether the drawer is positioned inline within its parent container rather than fixed/absolute to the page. When `True`, the drawer participates in normal flow layout and pushes or shrinks sibling items.\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",
"* **`variant`** (`Literal[\"temporary\", \"persistent\", \"permanent\", \"docked\"]`): Whether the Drawer is temporary, persistent, permanent, or docked.\n",
"\n",
"---"
]
Expand Down Expand Up @@ -119,9 +122,9 @@
"source": [
"### Persistent drawer\n",
"\n",
"Persistent navigation drawers can toggle open or closed, but unlike a `temporary` they sit at the same elevation at the content and participate in the document flow.\n",
"Persistent navigation drawers can toggle open or closed, but unlike a `temporary` they sit at the same elevation as the content and participate in the document flow.\n",
"\n",
"This means that the `Drawer` should be placed in the correct place in the display hierarchy:"
"This means that the `Drawer` should be placed in the correct position in the layout hierarchy:"
]
},
{
Expand Down Expand Up @@ -172,6 +175,68 @@
"\n",
"Row(drawer, Container(\"# Main Content\", width_option='sm')).preview()"
]
},
{
"cell_type": "markdown",
"id": "e12ad35f-057e-4b4f-84c7-f6e957b5d3e4",
"metadata": {},
"source": [
"### Inline drawers\n",
"\n",
"By default drawers are fixed or absolute to the page, overlaying other content. Setting `inline=True` makes the drawer participate in normal flow layout — when placed inside a `Row` or `Column` it pushes or shrinks sibling items rather than overlaying them.\n",
"\n",
"`inline` is independent of `variant`, so all combinations are supported.\n",
"\n",
"An inline docked drawer inside a `Row`:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e814d2b1",
"metadata": {},
"outputs": [],
"source": [
"drawer = Drawer(\n",
" \"## Navigation\",\n",
" Button(label=\"Home\"),\n",
" Button(label=\"Settings\"),\n",
" size=250,\n",
" variant=\"docked\",\n",
" anchor=\"left\",\n",
" inline=True,\n",
")\n",
"\n",
"Row(drawer, Container(\"# Main Content\", width_option='sm'), sizing_mode='stretch_width').preview()"
]
},
{
"cell_type": "markdown",
"id": "49c360cb",
"metadata": {},
"source": [
"An inline persistent drawer:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "043d8117",
"metadata": {},
"outputs": [],
"source": [
"drawer = Drawer(\"## I'm in a Drawer\", size=300, variant=\"persistent\", inline=True)\n",
"\n",
"button = drawer.create_toggle(styles={'margin-left': 'auto'}, color=\"light\")\n",
"content = Row('# Title', sizing_mode='stretch_width')\n",
"\n",
"Column(\n",
" AppBar(drawer_toggle=button, title=\"Hello\", sizing_mode=\"stretch_width\"),\n",
" Row(\n",
" drawer, Container(content, width_option='sm'),\n",
" )\n",
").preview()"
]
}
],
"metadata": {
Expand Down
110 changes: 101 additions & 9 deletions src/panel_material_ui/layout/Drawer.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Drawer from "@mui/material/Drawer"
import Icon from "@mui/material/Icon"
import Paper from "@mui/material/Paper"
import {apply_flex} from "./utils"
import {apply_flex, render_icon} from "./utils"

const TAB_SIZE = 24
const TAB_LENGTH = 48
Expand Down Expand Up @@ -30,11 +29,12 @@ function getPositionStyle(anchor, dockPosition) {
}
}

function DockedTab({anchor, open, dockPosition, attached, onClick, icon: customIcon}) {
function DockedTab({anchor, open, dockPosition, attached, inline, onClick, icon: customIcon}) {
const isHorizontal = anchor === "left" || anchor === "right"
const icon = customIcon || anchorToChevron[anchor][open ? "open" : "closed"]

const positionStyles = (() => {
if (inline) { return {} }
if (attached) {
const offsetSide = anchor === "left" ? "right" : anchor === "right" ? "left" : anchor === "top" ? "bottom" : "top"
return {
Expand All @@ -49,10 +49,12 @@ function DockedTab({anchor, open, dockPosition, attached, onClick, icon: customI
})()

const tabStyle = {
position: "absolute",
position: inline ? "relative" : "absolute",
...positionStyles,
width: isHorizontal ? `${TAB_SIZE}px` : `${TAB_LENGTH}px`,
height: isHorizontal ? `${TAB_LENGTH}px` : `${TAB_SIZE}px`,
...(inline && isHorizontal ? {height: "100%"} : {}),
...(inline && !isHorizontal ? {width: "100%"} : {}),
minWidth: 0,
minHeight: 0,
padding: 0,
Expand All @@ -79,7 +81,7 @@ function DockedTab({anchor, open, dockPosition, attached, onClick, icon: customI
role="button"
aria-label={open ? "Close drawer" : "Open drawer"}
>
<Icon sx={{fontSize: "1.2rem"}}>{icon}</Icon>
{render_icon(icon, null, null, "1.2rem")}
</Paper>
)
}
Expand All @@ -88,23 +90,25 @@ export function render({model, view}) {
const [anchor] = model.useState("anchor")
const [dockIcon] = model.useState("dock_icon")
const [dockPosition] = model.useState("dock_position")
const [inline] = model.useState("inline")
const [open, setOpen] = model.useState("open")
const [size] = model.useState("size")
const [sx] = model.useState("sx")
const [variant] = model.useState("variant")
const objects = model.get_child("objects")

const isDocked = variant === "docked"
const isHorizontal = anchor === "left" || anchor === "right"

let dims
if (!["top", "bottom"].includes(anchor)) {
dims = {width: `${size}px`}
if (!["temporary", "docked"].includes(variant)) {
if (!inline && !["temporary", "docked"].includes(variant)) {
view.el.style.width = `${open ? size : 0}px`
}
} else {
dims = {height: `${size}px`}
if (!["temporary", "docked"].includes(variant)) {
if (!inline && !["temporary", "docked"].includes(variant)) {
view.el.style.height = `${open ? size : 0}px`
}
}
Expand All @@ -119,8 +123,72 @@ export function render({model, view}) {
return () => model.off("lifecycle:update_layout", handler)
}, [])

// Update view.el dimensions so Panel's layout system tracks our size when inline.
React.useEffect(() => {
if (!inline) { return }
if (isDocked) {
if (isHorizontal) {
view.el.style.width = open ? `${size + TAB_SIZE}px` : `${TAB_SIZE}px`
view.el.style.height = "100%"
} else {
view.el.style.height = open ? `${size + TAB_SIZE}px` : `${TAB_SIZE}px`
view.el.style.width = "100%"
}
} else {
if (isHorizontal) {
view.el.style.width = `${open ? size : 0}px`
view.el.style.height = "100%"
} else {
view.el.style.height = `${open ? size : 0}px`
view.el.style.width = "100%"
}
}
}, [inline, isDocked, open, size, isHorizontal])

if (isDocked) {
const isHorizontal = anchor === "left" || anchor === "right"
if (inline) {
// Inline docked: same structure as fixed docked but scoped to a relative container.
const containerStyle = {
position: "relative",
[isHorizontal ? "width" : "height"]: open ? `${size + TAB_SIZE}px` : `${TAB_SIZE}px`,
[isHorizontal ? "height" : "width"]: "100%",
transition: "width 225ms cubic-bezier(0, 0, 0.2, 1), height 225ms cubic-bezier(0, 0, 0.2, 1)",
pointerEvents: "none",
}

const innerStyle = {
position: "relative",
width: "100%",
height: "100%",
pointerEvents: "auto",
}

return (
<div style={containerStyle}>
<div style={innerStyle}>
<Drawer
anchor={anchor}
open={open}
onClose={() => setOpen(false)}
slotProps={{paper: {sx: [dims, {overflow: "visible"}, sx || {}]}}}
variant="persistent"
sx={{"& .MuiDrawer-root": {position: "absolute"}, "& .MuiPaper-root": {position: "absolute"}}}
>
{objects.map((object, index) => {
apply_flex(view.get_child_view(model.objects[index]), "column")
return object
})}
<DockedTab anchor={anchor} open={open} dockPosition={dockPosition} attached onClick={() => setOpen(false)} />
</Drawer>
{!open && (
<DockedTab anchor={anchor} open={false} dockPosition={dockPosition} attached={false} onClick={() => setOpen(true)} />
)}
</div>
</div>
)
}

// Fixed docked: tab strip fixed to the page edge.
const containerStyle = {
position: "fixed",
top: 0,
Expand Down Expand Up @@ -162,8 +230,32 @@ export function render({model, view}) {
)
}

// For temporary / persistent / permanent: use MUI Drawer directly.
// When inline, render inside the parent's flow (position: relative container) so
// siblings are pushed rather than overlaid.
const slotProps = {paper: {sx: [dims, sx || {}]}}
if (inline) {
return (
<div style={{position: "relative", overflow: "hidden", [isHorizontal ? "height" : "width"]: "100%"}}>
<Drawer
anchor={anchor}
open={open}
onClose={() => setOpen(false)}
slotProps={slotProps}
variant={variant === "temporary" ? "persistent" : variant}
sx={{"& .MuiDrawer-root": {position: "absolute"}, "& .MuiPaper-root": {position: "absolute"}}}
>
{objects.map((object, index) => {
apply_flex(view.get_child_view(model.objects[index]), "column")
return object
})}
</Drawer>
</div>
)
}

return (
<Drawer anchor={anchor} open={open} onClose={() => setOpen(false)} slotProps={{paper: {sx: [dims, sx || {}]}}} variant={variant}>
<Drawer anchor={anchor} open={open} onClose={() => setOpen(false)} slotProps={slotProps} variant={variant}>
{objects.map((object, index) => {
apply_flex(view.get_child_view(model.objects[index]), "column")
return object
Expand Down
17 changes: 14 additions & 3 deletions src/panel_material_ui/layout/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1116,6 +1116,11 @@ class Drawer(MaterialListLike):
default="middle", objects=["start", "middle", "end"],
doc="Position of the toggle tab along the drawer edge (only applies to 'docked' variant).") # type: ignore[assignment]

inline = param.Boolean(default=False, doc="""
Whether the drawer is positioned inline within its parent container rather than
fixed/absolute to the page. When True, the drawer participates in normal flow
layout and pushes or shrinks sibling items.""")

size = param.Integer(default=250, doc="""
The width (for left/right anchors) or height (for top/bottom anchors) of the drawer.""")

Expand All @@ -1128,10 +1133,16 @@ class Drawer(MaterialListLike):

_esm_base = "Drawer.jsx"

@param.depends("variant", watch=True, on_init=True)
@param.depends("variant", "inline", "anchor", watch=True, on_init=True)
def _force_zero_dimensions(self):
if self.variant in ("temporary", "docked"):
self.param.update(width=0, height=0, sizing_mode="fixed")
if not self.inline:
if self.variant in ("temporary", "docked"):
self.param.update(width=0, height=0, sizing_mode="fixed")
else:
if self.anchor in ("left", "right"):
self.param.update(sizing_mode="stretch_height")
else:
self.param.update(sizing_mode="stretch_width")

def create_toggle(
self,
Expand Down
Loading