Skip to content

Commit

Permalink
Add a standalone Modal layout (#7083)
Browse files Browse the repository at this point in the history
  • Loading branch information
hoxbro authored Jan 20, 2025
1 parent cb4bd74 commit 64e9992
Show file tree
Hide file tree
Showing 11 changed files with 567 additions and 1 deletion.
89 changes: 89 additions & 0 deletions examples/reference/layouts/Modal.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "5c557435-c052-4622-8023-81ed6f63f231",
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"pn.extension('modal')"
]
},
{
"cell_type": "markdown",
"id": "a44c5eae-9c73-4626-96ff-019cf959d647",
"metadata": {},
"source": [
"The `Modal` layout provides a dialog windows on top of the layout. It is built on-top of [a11y-dialog](https://a11y-dialog.netlify.app/). It has a list-like API with methods to `append`, `extend`, `clear`, `insert`, `pop`, `remove` and `__setitem__`, which make it possible to interactively update and modify the layout. Components inside it are laid out like a `Column`.\n",
"\n",
"#### Parameters:\n",
"\n",
"* **`open`** (boolean): Whether to open the modal.\n",
"* **`show_close_button`** (boolean): Whether to show a close button in the modal.\n",
"* **`background_close`** (boolean): Whether to enable closing the modal when clicking outside the modal.\n",
"\n",
"#### Methods:\n",
"\n",
"* **`show`**: Show the modal.\n",
"* **`hide`**: Hide the modal.\n",
"* **`toggle`**: toggle the modal.\n",
"* **`create_button`**: Create a button which can either show, hide, or toggle the modal.\n",
"\n"
]
},
{
"cell_type": "markdown",
"id": "2b3a8784-7e78-4414-a1a5-600b0ff14664",
"metadata": {},
"source": [
"A `Modal` layout can either be instantiated as empty and populated after the fact or using a list of objects provided as positional arguments. If the objects are not already panel components they will each be converted to one using the `pn.panel` conversion method."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9fd46bf9-a63e-457c-bcc2-a1578b8f9bda",
"metadata": {},
"outputs": [],
"source": [
"w1 = pn.widgets.TextInput(name='Text:')\n",
"w2 = pn.widgets.FloatSlider(name='Slider')\n",
"\n",
"modal = pn.Modal(w1, w2, name='Basic FloatPanel', margin=20)\n",
"toggle_button = modal.create_button(\"toggle\", name=\"Toggle modal\")\n",
"\n",
"pn.Column('**Example: Basic `Modal`**', toggle_button, modal)"
]
},
{
"cell_type": "markdown",
"id": "558852dd-9a11-4099-bae3-6da3e38f79f9",
"metadata": {},
"source": [
"### Controls\n",
"\n",
"The `Modal` widget exposes a number of options which can be changed from both Python and Javascript. Try out the effect of these parameters interactively:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b49ed4d6-b06b-47e1-a917-88ff3141936b",
"metadata": {},
"outputs": [],
"source": [
"modal.controls(jslink=True)"
]
}
],
"metadata": {
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
3 changes: 2 additions & 1 deletion panel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
)
from .layout import ( # noqa
Accordion, Card, Column, Feed, FlexBox, FloatPanel, GridBox, GridSpec,
GridStack, HSpacer, Row, Spacer, Swipe, Tabs, VSpacer, WidgetBox,
GridStack, HSpacer, Modal, Row, Spacer, Swipe, Tabs, VSpacer, WidgetBox,
)
from .pane import panel # noqa
from .param import Param, ReactiveExpr # noqa
Expand All @@ -88,6 +88,7 @@
"GridSpec",
"GridStack",
"HSpacer",
"Modal",
"Param",
"ReactiveExpr",
"Row",
Expand Down
1 change: 1 addition & 0 deletions panel/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,7 @@ class panel_extension(_pyviz_extension):
'jsoneditor': 'panel.models.jsoneditor',
'katex': 'panel.models.katex',
'mathjax': 'panel.models.mathjax',
'modal': 'panel.models.modal',
'perspective': 'panel.models.perspective',
'plotly': 'panel.models.plotly',
'tabulator': 'panel.models.tabulator',
Expand Down
2 changes: 2 additions & 0 deletions panel/layout/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from .float import FloatPanel # noqa
from .grid import GridBox, GridSpec # noqa
from .gridstack import GridStack # noqa
from .modal import Modal
from .spacer import ( # noqa
Divider, HSpacer, Spacer, VSpacer,
)
Expand All @@ -58,6 +59,7 @@
"HSpacer",
"ListLike",
"ListPanel",
"Modal",
"Panel",
"Row",
"Spacer",
Expand Down
75 changes: 75 additions & 0 deletions panel/layout/modal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from __future__ import annotations

from typing import (
TYPE_CHECKING, ClassVar, Literal, Mapping, Optional,
)

import param

from pyviz_comms import JupyterComm

from ..models.modal import ModalDialogEvent
from ..util import lazy_load
from ..util.warnings import PanelUserWarning, warn
from .base import ListPanel

if TYPE_CHECKING:
from bokeh.document import Document
from bokeh.model import Model
from pyviz_comms import Comm


class Modal(ListPanel):
"""Create a modal dialog that can be opened and closed."""

open = param.Boolean(default=False, doc="Whether to open the modal.")

show_close_button = param.Boolean(default=True, doc="Whether to show a close button in the modal.")

background_close = param.Boolean(default=True, doc="Whether to enable closing the modal when clicking the background.")

_rename: ClassVar[Mapping[str, str | None]] = {}

_source_transforms: ClassVar[Mapping[str, str | None]] = {'objects': None}

def _get_model(
self, doc: Document, root: Optional[Model] = None,
parent: Optional[Model] = None, comm: Optional[Comm] = None
) -> Model:
Modal._bokeh_model = lazy_load(
'panel.models.modal', 'Modal', isinstance(comm, JupyterComm), root
)
return super()._get_model(doc, root, parent, comm)

def show(self):
self.open = True

def hide(self):
self.open = False

def toggle(self):
self.open = not self.open

@param.depends("open", watch=True)
def _open(self):
if not self._models:
msg = "To use the Modal, you must use '.servable' in a server setting or output the Modal in Jupyter Notebook."
warn(msg, category=PanelUserWarning)
self._send_event(ModalDialogEvent, open=self.open)

def create_button(self, action: Literal["show", "hide", "toggle"], **kwargs):
"""Create a button to show, hide or toggle the modal."""
from panel.widgets import Button

button = Button(**kwargs)
match action:
case "show":
button.on_click(lambda *e: self.show())
case "hide":
button.on_click(lambda *e: self.hide())
case "toggle":
button.on_click(lambda *e: self.toggle())
case _:
raise TypeError(f"Invalid action: {action}")

return button
1 change: 1 addition & 0 deletions panel/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export {JSONEditor} from "./jsoneditor"
export {KaTeX} from "./katex"
export {Location} from "./location"
export {MathJax} from "./mathjax"
export {Modal} from "./modal"
export {PDF} from "./pdf"
export {Perspective} from "./perspective"
export {Player} from "./player"
Expand Down
54 changes: 54 additions & 0 deletions panel/models/modal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from typing import Any

from bokeh.core.properties import Bool
from bokeh.events import ModelEvent
from bokeh.model import Model

from ..config import config
from ..io.resources import bundled_files
from ..util import classproperty
from .layout import Column

__all__ = (
"Modal",
"ModalDialogEvent",
)


class Modal(Column):

__javascript_raw__ = [
f"{config.npm_cdn}/a11y-dialog@7/dist/a11y-dialog.min.js"
]

@classproperty
def __javascript__(cls):
return bundled_files(cls)

@classproperty
def __js_skip__(cls):
return {'A11yDialog': cls.__javascript__[:1]}

__js_require__ = {
'paths': {
'a11y-dialog': f"{config.npm_cdn}/a11y-dialog@7/dist/a11y-dialog.min",
},
'exports': {
'A11yDialog': 'a11y-dialog',
}
}

open = Bool(default=False, help="Whether or not the modal is open.")
show_close_button = Bool(True, help="Whether to show a close button in the modal.")
background_close = Bool(True, help="Whether to enable closing the modal when clicking the background.")


class ModalDialogEvent(ModelEvent):
event_name = 'modal-dialog-event'

def __init__(self, model: Model | None, open: bool):
self.open = open
super().__init__(model=model)

def event_values(self) -> dict[str, Any]:
return dict(super().event_values(), open=self.open)
Loading

0 comments on commit 64e9992

Please sign in to comment.