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

Draft
wants to merge 35 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 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
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
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
86 changes: 86 additions & 0 deletions panel/dist/css/models/modal.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
:host(.bk-panel-models-modal-Modal) {
width: 0;
height: 0;
}

.dialog-container,
.dialog-overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.dialog-container {
z-index: 100002;
display: flex;
}
.dialog-overlay {
z-index: 100001;
background-color: rgb(43 46 56 / 0.9);
}
.dialog-content {
margin: auto;
z-index: 100002;
position: relative;
background-color: white;
border-radius: 2px;
padding: 10px;
padding-bottom: 20px;
}
fast-design-system-provider .dialog-content {
background-color: var(--background-color);
border-radius: calc(var(--corner-radius) * 1px);
}
@keyframes fade-in {
from {
opacity: 0;
}
}
@keyframes slide-up {
from {
transform: translateY(10%);
}
}
.dialog-overlay {
animation: fade-in 200ms both;
}
.dialog-content {
animation:
fade-in 400ms 200ms both,
slide-up 400ms 200ms both;
}
@media (prefers-reduced-motion: reduce) {
.dialog-overlay,
.dialog-content {
animation: none;
}
}
.pnx-dialog-close {
position: absolute;
top: 0.5em;
right: 0.5em;
border: 0;
padding: 0.25em;
background-color: transparent;
font-size: 1.5em;
width: 1.5em;
height: 1.5em;
text-align: center;
cursor: pointer;
transition: 0.15s;
border-radius: 50%;
z-index: 100003;
}
fast-design-system-provider .pnx-dialog-close {
color: var(--neutral-foreground-rest);
}
.pnx-dialog-close:hover {
background-color: rgb(50 50 0 / 0.15);
}
fast-design-system-provider .pnx-dialog-close:hover {
background-color: var(--neutral-fill-hover);
}
.lm-Widget.p-Widget.lm-TabBar.p-TabBar.lm-DockPanel-tabBar.jp-Activity {
z-index: -1;
}
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
72 changes: 72 additions & 0 deletions panel/layout/modal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from __future__ import annotations
Copy link
Collaborator

Choose a reason for hiding this comment

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

When adding documentation please consider if request awesome-panel/panel-modal#3 should be part of the docs. Thx.


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?

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

import param

from pyviz_comms import JupyterComm

from ..io.resources import CDN_DIST
from ..models.modal import ModalDialogEvent
from ..util import lazy_load
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.")

_stylesheets: ClassVar[list[str]] = [f"{CDN_DIST}css/models/modal.css"]

_rename: ClassVar[Mapping[str, str | 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):
self._send_event(ModalDialogEvent, open=self.open)

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

button = Button(**kwargs)
match button_type:
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 button_type: {button_type}")

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
53 changes: 53 additions & 0 deletions panel/models/modal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from typing import Any

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

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

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


class Modal(Column):

__javascript_raw__ = [
"https://cdn.jsdelivr.net/npm/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': "https://cdn.jsdelivr.net/npm/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