From a1dcb83f50ec473e0665bd06e7c295f4d589a481 Mon Sep 17 00:00:00 2001 From: MarcSkovMadsen Date: Mon, 20 Nov 2023 04:43:26 +0000 Subject: [PATCH 1/2] panel-modal --- examples/reference/layouts/Modal.ipynb | 123 ++++++++++++++++ panel/__init__.py | 3 +- panel/layout/__init__.py | 2 + panel/layout/modal.py | 187 +++++++++++++++++++++++++ 4 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 examples/reference/layouts/Modal.ipynb create mode 100644 panel/layout/modal.py diff --git a/examples/reference/layouts/Modal.ipynb b/examples/reference/layouts/Modal.ipynb new file mode 100644 index 0000000000..fdfe3fcc7f --- /dev/null +++ b/examples/reference/layouts/Modal.ipynb @@ -0,0 +1,123 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "aaac1313-e203-4d79-82d1-a316671a2022", + "metadata": {}, + "source": [ + "# Panel Modal\n", + "\n", + "A *modal* is an element that displays in front of and deactivates all other page content.\n", + "\n", + "Compared to the modal built in to Panels templates this modal provides more flexibility\n", + "as it also works works without using templates and in notebooks.\n", + "\n", + "#### Parameters:\n", + "\n", + "For details on other options for customizing the component see the [layout](../../how_to/layout/index.md) and [styling](../../how_to/styling/index.md) how-to guides.\n", + "\n", + "* **``is_open``** (bool, default=False): Whether or not the modal is open. Set this to `True` to open the modal or `False` to close it.\n", + "* **``show_close_button``** (bool, default=True): Whether to show a close button in the modal.\n", + "\n", + "#### Methods:\n", + "\n", + "* **``open``** (bool, default=False): Run this action to open the modal.\n", + "* **``close``** (bool, default=False): Run this action to close the modal.\n", + "\n", + "___" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e57edfe6-3fbe-484e-963d-d93efef0aa5f", + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "import hvplot.pandas # noqa\n", + "import pandas as pd\n", + "\n", + "from panel import Modal\n", + "\n", + "pn.extension(\"modal\", sizing_mode=\"stretch_width\")" + ] + }, + { + "cell_type": "markdown", + "id": "72e383f8-b7fb-4beb-ba5e-08b0b7dcb915", + "metadata": {}, + "source": [ + "Lets create some `content` to display in the `Modal`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30043f44-4b4f-4090-b329-86007a342711", + "metadata": {}, + "outputs": [], + "source": [ + "age_list = [8, 10, 12, 14, 72, 74, 76, 78, 20, 25, 30, 35, 60, 85]\n", + "df = pd.DataFrame({\"gender\": list(\"MMMMMMMMFFFFFF\"), \"age\": age_list})\n", + "plot = df.hvplot.box(y='age', by='gender', height=400, legend=False, ylim=(0, None))\n", + "\n", + "content = pn.Column(\n", + " \"## Hi. I'm a *modal*\", plot, \"What a nice plot!\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "9374fa8e-925b-456c-982a-3d872f8d8ba9", + "metadata": {}, + "source": [ + "Lets create the `modal`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e15915df-cb6d-47d0-8513-463df5a847cc", + "metadata": {}, + "outputs": [], + "source": [ + "modal = Modal(content)" + ] + }, + { + "cell_type": "markdown", + "id": "f6a18d7e-c75a-4ddc-a6f4-4ebdb018bdfc", + "metadata": {}, + "source": [ + "Let us create a `Column` *layout* containing and `open` button and the `modal`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "426d2a11-71d6-4fbd-b0c9-737c5ee4614e", + "metadata": {}, + "outputs": [], + "source": [ + "pn.Column(modal.param.open, modal, modal.param.is_open, modal.param.show_close_button).servable()" + ] + }, + { + "cell_type": "markdown", + "id": "afe4fdeb-6de6-4961-82ce-e7cf6e9671a5", + "metadata": {}, + "source": [ + "Try clicking the *Open* button." + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/panel/__init__.py b/panel/__init__.py index 7505e77229..a55027503e 100644 --- a/panel/__init__.py +++ b/panel/__init__.py @@ -64,7 +64,7 @@ ) from .layout import ( # noqa Accordion, Card, Column, FlexBox, FloatPanel, GridBox, GridSpec, GridStack, - HSpacer, Row, Spacer, Swipe, Tabs, VSpacer, WidgetBox, + HSpacer, Modal, Row, Spacer, Swipe, Tabs, VSpacer, WidgetBox, ) from .pane import panel # noqa from .param import Param, ReactiveExpr # noqa @@ -83,6 +83,7 @@ "GridSpec", "GridStack", "HSpacer", + "Modal", "Param", "ReactiveExpr", "Row", diff --git a/panel/layout/__init__.py b/panel/layout/__init__.py index 9cc2a26135..502d75d2d6 100644 --- a/panel/layout/__init__.py +++ b/panel/layout/__init__.py @@ -37,6 +37,7 @@ from .float import FloatPanel # noqa from .grid import GridBox, GridSpec # noqa from .gridstack import GridStack # noqa +from .modal import Modal # noqa from .spacer import ( # noqa Divider, HSpacer, Spacer, VSpacer, ) @@ -56,6 +57,7 @@ "HSpacer", "ListLike", "ListPanel", + "Modal", "Panel", "Row", "Spacer", diff --git a/panel/layout/modal.py b/panel/layout/modal.py new file mode 100644 index 0000000000..731faf0c36 --- /dev/null +++ b/panel/layout/modal.py @@ -0,0 +1,187 @@ +"""The `Modal` is an element that displays in front of and deactivates all other page content.""" +import param + +from ..reactive import ReactiveHTML +from .base import NamedListLike + +# See https://a11y-dialog.netlify.app/ +JS_FILE = "https://cdn.jsdelivr.net/npm/a11y-dialog@7/dist/a11y-dialog.min.js" + +STYLE = """ +.dialog-container, +.dialog-overlay { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; +} +.dialog-container { + z-index: 100002; + display: flex; +} +.dialog-container[aria-hidden='true'] { + display: none; +} +.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: 10003; +} +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; +} +""" + +class Modal(ReactiveHTML, NamedListLike): # pylint: disable=too-many-ancestors + """The `Modal` layout is a *pop up* element that displays in front of and deactivates + all other page content. + + You will need to include the `Modal` in a layout or your template. It will not be + shown before you trigger the `open` event or equivalently set `is_open=True`. + + Args: + *objects: The objects to display in the modal. + + Reference: https://panel.holoviz.org/reference/layouts/Modal.html + + Example: + + >>> import panel as pn + >>> from panel import Modal + >>> pn.extension("modal") + >>> modal = Modal(pn.panel("Hi. I am the Panel Modal!", width=200)) + >>> pn.Column(modal.param.open, modal).servable() + """ + + is_open = param.Boolean(doc=""" + Whether or not the modal is open. Set to True to open. Set to False to close.""") + show_close_button = param.Boolean(True, doc="Whether to show a close button in the modal") + + open = param.Event(doc="Click here to open the modal") + close = param.Event(doc="Click here to close the modal") + + style = param.String(STYLE, doc="The css styles applied to the modal") + + def __init__(self, *objects, **params): # pylint: disable=redefined-builtin + params["height"] = params["width"] = params["margin"] = 0 + + NamedListLike.__init__(self, *objects, **params) + ReactiveHTML.__init__(self, objects=self.objects, **params) + + @param.depends("open", watch=True) + def _show(self): + self.is_open = True + + @param.depends("close", watch=True) + def _hide(self): + self.is_open = False + + _extension_name = "modal" + + __javascript__ = [JS_FILE] + + + _template = """ + + +""" + + _scripts = { + "render": """ + fast_el = document.getElementById("body-design-provider") + if (fast_el!==null){ + fast_el.appendChild(pnx_dialog_style) + fast_el.appendChild(pnx_dialog) + } + self.show_close_button() + self.init_modal() + """, + "init_modal": """ +state.modal = new A11yDialog(pnx_dialog) +state.modal.on('show', function (element, event) {data.is_open=true}) +state.modal.on('hide', function (element, event) {data.is_open=false}) +if (data.is_open==true){state.modal.show()} +""", + "is_open": """\ +if (data.is_open==true){state.modal.show();view.invalidate_layout()} else {state.modal.hide()}""", + "show_close_button": """ +if (data.show_close_button){pnx_dialog_close.style.display = " block"}else{pnx_dialog_close.style.display = "none"} +""", + } From f1c3599294e0a1eea755b7678ee893b61d6e5670 Mon Sep 17 00:00:00 2001 From: MarcSkovMadsen Date: Wed, 22 Nov 2023 04:01:40 +0000 Subject: [PATCH 2/2] start testing panel modal --- panel/layout/modal.py | 26 ++++++++++++++------------ panel/tests/layout/test_modal.py | 10 ++++++++++ 2 files changed, 24 insertions(+), 12 deletions(-) create mode 100644 panel/tests/layout/test_modal.py diff --git a/panel/layout/modal.py b/panel/layout/modal.py index 731faf0c36..40606c39b0 100644 --- a/panel/layout/modal.py +++ b/panel/layout/modal.py @@ -1,11 +1,15 @@ -"""The `Modal` is an element that displays in front of and deactivates all other page content.""" +"""The `Modal` is an element that displays in front of and deactivates all other page content. + +Use it to focus your users attention on a specific piece of information or a +specific action. +""" import param from ..reactive import ReactiveHTML from .base import NamedListLike # See https://a11y-dialog.netlify.app/ -JS_FILE = "https://cdn.jsdelivr.net/npm/a11y-dialog@7/dist/a11y-dialog.min.js" +JS_FILE = "https://cdn.jsdelivr.net/npm/a11y-dialog@8/dist/a11y-dialog.min.js" STYLE = """ .dialog-container, @@ -93,9 +97,12 @@ """ class Modal(ReactiveHTML, NamedListLike): # pylint: disable=too-many-ancestors - """The `Modal` layout is a *pop up* element that displays in front of and deactivates + """The `Modal` layout is a *pop up*, element that displays in front of and deactivates all other page content. + Use it to focus your users attention on a specific piece of information or a + specific action. + You will need to include the `Modal` in a layout or your template. It will not be shown before you trigger the `open` event or equivalently set `is_open=True`. @@ -112,19 +119,16 @@ class Modal(ReactiveHTML, NamedListLike): # pylint: disable=too-many-ancestors >>> modal = Modal(pn.panel("Hi. I am the Panel Modal!", width=200)) >>> pn.Column(modal.param.open, modal).servable() """ + show_close_button = param.Boolean(True, doc="Whether to show a close button in the modal") is_open = param.Boolean(doc=""" Whether or not the modal is open. Set to True to open. Set to False to close.""") - show_close_button = param.Boolean(True, doc="Whether to show a close button in the modal") - open = param.Event(doc="Click here to open the modal") close = param.Event(doc="Click here to close the modal") - style = param.String(STYLE, doc="The css styles applied to the modal") - def __init__(self, *objects, **params): # pylint: disable=redefined-builtin + params["sizing_mode"]="fixed" params["height"] = params["width"] = params["margin"] = 0 - NamedListLike.__init__(self, *objects, **params) ReactiveHTML.__init__(self, objects=self.objects, **params) @@ -142,11 +146,9 @@ def _hide(self): _template = """ - +