From 5d211f4e607f1fd000a4f0b296666bcc380c4d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 2 Aug 2024 15:27:10 +0200 Subject: [PATCH 01/31] Add boilerplate --- panel/models/__init__.py | 2 +- panel/models/index.ts | 1 + panel/models/layout.py | 6 +++++ panel/models/modal.ts | 48 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 panel/models/modal.ts diff --git a/panel/models/__init__.py b/panel/models/__init__.py index c00045f5bd..69debd68e0 100644 --- a/panel/models/__init__.py +++ b/panel/models/__init__.py @@ -10,7 +10,7 @@ from .feed import Feed # noqa from .icon import ButtonIcon, ToggleIcon, _ClickableIcon # noqa from .ipywidget import IPyWidget # noqa -from .layout import Card, Column # noqa +from .layout import Card, Column, Modal # noqa from .location import Location # noqa from .markup import HTML, JSON, PDF # noqa from .reactive_html import ReactiveHTML # noqa diff --git a/panel/models/index.ts b/panel/models/index.ts index f6e656899c..5a4769dc09 100644 --- a/panel/models/index.ts +++ b/panel/models/index.ts @@ -26,6 +26,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" diff --git a/panel/models/layout.py b/panel/models/layout.py index 98cae44570..27ba63ac96 100644 --- a/panel/models/layout.py +++ b/panel/models/layout.py @@ -8,6 +8,7 @@ "Card", "HTMLBox", "Column", + "Modal", ) @@ -71,3 +72,8 @@ class Card(Column): hide_header = Bool(False, help="Whether to hide the Card header") tag = String("tag", help="CSS class to use for the Card as a whole.") + + +class Modal(Column): + is_open = Bool(False, help="Whether or not the modal is open.") + show_close_button = Bool(True, help="Whether to show a close button in the modal") diff --git a/panel/models/modal.ts b/panel/models/modal.ts new file mode 100644 index 0000000000..a448e0312d --- /dev/null +++ b/panel/models/modal.ts @@ -0,0 +1,48 @@ +import type * as p from "@bokehjs/core/properties" +import {Column as BkColumn, ColumnView as BkColumnView} from "@bokehjs/models/layouts/column" + +export class ModalView extends BkColumnView { + declare model: Modal + + override connect_signals(): void { + super.connect_signals() + const {} = this.model.properties + this.on_change([], () => { this.update() }) + } + + override render(): void { + super.render() + } + + update(): void { + console.log("update") + } +} + +export namespace Modal { + export type Attrs = p.AttrsOf + + export type Props = BkColumn.Props & { + is_open: p.Property + show_close_button: p.Property + } +} + +export interface Modal extends Modal.Attrs {} + +export class Modal extends BkColumn { + declare properties: Modal.Props + + constructor(attrs?: Partial) { + super(attrs) + } + + static override __module__ = "panel.models.layout" + static { + this.prototype.default_view = ModalView + this.define(({Bool}) => ({ + is_open: [Bool, false], + show_close_button: [Bool, true], + })) + } +} From d301572b2ba64755449145e7498982da1d4f3287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 5 Aug 2024 12:06:06 +0200 Subject: [PATCH 02/31] Getting it working --- panel/models/layout.py | 16 ++++++++ panel/models/modal.ts | 90 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/panel/models/layout.py b/panel/models/layout.py index 27ba63ac96..c1df82ce19 100644 --- a/panel/models/layout.py +++ b/panel/models/layout.py @@ -75,5 +75,21 @@ class Card(Column): class Modal(Column): + __css__ = [] + + __javascript__ = [ + "https://cdn.jsdelivr.net/npm/a11y-dialog@7/dist/a11y-dialog.min.js" + ] + + __js_skip__ = { + # 'modal': __javascript__[:1], + } + __js_require__ = { + 'paths': { + 'modal': "https://cdn.jsdelivr.net/npm/a11y-dialog@7/dist/a11y-dialog.min", + }, + 'exports': {} + } + is_open = Bool(False, help="Whether or not the modal is open.") show_close_button = Bool(True, help="Whether to show a close button in the modal") diff --git a/panel/models/modal.ts b/panel/models/modal.ts index a448e0312d..59d3aec5e4 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -1,22 +1,106 @@ import type * as p from "@bokehjs/core/properties" import {Column as BkColumn, ColumnView as BkColumnView} from "@bokehjs/models/layouts/column" +import {div} from "@bokehjs/core/dom" export class ModalView extends BkColumnView { declare model: Modal + modal: any + dialog: any + + override initialize(): void { + super.initialize() + } + override connect_signals(): void { super.connect_signals() const {} = this.model.properties this.on_change([], () => { this.update() }) } + // "render": """ + // fast_el = document.getElementById("body-design-provider") + // if (fast_el!==null){ + // fast_el.append(pnx_dialog_style) + // fast_el.append(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"} + //""", override render(): void { super.render() - } + // + const container = div({style: {display: "contents"}}) + this.dialog = div({ + id: "pnx_dialog", + class: "dialog-container bk-root", + role: "dialog", + "aria-hidden": "true", + "aria-modal": "true", + tabindex: "-1", + } as any) + const dialog_overlay = div({ + class: "dialog-overlay", + "data-a11y-dialog-hide": "", + } as any) + const dialog_content = div({ + id: "pnx_dialog_content", + class: "dialog-content", + role: "document", + //"data-a11y-dialog-hide": "true", + } as any) + const dialog_close = div({ + id: "pnx_dialog_close", + "data-a11y-dialog-hide": "", + class: "pnx-dialog-close", + ariaLabel: "Close this dialog window", + } as any) - update(): void { - console.log("update") + container.append(this.dialog) + this.dialog.append(dialog_overlay) + this.dialog.append(dialog_content) + dialog_content.append(dialog_close) + this.shadow_el.append(container) + + const test = div({ + id: "pnx_modal_object", + class: "dialog-content", + role: "document", + } as any) + test.innerText = "Hello world" + dialog_content.append(test) + + this.modal = new (window as any).A11yDialog(this.dialog) + this.modal.show() } + + update(): void { } } export namespace Modal { From 7dcf020b6a7d96d1a9fe84ca1461d85d7bc70720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 5 Aug 2024 12:26:53 +0200 Subject: [PATCH 03/31] Clean up --- panel/models/modal.ts | 52 +++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/panel/models/modal.ts b/panel/models/modal.ts index 59d3aec5e4..2984e877ec 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -1,12 +1,13 @@ import type * as p from "@bokehjs/core/properties" import {Column as BkColumn, ColumnView as BkColumnView} from "@bokehjs/models/layouts/column" -import {div} from "@bokehjs/core/dom" +import {div, button} from "@bokehjs/core/dom" export class ModalView extends BkColumnView { declare model: Modal modal: any dialog: any + content: any override initialize(): void { super.initialize() @@ -40,67 +41,54 @@ export class ModalView extends BkColumnView { //""", override render(): void { super.render() - // const container = div({style: {display: "contents"}}) this.dialog = div({ id: "pnx_dialog", class: "dialog-container bk-root", - role: "dialog", "aria-hidden": "true", - "aria-modal": "true", - tabindex: "-1", } as any) const dialog_overlay = div({ class: "dialog-overlay", "data-a11y-dialog-hide": "", } as any) - const dialog_content = div({ + this.content = div({ id: "pnx_dialog_content", class: "dialog-content", role: "document", - //"data-a11y-dialog-hide": "true", } as any) - const dialog_close = div({ + const dialog_close = button({ + content: "Close", id: "pnx_dialog_close", "data-a11y-dialog-hide": "", class: "pnx-dialog-close", ariaLabel: "Close this dialog window", + style: { + backgroundColor: "red", + }, } as any) container.append(this.dialog) this.dialog.append(dialog_overlay) - this.dialog.append(dialog_content) - dialog_content.append(dialog_close) + this.dialog.append(this.content) + this.content.append(dialog_close) this.shadow_el.append(container) + this.modal = new (window as any).A11yDialog(this.dialog) + this.update() + this.modal.on("show", (_element: any, _event: any) => { this.model.is_open = true }) + this.modal.on("hide", (_element: any, _event: any) => { this.model.is_open = true }) + this.modal.show() + } + + update(): void { const test = div({ id: "pnx_modal_object", class: "dialog-content", role: "document", } as any) - test.innerText = "Hello world" - dialog_content.append(test) - - this.modal = new (window as any).A11yDialog(this.dialog) - this.modal.show() + test.innerText = "Hello world2" + this.content.append(test) } - - update(): void { } } export namespace Modal { From 4220c27074179777630ad6d2be0d861911f62d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 5 Aug 2024 12:36:33 +0200 Subject: [PATCH 04/31] More clean up --- panel/models/modal.ts | 43 +++++++++++++++---------------------------- 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/panel/models/modal.ts b/panel/models/modal.ts index 2984e877ec..039e4c0e31 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -8,37 +8,15 @@ export class ModalView extends BkColumnView { modal: any dialog: any content: any - - override initialize(): void { - super.initialize() - } + close_button: any override connect_signals(): void { super.connect_signals() - const {} = this.model.properties - this.on_change([], () => { this.update() }) + const {children, show_close_button} = this.model.properties + this.on_change([children], this.update) + this.on_change([show_close_button], this.update_close_button) } - // "render": """ - // fast_el = document.getElementById("body-design-provider") - // if (fast_el!==null){ - // fast_el.append(pnx_dialog_style) - // fast_el.append(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"} - //""", override render(): void { super.render() const container = div({style: {display: "contents"}}) @@ -56,7 +34,7 @@ export class ModalView extends BkColumnView { class: "dialog-content", role: "document", } as any) - const dialog_close = button({ + this.close_button = button({ content: "Close", id: "pnx_dialog_close", "data-a11y-dialog-hide": "", @@ -70,7 +48,7 @@ export class ModalView extends BkColumnView { container.append(this.dialog) this.dialog.append(dialog_overlay) this.dialog.append(this.content) - this.content.append(dialog_close) + this.content.append(this.close_button) this.shadow_el.append(container) this.modal = new (window as any).A11yDialog(this.dialog) @@ -88,6 +66,15 @@ export class ModalView extends BkColumnView { } as any) test.innerText = "Hello world2" this.content.append(test) + this.update_close_button() + } + + update_close_button(): void { + if (this.model.show_close_button) { + this.close_button.style.display = "block" + } else { + this.close_button.style.display = "none" + } } } From 6d1689f6a991c74f3f664eee9a19d47116187ac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 5 Aug 2024 14:01:27 +0200 Subject: [PATCH 05/31] Updates --- panel/models/modal.ts | 42 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/panel/models/modal.ts b/panel/models/modal.ts index 039e4c0e31..8c54b8f7ee 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -6,9 +6,8 @@ export class ModalView extends BkColumnView { declare model: Modal modal: any - dialog: any - content: any - close_button: any + close_button: HTMLButtonElement + modal_children: HTMLElement override connect_signals(): void { super.connect_signals() @@ -20,7 +19,7 @@ export class ModalView extends BkColumnView { override render(): void { super.render() const container = div({style: {display: "contents"}}) - this.dialog = div({ + const dialog = div({ id: "pnx_dialog", class: "dialog-container bk-root", "aria-hidden": "true", @@ -29,7 +28,7 @@ export class ModalView extends BkColumnView { class: "dialog-overlay", "data-a11y-dialog-hide": "", } as any) - this.content = div({ + const content = div({ id: "pnx_dialog_content", class: "dialog-content", role: "document", @@ -40,32 +39,29 @@ export class ModalView extends BkColumnView { "data-a11y-dialog-hide": "", class: "pnx-dialog-close", ariaLabel: "Close this dialog window", - style: { - backgroundColor: "red", - }, } as any) + this.modal_children = div({id: "pnx_modal_object"}) - container.append(this.dialog) - this.dialog.append(dialog_overlay) - this.dialog.append(this.content) - this.content.append(this.close_button) + container.append(dialog) + dialog.append(dialog_overlay) + dialog.append(content) + content.append(this.close_button) + content.append(this.modal_children) this.shadow_el.append(container) - this.modal = new (window as any).A11yDialog(this.dialog) + this.modal = new (window as any).A11yDialog(dialog) this.update() - this.modal.on("show", (_element: any, _event: any) => { this.model.is_open = true }) - this.modal.on("hide", (_element: any, _event: any) => { this.model.is_open = true }) + this.modal.on("show", () => { this.model.is_open = true }) + this.modal.on("hide", () => { this.model.is_open = true }) this.modal.show() } update(): void { - const test = div({ - id: "pnx_modal_object", - class: "dialog-content", - role: "document", - } as any) - test.innerText = "Hello world2" - this.content.append(test) + // TODO: clear old children + for (const child of this.children()) { + // FIXME: remove any and look into better method + this.modal_children.append((child as any).el) + } this.update_close_button() } @@ -100,7 +96,7 @@ export class Modal extends BkColumn { static { this.prototype.default_view = ModalView this.define(({Bool}) => ({ - is_open: [Bool, false], + is_open: [Bool, false], // TODO: read-only show_close_button: [Bool, true], })) } From 009c57d8752e669898d70259bcad968f712c6340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 5 Aug 2024 14:13:10 +0200 Subject: [PATCH 06/31] Add modal css file --- panel/dist/css/models/modal.css | 84 +++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 panel/dist/css/models/modal.css diff --git a/panel/dist/css/models/modal.css b/panel/dist/css/models/modal.css new file mode 100644 index 0000000000..ee6dcc5ce1 --- /dev/null +++ b/panel/dist/css/models/modal.css @@ -0,0 +1,84 @@ +.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: 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; +} From 6cea115968a8d9fea5bfec4872210e42259f92f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 5 Aug 2024 14:37:57 +0200 Subject: [PATCH 07/31] add first modal --- panel/layout/modal.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 panel/layout/modal.py diff --git a/panel/layout/modal.py b/panel/layout/modal.py new file mode 100644 index 0000000000..e8bee7efdf --- /dev/null +++ b/panel/layout/modal.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, ClassVar, Mapping + +import param + +from ..io.resources import CDN_DIST +from ..models import Modal as BkModal +from .base import ListPanel + +if TYPE_CHECKING: + from bokeh.model import Model + + +class Modal(ListPanel): + height = param.Integer(default=None, bounds=(0, None)) + + width = param.Integer(default=None, bounds=(0, None)) + + is_open = param.Boolean(default=False, doc="Whether the modal is open.") + + show_close_button = param.Boolean(default=True, doc="Whether to show a close button in the modal.") + + _bokeh_model: ClassVar[type[Model]] = BkModal + + _stylesheets: ClassVar[list[str]] = [f"{CDN_DIST}css/models/modal.css"] + _rename: ClassVar[Mapping[str, str | None]] = {} + + def __init__(self, *objects, **params): + super().__init__(*objects, **params) From e8585a970e9f69ac8ecf6110f606e1a455c72554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 5 Aug 2024 15:16:01 +0200 Subject: [PATCH 08/31] First attempt at serverside events --- panel/layout/modal.py | 7 +++++++ panel/models/layout.py | 6 ++++++ panel/models/modal.ts | 27 +++++++++++++++++++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/panel/layout/modal.py b/panel/layout/modal.py index e8bee7efdf..a852eb31fb 100644 --- a/panel/layout/modal.py +++ b/panel/layout/modal.py @@ -6,6 +6,7 @@ from ..io.resources import CDN_DIST from ..models import Modal as BkModal +from ..models.layout import ModalDialogEvent from .base import ListPanel if TYPE_CHECKING: @@ -28,3 +29,9 @@ class Modal(ListPanel): def __init__(self, *objects, **params): super().__init__(*objects, **params) + + def open(self): + self._send_event(ModalDialogEvent) + + # def close(self): + # self._send_event(ModalDialogEvent, open=False) diff --git a/panel/models/layout.py b/panel/models/layout.py index c1df82ce19..5fb656c59f 100644 --- a/panel/models/layout.py +++ b/panel/models/layout.py @@ -1,6 +1,7 @@ from bokeh.core.properties import ( Bool, Int, List, Nullable, String, ) +from bokeh.events import ModelEvent from bokeh.models import Column as BkColumn from bokeh.models.layouts import LayoutDOM @@ -93,3 +94,8 @@ class Modal(Column): is_open = Bool(False, help="Whether or not the modal is open.") show_close_button = Bool(True, help="Whether to show a close button in the modal") + + +class ModalDialogEvent(ModelEvent): + event_name = 'modal-dialog-event' + diff --git a/panel/models/modal.ts b/panel/models/modal.ts index 8c54b8f7ee..927a023e07 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -1,6 +1,27 @@ import type * as p from "@bokehjs/core/properties" import {Column as BkColumn, ColumnView as BkColumnView} from "@bokehjs/models/layouts/column" import {div, button} from "@bokehjs/core/dom" +import {ModelEvent, server_event} from "@bokehjs/core/bokeh_events" +import type {Attrs} from "@bokehjs/core/types" + + +@server_event("modal-dialog-event") +export class ModalDialogEvent extends ModelEvent { + constructor() { + super() + //this.open = open + } + + protected override get event_values(): Attrs { + return {} // open: this.open} + } + + static override from_values() { + return new ModalDialogEvent() + + } +} + export class ModalView extends BkColumnView { declare model: Modal @@ -14,6 +35,9 @@ export class ModalView extends BkColumnView { const {children, show_close_button} = this.model.properties this.on_change([children], this.update) this.on_change([show_close_button], this.update_close_button) + this.model.on_event(ModalDialogEvent, (event) => { + this.modal.show() + }) } override render(): void { @@ -52,8 +76,7 @@ export class ModalView extends BkColumnView { this.modal = new (window as any).A11yDialog(dialog) this.update() this.modal.on("show", () => { this.model.is_open = true }) - this.modal.on("hide", () => { this.model.is_open = true }) - this.modal.show() + this.modal.on("hide", () => { this.model.is_open = false }) } update(): void { From 185d5e205858b6f009c76de9cba74b6a93c072bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 5 Aug 2024 17:20:45 +0200 Subject: [PATCH 09/31] Add open/close event --- panel/layout/modal.py | 9 +++------ panel/models/layout.py | 9 +++++++++ panel/models/modal.ts | 30 ++++++++++++++++++++++-------- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/panel/layout/modal.py b/panel/layout/modal.py index a852eb31fb..a5afe02152 100644 --- a/panel/layout/modal.py +++ b/panel/layout/modal.py @@ -27,11 +27,8 @@ class Modal(ListPanel): _stylesheets: ClassVar[list[str]] = [f"{CDN_DIST}css/models/modal.css"] _rename: ClassVar[Mapping[str, str | None]] = {} - def __init__(self, *objects, **params): - super().__init__(*objects, **params) - def open(self): - self._send_event(ModalDialogEvent) + self._send_event(ModalDialogEvent, open=True) - # def close(self): - # self._send_event(ModalDialogEvent, open=False) + def close(self): + self._send_event(ModalDialogEvent, open=False) diff --git a/panel/models/layout.py b/panel/models/layout.py index 5fb656c59f..b11e4109e5 100644 --- a/panel/models/layout.py +++ b/panel/models/layout.py @@ -1,3 +1,5 @@ +from typing import Any + from bokeh.core.properties import ( Bool, Int, List, Nullable, String, ) @@ -10,6 +12,7 @@ "HTMLBox", "Column", "Modal", + "ModalDialogEvent", ) @@ -99,3 +102,9 @@ class Modal(Column): class ModalDialogEvent(ModelEvent): event_name = 'modal-dialog-event' + def __init__(self, model: ModelEvent, open: bool): + self.open = open + super().__init__(model=model) + + def event_values(self) -> dict[str, Any]: + return dict(super().event_values(), open=self.open) diff --git a/panel/models/modal.ts b/panel/models/modal.ts index 927a023e07..cfcc16eac5 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -4,25 +4,35 @@ import {div, button} from "@bokehjs/core/dom" import {ModelEvent, server_event} from "@bokehjs/core/bokeh_events" import type {Attrs} from "@bokehjs/core/types" +//type A11yDialogView = { +// addSignalListener(event: unknown, listener: unknown): void +// finalize(): void +// on(event: string, listener: () => void): void +// new +//} +// +//type A11yDialog = (container: HTMLElement, embed_data: unknown, config: unknown) => Promise<{view: A11yDialogView}> +//declare const A11yDialog: A11yDialog @server_event("modal-dialog-event") export class ModalDialogEvent extends ModelEvent { - constructor() { + open: boolean + + constructor(open: boolean) { super() - //this.open = open + this.open = open } protected override get event_values(): Attrs { - return {} // open: this.open} + return {open: this.open} } - static override from_values() { - return new ModalDialogEvent() - + static override from_values(values: object) { + const {open} = values as {open: boolean} + return new ModalDialogEvent(open) } } - export class ModalView extends BkColumnView { declare model: Modal @@ -36,7 +46,11 @@ export class ModalView extends BkColumnView { this.on_change([children], this.update) this.on_change([show_close_button], this.update_close_button) this.model.on_event(ModalDialogEvent, (event) => { - this.modal.show() + if (event.open) { + this.modal.show() + } else { + this.modal.hide() + } }) } From 5a82aca41ca8c73c51f6b86da44437302b593bb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 5 Aug 2024 17:32:04 +0200 Subject: [PATCH 10/31] Make is_open readonly --- panel/layout/modal.py | 8 +++++++- panel/models/layout.py | 4 ++-- panel/models/modal.ts | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/panel/layout/modal.py b/panel/layout/modal.py index a5afe02152..4e6842ff7d 100644 --- a/panel/layout/modal.py +++ b/panel/layout/modal.py @@ -18,13 +18,14 @@ class Modal(ListPanel): width = param.Integer(default=None, bounds=(0, None)) - is_open = param.Boolean(default=False, doc="Whether the modal is open.") + is_open = param.Boolean(default=False, readonly=True, doc="Whether the modal is open.") show_close_button = param.Boolean(default=True, doc="Whether to show a close button in the modal.") _bokeh_model: ClassVar[type[Model]] = BkModal _stylesheets: ClassVar[list[str]] = [f"{CDN_DIST}css/models/modal.css"] + _rename: ClassVar[Mapping[str, str | None]] = {} def open(self): @@ -32,3 +33,8 @@ def open(self): def close(self): self._send_event(ModalDialogEvent, open=False) + + def _process_param_change(self, msg): + msg = super()._process_param_change(msg) + msg.pop("is_open", None) + return msg diff --git a/panel/models/layout.py b/panel/models/layout.py index b11e4109e5..4abe574526 100644 --- a/panel/models/layout.py +++ b/panel/models/layout.py @@ -1,7 +1,7 @@ from typing import Any from bokeh.core.properties import ( - Bool, Int, List, Nullable, String, + Bool, Int, List, Nullable, Readonly, String, ) from bokeh.events import ModelEvent from bokeh.models import Column as BkColumn @@ -95,7 +95,7 @@ class Modal(Column): 'exports': {} } - is_open = Bool(False, help="Whether or not the modal is open.") + is_open = Readonly(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") diff --git a/panel/models/modal.ts b/panel/models/modal.ts index cfcc16eac5..a84b5d9419 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -133,7 +133,7 @@ export class Modal extends BkColumn { static { this.prototype.default_view = ModalView this.define(({Bool}) => ({ - is_open: [Bool, false], // TODO: read-only + is_open: [Bool, false], show_close_button: [Bool, true], })) } From e3c8e756bea7006b67d5a50b0c3e8c77b89e85a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 5 Aug 2024 18:00:46 +0200 Subject: [PATCH 11/31] Add background_close option --- panel/layout/modal.py | 2 ++ panel/models/layout.py | 3 ++- panel/models/modal.ts | 24 ++++++++++++++---------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/panel/layout/modal.py b/panel/layout/modal.py index 4e6842ff7d..213931d410 100644 --- a/panel/layout/modal.py +++ b/panel/layout/modal.py @@ -22,6 +22,8 @@ class Modal(ListPanel): 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.") + _bokeh_model: ClassVar[type[Model]] = BkModal _stylesheets: ClassVar[list[str]] = [f"{CDN_DIST}css/models/modal.css"] diff --git a/panel/models/layout.py b/panel/models/layout.py index 4abe574526..eb3ab160ba 100644 --- a/panel/models/layout.py +++ b/panel/models/layout.py @@ -96,7 +96,8 @@ class Modal(Column): } is_open = Readonly(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") + 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): diff --git a/panel/models/modal.ts b/panel/models/modal.ts index a84b5d9419..25f5c423bb 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -46,33 +46,35 @@ export class ModalView extends BkColumnView { this.on_change([children], this.update) this.on_change([show_close_button], this.update_close_button) this.model.on_event(ModalDialogEvent, (event) => { - if (event.open) { - this.modal.show() - } else { - this.modal.hide() - } + event.open ? this.modal.show() : this.modal.hide() }) } override render(): void { super.render() + this.create_modal() + } + + create_modal(): void { const container = div({style: {display: "contents"}}) const dialog = div({ id: "pnx_dialog", class: "dialog-container bk-root", "aria-hidden": "true", } as any) - const dialog_overlay = div({ - class: "dialog-overlay", - "data-a11y-dialog-hide": "", - } as any) + + const dialog_overlay = div({class: "dialog-overlay"}) + if (this.model.background_close) { + dialog_overlay.setAttribute("data-a11y-dialog-hide", "") + } + + // TODO: Add width and height here const content = div({ id: "pnx_dialog_content", class: "dialog-content", role: "document", } as any) this.close_button = button({ - content: "Close", id: "pnx_dialog_close", "data-a11y-dialog-hide": "", class: "pnx-dialog-close", @@ -117,6 +119,7 @@ export namespace Modal { export type Props = BkColumn.Props & { is_open: p.Property show_close_button: p.Property + background_close: p.Property } } @@ -135,6 +138,7 @@ export class Modal extends BkColumn { this.define(({Bool}) => ({ is_open: [Bool, false], show_close_button: [Bool, true], + background_close: [Bool, true], })) } } From 1484cc38d212298ce521659851b5914c0c6d4fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 6 Aug 2024 11:01:51 +0200 Subject: [PATCH 12/31] Type A11yDialogView --- panel/models/modal.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/panel/models/modal.ts b/panel/models/modal.ts index 25f5c423bb..fd598f3fcf 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -4,15 +4,14 @@ import {div, button} from "@bokehjs/core/dom" import {ModelEvent, server_event} from "@bokehjs/core/bokeh_events" import type {Attrs} from "@bokehjs/core/types" -//type A11yDialogView = { -// addSignalListener(event: unknown, listener: unknown): void -// finalize(): void -// on(event: string, listener: () => void): void -// new -//} -// -//type A11yDialog = (container: HTMLElement, embed_data: unknown, config: unknown) => Promise<{view: A11yDialogView}> -//declare const A11yDialog: A11yDialog +type A11yDialogView = { + on(event: string, listener: () => void): void + show(): void + hide(): void +} + +type A11yDialog = (container: HTMLElement) => Promise<{view: A11yDialogView}> +declare const A11yDialog: A11yDialog @server_event("modal-dialog-event") export class ModalDialogEvent extends ModelEvent { @@ -36,7 +35,7 @@ export class ModalDialogEvent extends ModelEvent { export class ModalView extends BkColumnView { declare model: Modal - modal: any + modal: A11yDialogView close_button: HTMLButtonElement modal_children: HTMLElement From e68d42fea8b410c2de44e9eb250ed726291592c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 6 Aug 2024 11:14:24 +0200 Subject: [PATCH 13/31] Update render --- panel/models/modal.ts | 51 ++++++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/panel/models/modal.ts b/panel/models/modal.ts index fd598f3fcf..a58c23de07 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -3,6 +3,7 @@ import {Column as BkColumn, ColumnView as BkColumnView} from "@bokehjs/models/la import {div, button} from "@bokehjs/core/dom" import {ModelEvent, server_event} from "@bokehjs/core/bokeh_events" import type {Attrs} from "@bokehjs/core/types" +import {UIElementView} from "@bokehjs/models/ui/ui_element" type A11yDialogView = { on(event: string, listener: () => void): void @@ -41,8 +42,7 @@ export class ModalView extends BkColumnView { override connect_signals(): void { super.connect_signals() - const {children, show_close_button} = this.model.properties - this.on_change([children], this.update) + const {show_close_button} = this.model.properties this.on_change([show_close_button], this.update_close_button) this.model.on_event(ModalDialogEvent, (event) => { event.open ? this.modal.show() : this.modal.hide() @@ -50,8 +50,42 @@ export class ModalView extends BkColumnView { } override render(): void { - super.render() + UIElementView.prototype.render.call(this) + this.class_list.add(...this.css_classes()) this.create_modal() + + for (const child_view of this.child_views) { + this.modal_children.appendChild(child_view.el) + child_view.render() + child_view.after_render() + } + } + + override async update_children(): Promise { + const created = await this.build_child_views() + const created_children = new Set(created) + + // First remove and then either reattach existing elements or render and + // attach new elements, so that the order of children is consistent, while + // avoiding expensive re-rendering of existing views. + for (const child_view of this.child_views) { + child_view.el.remove() + } + + for (const child_view of this.child_views) { + const is_new = created_children.has(child_view) + + const target = child_view.rendering_target() ?? this.shadow_el + if (is_new) { + child_view.render_to(target) + } else { + target.append(child_view.el) + } + } + this.r_after_render() + + this._update_children() + this.invalidate_layout() } create_modal(): void { @@ -89,20 +123,11 @@ export class ModalView extends BkColumnView { this.shadow_el.append(container) this.modal = new (window as any).A11yDialog(dialog) - this.update() + this.update_close_button() this.modal.on("show", () => { this.model.is_open = true }) this.modal.on("hide", () => { this.model.is_open = false }) } - update(): void { - // TODO: clear old children - for (const child of this.children()) { - // FIXME: remove any and look into better method - this.modal_children.append((child as any).el) - } - this.update_close_button() - } - update_close_button(): void { if (this.model.show_close_button) { this.close_button.style.display = "block" From 7145d8bfbfbcaaf7e41acfb6dfbb583dd09340eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 6 Aug 2024 11:24:57 +0200 Subject: [PATCH 14/31] Update close button SVG --- panel/models/modal.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/panel/models/modal.ts b/panel/models/modal.ts index a58c23de07..fcb419ef9f 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -5,6 +5,13 @@ import {ModelEvent, server_event} from "@bokehjs/core/bokeh_events" import type {Attrs} from "@bokehjs/core/types" import {UIElementView} from "@bokehjs/models/ui/ui_element" +const SVG = ` + +` + type A11yDialogView = { on(event: string, listener: () => void): void show(): void @@ -113,6 +120,7 @@ export class ModalView extends BkColumnView { class: "pnx-dialog-close", ariaLabel: "Close this dialog window", } as any) + this.close_button.innerHTML = SVG this.modal_children = div({id: "pnx_modal_object"}) container.append(dialog) From 8a617aa5c1506a30984da83525ac1802f4008b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 6 Aug 2024 11:35:25 +0200 Subject: [PATCH 15/31] Clean up --- panel/models/modal.ts | 28 +++------------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/panel/models/modal.ts b/panel/models/modal.ts index fcb419ef9f..c9a3219cff 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -4,6 +4,7 @@ import {div, button} from "@bokehjs/core/dom" import {ModelEvent, server_event} from "@bokehjs/core/bokeh_events" import type {Attrs} from "@bokehjs/core/types" import {UIElementView} from "@bokehjs/models/ui/ui_element" +import {LayoutDOMView} from "@bokehjs/models/layouts/layout_dom" const SVG = ` @@ -62,37 +63,14 @@ export class ModalView extends BkColumnView { this.create_modal() for (const child_view of this.child_views) { - this.modal_children.appendChild(child_view.el) + this.modal_children.append(child_view.el) child_view.render() child_view.after_render() } } override async update_children(): Promise { - const created = await this.build_child_views() - const created_children = new Set(created) - - // First remove and then either reattach existing elements or render and - // attach new elements, so that the order of children is consistent, while - // avoiding expensive re-rendering of existing views. - for (const child_view of this.child_views) { - child_view.el.remove() - } - - for (const child_view of this.child_views) { - const is_new = created_children.has(child_view) - - const target = child_view.rendering_target() ?? this.shadow_el - if (is_new) { - child_view.render_to(target) - } else { - target.append(child_view.el) - } - } - this.r_after_render() - - this._update_children() - this.invalidate_layout() + await LayoutDOMView.prototype.update_children.call(this) } create_modal(): void { From 256b3375f1d5a63b01c08188eb947259636cc663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 9 Aug 2024 14:21:52 +0200 Subject: [PATCH 16/31] Try updating js and css --- panel/models/layout.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/panel/models/layout.py b/panel/models/layout.py index eb3ab160ba..ea8ba1b9f7 100644 --- a/panel/models/layout.py +++ b/panel/models/layout.py @@ -7,6 +7,9 @@ from bokeh.models import Column as BkColumn from bokeh.models.layouts import LayoutDOM +from ..io.resources import bundled_files +from ..util import classproperty + __all__ = ( "Card", "HTMLBox", @@ -79,20 +82,26 @@ class Card(Column): class Modal(Column): - __css__ = [] - __javascript__ = [ + __javascript_raw__ = [ "https://cdn.jsdelivr.net/npm/a11y-dialog@7/dist/a11y-dialog.min.js" ] - __js_skip__ = { - # 'modal': __javascript__[:1], - } + @classproperty + def __javascript__(cls): + return bundled_files(cls) + + @classproperty + def __js_skip__(cls): + return {'A11yDialog': cls.__javascript__[:1]} + __js_require__ = { 'paths': { - 'modal': "https://cdn.jsdelivr.net/npm/a11y-dialog@7/dist/a11y-dialog.min", + 'a11y-dialog': "https://cdn.jsdelivr.net/npm/a11y-dialog@7/dist/a11y-dialog.min", }, - 'exports': {} + 'exports': { + 'A11yDialog': 'a11y-dialog', + } } is_open = Readonly(Bool, default=False, help="Whether or not the modal is open.") From df0140ac9702edd3b4dae0a9565106f3410f5fc9 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 9 Aug 2024 16:58:41 +0200 Subject: [PATCH 17/31] Require modal extension call --- panel/config.py | 1 + panel/layout/modal.py | 20 ++++++++++++---- panel/models/__init__.py | 2 +- panel/models/layout.py | 49 +------------------------------------ panel/models/modal.py | 52 ++++++++++++++++++++++++++++++++++++++++ panel/models/modal.ts | 2 +- 6 files changed, 72 insertions(+), 54 deletions(-) create mode 100644 panel/models/modal.py diff --git a/panel/config.py b/panel/config.py index 5f7955d03f..f9e6d26ee8 100644 --- a/panel/config.py +++ b/panel/config.py @@ -668,6 +668,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', diff --git a/panel/layout/modal.py b/panel/layout/modal.py index 213931d410..fadafc8a83 100644 --- a/panel/layout/modal.py +++ b/panel/layout/modal.py @@ -1,19 +1,24 @@ from __future__ import annotations -from typing import TYPE_CHECKING, ClassVar, Mapping +from typing import ( + TYPE_CHECKING, ClassVar, Mapping, Optional, +) import param from ..io.resources import CDN_DIST -from ..models import Modal as BkModal from ..models.layout 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, JupyterComm class Modal(ListPanel): + height = param.Integer(default=None, bounds=(0, None)) width = param.Integer(default=None, bounds=(0, None)) @@ -24,12 +29,19 @@ class Modal(ListPanel): background_close = param.Boolean(default=True, doc="Whether to enable closing the modal when clicking the background.") - _bokeh_model: ClassVar[type[Model]] = BkModal - _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: + self._bokeh_model = lazy_load( + 'panel.models.modal', 'Modal', isinstance(comm, JupyterComm), root + ) + return super()._get_model(doc, root, parent, comm) + def open(self): self._send_event(ModalDialogEvent, open=True) diff --git a/panel/models/__init__.py b/panel/models/__init__.py index 69debd68e0..c00045f5bd 100644 --- a/panel/models/__init__.py +++ b/panel/models/__init__.py @@ -10,7 +10,7 @@ from .feed import Feed # noqa from .icon import ButtonIcon, ToggleIcon, _ClickableIcon # noqa from .ipywidget import IPyWidget # noqa -from .layout import Card, Column, Modal # noqa +from .layout import Card, Column # noqa from .location import Location # noqa from .markup import HTML, JSON, PDF # noqa from .reactive_html import ReactiveHTML # noqa diff --git a/panel/models/layout.py b/panel/models/layout.py index ea8ba1b9f7..98cae44570 100644 --- a/panel/models/layout.py +++ b/panel/models/layout.py @@ -1,21 +1,13 @@ -from typing import Any - from bokeh.core.properties import ( - Bool, Int, List, Nullable, Readonly, String, + Bool, Int, List, Nullable, String, ) -from bokeh.events import ModelEvent from bokeh.models import Column as BkColumn from bokeh.models.layouts import LayoutDOM -from ..io.resources import bundled_files -from ..util import classproperty - __all__ = ( "Card", "HTMLBox", "Column", - "Modal", - "ModalDialogEvent", ) @@ -79,42 +71,3 @@ class Card(Column): hide_header = Bool(False, help="Whether to hide the Card header") tag = String("tag", help="CSS class to use for the Card as a whole.") - - -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', - } - } - - is_open = Readonly(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: ModelEvent, open: bool): - self.open = open - super().__init__(model=model) - - def event_values(self) -> dict[str, Any]: - return dict(super().event_values(), open=self.open) diff --git a/panel/models/modal.py b/panel/models/modal.py new file mode 100644 index 0000000000..1f6f6b8a8d --- /dev/null +++ b/panel/models/modal.py @@ -0,0 +1,52 @@ +from typing import Any + +from bokeh.core.properties import Bool, Readonly +from bokeh.events import ModelEvent + +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', + } + } + + is_open = Readonly(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: ModelEvent, open: bool): + self.open = open + super().__init__(model=model) + + def event_values(self) -> dict[str, Any]: + return dict(super().event_values(), open=self.open) diff --git a/panel/models/modal.ts b/panel/models/modal.ts index c9a3219cff..a492f6de49 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -142,7 +142,7 @@ export class Modal extends BkColumn { super(attrs) } - static override __module__ = "panel.models.layout" + static override __module__ = "panel.models.modal" static { this.prototype.default_view = ModalView this.define(({Bool}) => ({ From 09c414d934b93d05e0f24dfd8b27c07af27f1840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 26 Aug 2024 16:16:48 +0200 Subject: [PATCH 18/31] Misc updates --- panel/layout/modal.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/panel/layout/modal.py b/panel/layout/modal.py index fadafc8a83..31c0517683 100644 --- a/panel/layout/modal.py +++ b/panel/layout/modal.py @@ -6,15 +6,17 @@ import param +from pyviz_comms import JupyterComm + from ..io.resources import CDN_DIST -from ..models.layout import ModalDialogEvent +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, JupyterComm + from pyviz_comms import Comm class Modal(ListPanel): @@ -48,6 +50,9 @@ def open(self): def close(self): self._send_event(ModalDialogEvent, open=False) + def toggle(self): + self.close() if self.is_open else self.open() + def _process_param_change(self, msg): msg = super()._process_param_change(msg) msg.pop("is_open", None) From 7f9b12b1eb19f4ea27b490e08434c14eff4008c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 26 Aug 2024 17:24:27 +0200 Subject: [PATCH 19/31] Use open as parameter, show / hide / toggle as function call --- panel/layout/modal.py | 15 +++++++++------ panel/models/modal.py | 4 ++-- panel/models/modal.ts | 8 ++++---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/panel/layout/modal.py b/panel/layout/modal.py index 31c0517683..5a067317dc 100644 --- a/panel/layout/modal.py +++ b/panel/layout/modal.py @@ -20,12 +20,11 @@ class Modal(ListPanel): - height = param.Integer(default=None, bounds=(0, None)) width = param.Integer(default=None, bounds=(0, None)) - is_open = param.Boolean(default=False, readonly=True, doc="Whether the modal is open.") + 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.") @@ -44,16 +43,20 @@ def _get_model( ) return super()._get_model(doc, root, parent, comm) - def open(self): + def show(self): self._send_event(ModalDialogEvent, open=True) - def close(self): + def hide(self): self._send_event(ModalDialogEvent, open=False) def toggle(self): - self.close() if self.is_open else self.open() + self._send_event(ModalDialogEvent, open=not self.open) + + @param.depends("open", watch=True) + def _open(self): + self._send_event(ModalDialogEvent, open=self.open) def _process_param_change(self, msg): msg = super()._process_param_change(msg) - msg.pop("is_open", None) + msg.pop("open", None) return msg diff --git a/panel/models/modal.py b/panel/models/modal.py index 1f6f6b8a8d..cad556b78f 100644 --- a/panel/models/modal.py +++ b/panel/models/modal.py @@ -1,6 +1,6 @@ from typing import Any -from bokeh.core.properties import Bool, Readonly +from bokeh.core.properties import Bool from bokeh.events import ModelEvent from ..io.resources import bundled_files @@ -36,7 +36,7 @@ def __js_skip__(cls): } } - is_open = Readonly(Bool, default=False, help="Whether or not the modal is open.") + 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.") diff --git a/panel/models/modal.ts b/panel/models/modal.ts index a492f6de49..b6594ebca6 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -110,8 +110,8 @@ export class ModalView extends BkColumnView { this.modal = new (window as any).A11yDialog(dialog) this.update_close_button() - this.modal.on("show", () => { this.model.is_open = true }) - this.modal.on("hide", () => { this.model.is_open = false }) + this.modal.on("show", () => { this.model.open = true }) + this.modal.on("hide", () => { this.model.open = false }) } update_close_button(): void { @@ -127,7 +127,7 @@ export namespace Modal { export type Attrs = p.AttrsOf export type Props = BkColumn.Props & { - is_open: p.Property + open: p.Property show_close_button: p.Property background_close: p.Property } @@ -146,7 +146,7 @@ export class Modal extends BkColumn { static { this.prototype.default_view = ModalView this.define(({Bool}) => ({ - is_open: [Bool, false], + open: [Bool, false], show_close_button: [Bool, true], background_close: [Bool, true], })) From c9ada928716a4d8e963620cb7f633743cadd3e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 2 Dec 2024 12:03:25 +0100 Subject: [PATCH 20/31] Use a symbol instead of SVG for close button --- panel/models/modal.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/panel/models/modal.ts b/panel/models/modal.ts index b6594ebca6..d5358dbf47 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -6,13 +6,6 @@ import type {Attrs} from "@bokehjs/core/types" import {UIElementView} from "@bokehjs/models/ui/ui_element" import {LayoutDOMView} from "@bokehjs/models/layouts/layout_dom" -const SVG = ` - -` - type A11yDialogView = { on(event: string, listener: () => void): void show(): void @@ -98,7 +91,7 @@ export class ModalView extends BkColumnView { class: "pnx-dialog-close", ariaLabel: "Close this dialog window", } as any) - this.close_button.innerHTML = SVG + this.close_button.innerHTML = "✕" this.modal_children = div({id: "pnx_modal_object"}) container.append(dialog) From 06aee3f3efb9ebd2526b37c5dc94e61782d76e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 2 Dec 2024 15:24:52 +0100 Subject: [PATCH 21/31] First show the content on modal on click --- panel/models/modal.ts | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/panel/models/modal.ts b/panel/models/modal.ts index d5358dbf47..e93504d8e2 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -54,12 +54,6 @@ export class ModalView extends BkColumnView { UIElementView.prototype.render.call(this) this.class_list.add(...this.css_classes()) this.create_modal() - - for (const child_view of this.child_views) { - this.modal_children.append(child_view.el) - child_view.render() - child_view.after_render() - } } override async update_children(): Promise { @@ -72,6 +66,7 @@ export class ModalView extends BkColumnView { id: "pnx_dialog", class: "dialog-container bk-root", "aria-hidden": "true", + style: {display: "none"}, } as any) const dialog_overlay = div({class: "dialog-overlay"}) @@ -85,6 +80,11 @@ export class ModalView extends BkColumnView { class: "dialog-content", role: "document", } as any) + for (const child_view of this.child_views) { + const target = child_view.rendering_target() ?? content + child_view.render_to(target) + } + this.close_button = button({ id: "pnx_dialog_close", "data-a11y-dialog-hide": "", @@ -100,11 +100,22 @@ export class ModalView extends BkColumnView { content.append(this.close_button) content.append(this.modal_children) this.shadow_el.append(container) + let first_open = false this.modal = new (window as any).A11yDialog(dialog) this.update_close_button() - this.modal.on("show", () => { this.model.open = true }) - this.modal.on("hide", () => { this.model.open = false }) + this.modal.on("show", () => { + this.model.open = true + dialog.style.display = "" + if (!first_open) { + requestAnimationFrame(() => { this.invalidate_layout() }) + first_open = true + } + }) + this.modal.on("hide", () => { + this.model.open = false + dialog.style.display = "none" + }) } update_close_button(): void { From cf3414ea39b18d951b73e02380237911ffbbcd32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 2 Dec 2024 15:26:45 +0100 Subject: [PATCH 22/31] remove modal_children --- panel/models/modal.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/panel/models/modal.ts b/panel/models/modal.ts index e93504d8e2..21ec9227ef 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -39,7 +39,6 @@ export class ModalView extends BkColumnView { modal: A11yDialogView close_button: HTMLButtonElement - modal_children: HTMLElement override connect_signals(): void { super.connect_signals() @@ -92,13 +91,11 @@ export class ModalView extends BkColumnView { ariaLabel: "Close this dialog window", } as any) this.close_button.innerHTML = "✕" - this.modal_children = div({id: "pnx_modal_object"}) container.append(dialog) dialog.append(dialog_overlay) dialog.append(content) content.append(this.close_button) - content.append(this.modal_children) this.shadow_el.append(container) let first_open = false From 1bb3c55fc2604f592942b5429c2e938a30d23b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 2 Dec 2024 16:27:39 +0100 Subject: [PATCH 23/31] Use model sizes for dialog_content --- panel/models/modal.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/panel/models/modal.ts b/panel/models/modal.ts index 21ec9227ef..05c8a7a7eb 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -4,6 +4,7 @@ import {div, button} from "@bokehjs/core/dom" import {ModelEvent, server_event} from "@bokehjs/core/bokeh_events" import type {Attrs} from "@bokehjs/core/types" import {UIElementView} from "@bokehjs/models/ui/ui_element" +import {isNumber} from "@bokehjs/core/util/types" import {LayoutDOMView} from "@bokehjs/models/layouts/layout_dom" type A11yDialogView = { @@ -73,11 +74,21 @@ export class ModalView extends BkColumnView { dialog_overlay.setAttribute("data-a11y-dialog-hide", "") } - // TODO: Add width and height here + const {height, width, min_height, min_width, max_height, max_width} = this.model + const convert_fn = (size: any) => { return isNumber(size) ? `${size}px` : size } const content = div({ id: "pnx_dialog_content", class: "dialog-content", role: "document", + style: { + height: convert_fn(height), + width: convert_fn(width), + min_height: convert_fn(min_height), + min_width: convert_fn(min_width), + max_height: convert_fn(max_height), + max_width: convert_fn(max_width), + overflow: "auto", + }, } as any) for (const child_view of this.child_views) { const target = child_view.rendering_target() ?? content From e7844958fab463d7fb6947c68d59a5cf8c9d6a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 2 Dec 2024 17:49:24 +0100 Subject: [PATCH 24/31] Simplify logic and enable option to show modal at start --- panel/layout/modal.py | 11 +++-------- panel/models/modal.ts | 2 ++ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/panel/layout/modal.py b/panel/layout/modal.py index 5a067317dc..a9edf9ff36 100644 --- a/panel/layout/modal.py +++ b/panel/layout/modal.py @@ -44,19 +44,14 @@ def _get_model( return super()._get_model(doc, root, parent, comm) def show(self): - self._send_event(ModalDialogEvent, open=True) + self.open = True def hide(self): - self._send_event(ModalDialogEvent, open=False) + self.open = False def toggle(self): - self._send_event(ModalDialogEvent, open=not self.open) + self.open = not self.open @param.depends("open", watch=True) def _open(self): self._send_event(ModalDialogEvent, open=self.open) - - def _process_param_change(self, msg): - msg = super()._process_param_change(msg) - msg.pop("open", None) - return msg diff --git a/panel/models/modal.ts b/panel/models/modal.ts index 05c8a7a7eb..735603cbd0 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -124,6 +124,8 @@ export class ModalView extends BkColumnView { this.model.open = false dialog.style.display = "none" }) + + if (this.model.open) { this.modal.show() } } update_close_button(): void { From 52454dc9b9a4ba34f5b5cce5953db73090e1776d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 3 Dec 2024 09:53:26 +0100 Subject: [PATCH 25/31] Set host class to have no size --- panel/dist/css/models/modal.css | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/panel/dist/css/models/modal.css b/panel/dist/css/models/modal.css index ee6dcc5ce1..46a35d15c8 100644 --- a/panel/dist/css/models/modal.css +++ b/panel/dist/css/models/modal.css @@ -1,3 +1,8 @@ +:host(.bk-panel-models-modal-Modal) { + width: 0; + height: 0; +} + .dialog-container, .dialog-overlay { position: fixed; @@ -10,7 +15,7 @@ z-index: 100002; display: flex; } -.dialog-container[aria-hidden='true'] { +.dialog-container[aria-hidden="true"] { display: none; } .dialog-overlay { From 1e1ded7c3ffcf54f7bd1320ec7084fd2473d3af8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 3 Dec 2024 10:00:04 +0100 Subject: [PATCH 26/31] Remove container --- panel/models/modal.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/panel/models/modal.ts b/panel/models/modal.ts index 735603cbd0..591a9cc2b1 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -61,7 +61,6 @@ export class ModalView extends BkColumnView { } create_modal(): void { - const container = div({style: {display: "contents"}}) const dialog = div({ id: "pnx_dialog", class: "dialog-container bk-root", @@ -103,11 +102,10 @@ export class ModalView extends BkColumnView { } as any) this.close_button.innerHTML = "✕" - container.append(dialog) dialog.append(dialog_overlay) dialog.append(content) content.append(this.close_button) - this.shadow_el.append(container) + this.shadow_el.append(dialog) let first_open = false this.modal = new (window as any).A11yDialog(dialog) From 5e82f98696933c57c96ca860912db33cf2c6a000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 3 Dec 2024 11:38:37 +0100 Subject: [PATCH 27/31] Remove window as any --- panel/models/modal.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/panel/models/modal.ts b/panel/models/modal.ts index 591a9cc2b1..02f5d36c9a 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -7,14 +7,14 @@ import {UIElementView} from "@bokehjs/models/ui/ui_element" import {isNumber} from "@bokehjs/core/util/types" import {LayoutDOMView} from "@bokehjs/models/layouts/layout_dom" -type A11yDialogView = { +declare type A11yDialogView = { on(event: string, listener: () => void): void show(): void hide(): void } -type A11yDialog = (container: HTMLElement) => Promise<{view: A11yDialogView}> -declare const A11yDialog: A11yDialog +declare interface A11yDialogInterface { new (container: HTMLElement): A11yDialogView } +declare const A11yDialog: A11yDialogInterface @server_event("modal-dialog-event") export class ModalDialogEvent extends ModelEvent { @@ -108,7 +108,7 @@ export class ModalView extends BkColumnView { this.shadow_el.append(dialog) let first_open = false - this.modal = new (window as any).A11yDialog(dialog) + this.modal = new A11yDialog(dialog) this.update_close_button() this.modal.on("show", () => { this.model.open = true From 59bba8ac0bc193ffa4d45cb41ce844a29f1b7992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 3 Dec 2024 11:39:02 +0100 Subject: [PATCH 28/31] clean ups --- panel/models/modal.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/panel/models/modal.ts b/panel/models/modal.ts index 02f5d36c9a..79edc0d25c 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -64,7 +64,7 @@ export class ModalView extends BkColumnView { const dialog = div({ id: "pnx_dialog", class: "dialog-container bk-root", - "aria-hidden": "true", + "aria-hidden": true, style: {display: "none"}, } as any) @@ -74,18 +74,17 @@ export class ModalView extends BkColumnView { } const {height, width, min_height, min_width, max_height, max_width} = this.model - const convert_fn = (size: any) => { return isNumber(size) ? `${size}px` : size } const content = div({ id: "pnx_dialog_content", class: "dialog-content", role: "document", style: { - height: convert_fn(height), - width: convert_fn(width), - min_height: convert_fn(min_height), - min_width: convert_fn(min_width), - max_height: convert_fn(max_height), - max_width: convert_fn(max_width), + height: isNumber(height) ? `${height}px` : height, + width: isNumber(width) ? `${width}px` : width, + min_height: isNumber(min_height) ? `${min_height}px` : min_height, + min_width: isNumber(min_width) ? `${min_width}px` : min_width, + max_height: isNumber(max_height) ? `${max_height}px` : max_height, + max_width: isNumber(max_width) ? `${max_width}px` : max_width, overflow: "auto", }, } as any) From 9d531b10f1f1cbad94caec18043a3e3f15f15cc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 3 Dec 2024 12:15:17 +0100 Subject: [PATCH 29/31] Fix types --- panel/layout/modal.py | 2 +- panel/models/modal.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/panel/layout/modal.py b/panel/layout/modal.py index a9edf9ff36..1dbc9e49d9 100644 --- a/panel/layout/modal.py +++ b/panel/layout/modal.py @@ -38,7 +38,7 @@ def _get_model( self, doc: Document, root: Optional[Model] = None, parent: Optional[Model] = None, comm: Optional[Comm] = None ) -> Model: - self._bokeh_model = lazy_load( + Modal._bokeh_model = lazy_load( 'panel.models.modal', 'Modal', isinstance(comm, JupyterComm), root ) return super()._get_model(doc, root, parent, comm) diff --git a/panel/models/modal.py b/panel/models/modal.py index cad556b78f..5083751d0e 100644 --- a/panel/models/modal.py +++ b/panel/models/modal.py @@ -2,6 +2,7 @@ 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 @@ -44,7 +45,7 @@ def __js_skip__(cls): class ModalDialogEvent(ModelEvent): event_name = 'modal-dialog-event' - def __init__(self, model: ModelEvent, open: bool): + def __init__(self, model: Model | None, open: bool): self.open = open super().__init__(model=model) From 77e5adc387b51ec07a03e55729b7e5ede8c85d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 3 Dec 2024 15:39:30 +0100 Subject: [PATCH 30/31] More clean up --- panel/dist/css/models/modal.css | 3 --- panel/layout/__init__.py | 2 ++ panel/layout/modal.py | 6 ++---- panel/models/modal.ts | 3 +-- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/panel/dist/css/models/modal.css b/panel/dist/css/models/modal.css index 46a35d15c8..c4652dd8b1 100644 --- a/panel/dist/css/models/modal.css +++ b/panel/dist/css/models/modal.css @@ -15,9 +15,6 @@ 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); diff --git a/panel/layout/__init__.py b/panel/layout/__init__.py index 90fc7e4b48..e5a719611c 100644 --- a/panel/layout/__init__.py +++ b/panel/layout/__init__.py @@ -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, ) @@ -58,6 +59,7 @@ "HSpacer", "ListLike", "ListPanel", + "Modal", "Panel", "Row", "Spacer", diff --git a/panel/layout/modal.py b/panel/layout/modal.py index 1dbc9e49d9..15e583f385 100644 --- a/panel/layout/modal.py +++ b/panel/layout/modal.py @@ -1,7 +1,7 @@ from __future__ import annotations from typing import ( - TYPE_CHECKING, ClassVar, Mapping, Optional, + TYPE_CHECKING, ClassVar, Literal, Mapping, Optional, ) import param @@ -20,9 +20,7 @@ class Modal(ListPanel): - height = param.Integer(default=None, bounds=(0, None)) - - width = param.Integer(default=None, bounds=(0, None)) + """Create a modal dialog that can be opened and closed.""" open = param.Boolean(default=False, doc="Whether to open the modal.") diff --git a/panel/models/modal.ts b/panel/models/modal.ts index 79edc0d25c..4a64ac1063 100644 --- a/panel/models/modal.ts +++ b/panel/models/modal.ts @@ -64,9 +64,8 @@ export class ModalView extends BkColumnView { const dialog = div({ id: "pnx_dialog", class: "dialog-container bk-root", - "aria-hidden": true, style: {display: "none"}, - } as any) + }) const dialog_overlay = div({class: "dialog-overlay"}) if (this.model.background_close) { From 8235a48d7c13f11185077b43543de1c6761aef2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 3 Dec 2024 15:39:54 +0100 Subject: [PATCH 31/31] Add convenience method for creating button --- panel/layout/modal.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/panel/layout/modal.py b/panel/layout/modal.py index 15e583f385..1ee93b72cd 100644 --- a/panel/layout/modal.py +++ b/panel/layout/modal.py @@ -53,3 +53,20 @@ def toggle(self): @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