Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a standalone Modal layout #7083

Merged
merged 50 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
5d211f4
Add boilerplate
hoxbro Aug 2, 2024
d301572
Getting it working
hoxbro Aug 5, 2024
7dcf020
Clean up
hoxbro Aug 5, 2024
4220c27
More clean up
hoxbro Aug 5, 2024
6d1689f
Updates
hoxbro Aug 5, 2024
009c57d
Add modal css file
hoxbro Aug 5, 2024
6cea115
add first modal
hoxbro Aug 5, 2024
e8585a9
First attempt at serverside events
hoxbro Aug 5, 2024
185d5e2
Add open/close event
hoxbro Aug 5, 2024
5a82aca
Make is_open readonly
hoxbro Aug 5, 2024
e3c8e75
Add background_close option
hoxbro Aug 5, 2024
1484cc3
Type A11yDialogView
hoxbro Aug 6, 2024
e68d42f
Update render
hoxbro Aug 6, 2024
7145d8b
Update close button SVG
hoxbro Aug 6, 2024
8a617aa
Clean up
hoxbro Aug 6, 2024
256b337
Try updating js and css
hoxbro Aug 9, 2024
df0140a
Require modal extension call
philippjfr Aug 9, 2024
d5f8501
Merge branch 'main' into modal
hoxbro Aug 26, 2024
09c414d
Misc updates
hoxbro Aug 26, 2024
7f9b12b
Use open as parameter, show / hide / toggle as function call
hoxbro Aug 26, 2024
94a27fc
Merge branch 'main' into modal
hoxbro Oct 21, 2024
fea45f0
Merge branch 'main' into modal
hoxbro Dec 2, 2024
c9ada92
Use a symbol instead of SVG for close button
hoxbro Dec 2, 2024
06aee3f
First show the content on modal on click
hoxbro Dec 2, 2024
cf3414e
remove modal_children
hoxbro Dec 2, 2024
1bb3c55
Use model sizes for dialog_content
hoxbro Dec 2, 2024
e784495
Simplify logic and enable option to show modal at start
hoxbro Dec 2, 2024
52454dc
Set host class to have no size
hoxbro Dec 3, 2024
1e1ded7
Remove container
hoxbro Dec 3, 2024
5e82f98
Remove window as any
hoxbro Dec 3, 2024
59bba8a
clean ups
hoxbro Dec 3, 2024
5914e4e
Merge branch 'main' into modal
hoxbro Dec 3, 2024
9d531b1
Fix types
hoxbro Dec 3, 2024
77e5adc
More clean up
hoxbro Dec 3, 2024
8235a48
Add convenience method for creating button
hoxbro Dec 3, 2024
230e81e
type: Return `Self` for `.servable` (#7530)
hoxbro Dec 3, 2024
1f51a84
fix mypy (#7512)
MarcSkovMadsen Dec 3, 2024
6448b40
Merge branch 'main' into modal
hoxbro Jan 15, 2025
77a2ff7
remove close button padding
hoxbro Jan 15, 2025
346afa7
Add reference notebook
hoxbro Jan 15, 2025
c460798
Move to fast.css
hoxbro Jan 15, 2025
925c57d
Add ui tests
hoxbro Jan 15, 2025
d10ea1a
Merge branch 'main' into modal
hoxbro Jan 20, 2025
c573c0e
Add modal to root
hoxbro Jan 20, 2025
2d17f1b
Add warning if Modal is not served
hoxbro Jan 20, 2025
7b8017d
Move to less
hoxbro Jan 20, 2025
ea9dbdd
Don't overwrite Button default
hoxbro Jan 20, 2025
8ca8a61
Correctly import stylesheets
philippjfr Jan 20, 2025
06a0de2
Fix npm import
philippjfr Jan 20, 2025
504ad4c
Fix css stylesheet
philippjfr Jan 20, 2025
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
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
hoxbro marked this conversation as resolved.
Show resolved Hide resolved
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
philippjfr marked this conversation as resolved.
Show resolved Hide resolved

Copy link
Collaborator

@MarcSkovMadsen MarcSkovMadsen Oct 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing to consider is if the Modal could be made easier to use? Could the api of the Modal be generalized to a concept of actions/ action buttons.

Could the api be simplified?

The api of panel-modal that I made is:

import panel as pn

from panel_modal import Modal

pn.extension("modal")

modal = Modal(pn.panel("Hi. I am the Panel Modal!", width=200))

pn.Column(modal.param.open, modal).servable()

It requires you define a variable holding the Modal and include both the button (modal.param.open) and the modal modal.

If you want to customize the button it requires something like pn.widgets.Button.from_param(modal.param.open as well.

Could the api be simplified? Could it be enough to just include the modal?

import panel as pn

from panel_modal import Modal

pn.extension("modal")

pn.Column(
    Modal(pn.panel("Hi. I am the Panel Modal!", width=200))
).servable()

That should show the button. And when clicked the modal should open/ close. That would make it much simpler to drop in as you don't need to define a variable holding the Modal.

If you want to customize the button you can just do:

import panel as pn

from panel_modal import Modal

pn.extension("modal")

pn.Column(
    pn.widgets.Button.from_param(
        Modal(
            pn.panel("Hi. I am the Panel Modal!", width=200),
        ),
        button_style="outline",
    )
)
).servable()

If you want to trigger the modal from something else than a Button than you should be able to hide the button:

import panel as pn

from panel_modal import Modal

pn.extension("modal")

modal = Modal(..., visible=False)

...

def trigger_modal():
    modal.open=True

pn.Column(
    modal,
    .....
).servable()

Could the API be generalized?

A Modal is actually a part of a general concept for action buttons. You want to be able to trigger an action via a Button. You want to be able to easily set the source or target (object) and be able to customize the button.

modal_button = ModalButton(object=pn.panel(...))
copy_text_button= CopyToClipboardButton(object="...my code")
copy_dataframe_button= CopyToClipboardButton(object=df) # df is a dataframe
paste_button = PasteFromClipboardButton(object=pn.widgets.Tabulator) # Here the object is the target. Not the source.
link_button = OpenLinkButton(object="https://panel.holoviz.org, target="_blank")
maximize_content = pn.Column(...)
maximize_button = MaximizeButton(object=maximize_content) # Maximizes the object to full window size
...

I've had in the back of my head that I wanted to create a panel-action-buttons package one day. Compiling these as I use those for my work app all the time.

@philippjfr. What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I never responded, especially as we're working towards merging this. My feeling is that while I can see how it might be useful it kind of breaks a few mental models people have built. A few more lines of code seem like an okay price to pay for keeping a relatively consistent mental model for how things worked. For a Modal to render as a button and something like pn.widgets.Button.from_param(Modal()) to work we'd also internally have to break a lot of models on how things work, so I'm roughly -1 on this, though it's something we could think about adding later.

The one thing we could consider doing to make this potential option backward compatible would be to only allow the modal to be rendered as a root, i.e. pn.Row(modal).servable() would be disallowed and modal.servable() would be the only valid way to add a modal. What do you think @hoxbro?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added Modal.create_button after this comment to make it easier to use the Modal.

I'm not sure about the suggested API. I'm not hard against it. If you still want this, maybe open a new issue to discuss it.

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)
hoxbro marked this conversation as resolved.
Show resolved Hide resolved

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
Loading