diff --git a/src/panel_material_ui/base.py b/src/panel_material_ui/base.py index c6381c4d..1e6273c8 100644 --- a/src/panel_material_ui/base.py +++ b/src/panel_material_ui/base.py @@ -29,7 +29,7 @@ from panel.models import ReactComponent as BkReactComponent from panel.param import Param from panel.util import base_version, classproperty -from panel.viewable import Viewable +from panel.viewable import Children, Viewable from .__version import __version__ # noqa from .theme import MaterialDesign @@ -142,6 +142,10 @@ class MaterialComponent(ReactComponent): the JS dependencies and theming support via the ThemedTransform. """ + attached = Children(doc=""" + Elements that are attached to this object but are not direct + children.""") + dark_theme = param.Boolean(doc=""" Whether to use dark theme. If not specified, will default to Panel's global theme setting.""") @@ -174,7 +178,8 @@ class MaterialComponent(ReactComponent): "notistack": "https://esm.sh/notistack@3.0.2" } } - _rename = {'loading': 'loading'} + _rename = {'loading': 'loading', 'attached': 'elements'} + _source_transforms = {'attached': None} __abstract = True @@ -235,11 +240,17 @@ def _render_esm(cls, compiled: bool | Literal['compiling'] = True, server: bool return CDN_DIST return super()._render_esm(compiled=True, server=server) + def _get_children(self, data_model, doc, root, parent, comm): + children, old_models = super()._get_children(data_model, doc, root, parent, comm) + children.pop('attached', None) + return children, old_models + def _get_model( self, doc: Document, root: Model | None = None, parent: Model | None = None, comm: Comm | None = None ) -> Model: model = super()._get_model(doc, root, parent, comm) + model.elements, _ = self._get_child_model(self.attached, doc, model or root, parent, comm) # Ensure model loads ESM and CSS bundles from CDN # if requested or if in notebook if ( @@ -266,6 +277,7 @@ def _set_on_model(self, msg: Mapping[str, Any], root: Model, model: Model) -> No def _get_properties(self, doc: Document | None) -> dict[str, Any]: props = super()._get_properties(doc) + props.pop('elements', None) props.pop('loading', None) props['data'].loading = self.loading return props diff --git a/src/panel_material_ui/layout/Drawer.jsx b/src/panel_material_ui/layout/Drawer.jsx new file mode 100644 index 00000000..133172c7 --- /dev/null +++ b/src/panel_material_ui/layout/Drawer.jsx @@ -0,0 +1,14 @@ +import Drawer from "@mui/material/Drawer" + +export function render({model, view}) { + const [open, setOpen] = model.useState("open") + const objects = model.get_child("objects") + + const anchorEl = view.parent.element_views.includes(view) ? view.parent.el : null + + return ( + setOpen(true)} onClose={() => setOpen(false)}> + {objects} + + ) +} diff --git a/src/panel_material_ui/layout/base.py b/src/panel_material_ui/layout/base.py index bd28a52a..f86d08c4 100644 --- a/src/panel_material_ui/layout/base.py +++ b/src/panel_material_ui/layout/base.py @@ -465,6 +465,28 @@ class Dialog(MaterialListLike): _esm_base = "Dialog.jsx" +class Drawer(MaterialListLike): + """ + The `Drawer` component can be used to display important content in a modal-like overlay that requires + user interaction. It is often used for tasks such as confirmations, forms, or displaying + additional information. + + Reference: https://mui.com/material-ui/react-drawer/ + + :Example: + >>> close = Button(on_click=lambda _: drawer.param.update(open=False), label='Close') # type: ignore + >>> drawer = Drawer("This is a drawer", close) + >>> button = Button(on_click=lambda _: drawer.param.update(open=True), label=f'Open {Drawer.name}') + >>> pn.Column(button, drawer).servable() + """ + + open = param.Boolean(default=False, doc=""" + Whether the drawer is open.""") + + _esm_base = "Drawer.jsx" + + + __all__ = [ "Paper", "Container", @@ -475,5 +497,6 @@ class Dialog(MaterialListLike): "Divider", "Alert", "Backdrop", - "Dialog" + "Dialog", + "Drawer" ] diff --git a/src/panel_material_ui/widgets/Menu.jsx b/src/panel_material_ui/widgets/Menu.jsx new file mode 100644 index 00000000..0ce40148 --- /dev/null +++ b/src/panel_material_ui/widgets/Menu.jsx @@ -0,0 +1,39 @@ +import Divider from "@mui/material/Divider" +import Menu from "@mui/material/Menu" +import MenuItem from "@mui/material/MenuItem" + +export function render({model, view}) { + const [dense] = model.useState("dense") + const [items] = model.useState("items") + const [open, setOpen] = model.useState("open") + const [sx] = model.useState("sx") + + const keys = Array.isArray(items) ? items.map((_, index) => index) : Object.keys(items) + const anchorEl = view.parent?.element_views.includes(view) ? view.parent.el : null + + return ( + setOpen(false)} + open={open} + sx={sx} + > + {keys.map((name) => { + const item = items[name] + if (item === null || item.label === "---") { + return + } + const label = item.label || name + const props = {key: name, onClick: () => { model.send_msg(name) }} + return ( + + {item.icon ? {item.icon} : null} + {label} + + ) + })} + + ) +} diff --git a/src/panel_material_ui/widgets/menus.py b/src/panel_material_ui/widgets/menus.py index 45889006..f7b2f604 100644 --- a/src/panel_material_ui/widgets/menus.py +++ b/src/panel_material_ui/widgets/menus.py @@ -11,8 +11,8 @@ from panel.layout.base import ListLike from panel.models.reactive_html import DOMEvent -from ..base import COLORS -from .base import MaterialWidget +from ..base import COLORS, LoadingTransform, ThemedTransform +from .base import MaterialWidget, TooltipTransform from .button import _ButtonBase @@ -263,6 +263,27 @@ def remove_on_action(self, action: str, callback: Callable[[DOMEvent], None]): self._on_action_callbacks[action].remove(callback) +class Menu(MenuBase): + """ + The `Menu` component is a menu component that allows selecting from a list of items. + + Menu items can be strings or objects with properties: + - label: The label of the menu item (required) + - icon: The icon of the menu item (optional) + - color: The color of the menu item (optional) + + Reference: https://mui.com/material-ui/react-menu/ + """ + + dense = param.Boolean(default=False, doc="Whether to show the menu items in a dense format.") + + open = param.Boolean(default=False, doc="Whether the menu is open.") + + _esm_base = "Menu.jsx" + + _item_keys = ['label', 'icon', 'color', 'items'] + + class MenuButton(MenuBase, _ButtonBase): """ The `MenuButton` component is a button component that allows selecting from a list of items. @@ -278,6 +299,7 @@ class MenuButton(MenuBase, _ButtonBase): margin = Margin(default=5) _esm_base = "MenuButton.jsx" + _esm_transforms = [LoadingTransform, TooltipTransform, ThemedTransform] _source_transforms = { "attached": None, "button_type": None, @@ -311,7 +333,7 @@ class Pagination(MaterialWidget): value = param.Integer(default=None, doc="The current zero-indexed page number.") - variant = param.Selector(default="outlined", objects=["outlined", "text"], doc="The variant of the pagination.") + variant = param.Selector(default="text", objects=["outlined", "text"], doc="The variant of the pagination.") width = param.Integer(default=None, doc="The width of the pagination.")